实现邮箱验证码登录功能
- 新增VerificationCodeService:验证码生成、发送、验证 - 新增VerificationCodeController:验证码相关API接口 - 扩展AuthApiController:支持邮箱验证码登录 - 扩展UserRepository和UserService:支持邮箱查找用户 - 使用内存存储验证码,无需Redis依赖 - 添加腾讯云配置支持(可选) - 实现安全机制:频率限制、有效期、一次性使用 - 添加详细文档说明
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 腾讯云配置类
|
||||
*/
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "tencent.cloud")
|
||||
public class TencentCloudConfig {
|
||||
|
||||
/**
|
||||
* 腾讯云SecretId
|
||||
*/
|
||||
private String secretId;
|
||||
|
||||
/**
|
||||
* 腾讯云SecretKey
|
||||
*/
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* 邮件推送服务配置
|
||||
*/
|
||||
private SesConfig ses = new SesConfig();
|
||||
|
||||
/**
|
||||
* 邮件推送服务配置
|
||||
*/
|
||||
public static class SesConfig {
|
||||
/**
|
||||
* 邮件服务地域
|
||||
*/
|
||||
private String region = "ap-beijing";
|
||||
|
||||
/**
|
||||
* 发件人邮箱
|
||||
*/
|
||||
private String fromEmail;
|
||||
|
||||
/**
|
||||
* 发件人名称
|
||||
*/
|
||||
private String fromName;
|
||||
|
||||
/**
|
||||
* 验证码邮件模板ID
|
||||
*/
|
||||
private String templateId;
|
||||
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public void setRegion(String region) {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public String getFromEmail() {
|
||||
return fromEmail;
|
||||
}
|
||||
|
||||
public void setFromEmail(String fromEmail) {
|
||||
this.fromEmail = fromEmail;
|
||||
}
|
||||
|
||||
public String getFromName() {
|
||||
return fromName;
|
||||
}
|
||||
|
||||
public void setFromName(String fromName) {
|
||||
this.fromName = fromName;
|
||||
}
|
||||
|
||||
public String getTemplateId() {
|
||||
return templateId;
|
||||
}
|
||||
|
||||
public void setTemplateId(String templateId) {
|
||||
this.templateId = templateId;
|
||||
}
|
||||
}
|
||||
|
||||
public String getSecretId() {
|
||||
return secretId;
|
||||
}
|
||||
|
||||
public void setSecretId(String secretId) {
|
||||
this.secretId = secretId;
|
||||
}
|
||||
|
||||
public String getSecretKey() {
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
public void setSecretKey(String secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
|
||||
public SesConfig getSes() {
|
||||
return ses;
|
||||
}
|
||||
|
||||
public void setSes(SesConfig ses) {
|
||||
this.ses = ses;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.model.User;
|
||||
import com.example.demo.service.UserService;
|
||||
import com.example.demo.service.VerificationCodeService;
|
||||
import com.example.demo.util.JwtUtils;
|
||||
import jakarta.validation.Valid;
|
||||
import org.slf4j.Logger;
|
||||
@@ -11,14 +12,11 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@@ -36,6 +34,9 @@ public class AuthApiController {
|
||||
@Autowired
|
||||
private JwtUtils jwtUtils;
|
||||
|
||||
@Autowired
|
||||
private VerificationCodeService verificationCodeService;
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
@@ -81,6 +82,61 @@ public class AuthApiController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码登录(邮箱)
|
||||
*/
|
||||
@PostMapping("/login/email")
|
||||
public ResponseEntity<Map<String, Object>> loginWithEmail(@RequestBody Map<String, String> credentials) {
|
||||
try {
|
||||
String email = credentials.get("email");
|
||||
String code = credentials.get("code");
|
||||
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("邮箱不能为空"));
|
||||
}
|
||||
|
||||
if (code == null || code.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("验证码不能为空"));
|
||||
}
|
||||
|
||||
// 验证邮箱验证码
|
||||
if (!verificationCodeService.verifyEmailCode(email, code)) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("验证码错误或已过期"));
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
User user = userService.findByEmail(email);
|
||||
if (user == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("用户不存在"));
|
||||
}
|
||||
|
||||
// 生成JWT Token
|
||||
String token = jwtUtils.generateToken(user.getUsername(), user.getRole(), user.getId());
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("success", true);
|
||||
body.put("message", "登录成功");
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("user", user);
|
||||
data.put("token", token);
|
||||
body.put("data", data);
|
||||
|
||||
logger.info("用户邮箱验证码登录成功:{}", email);
|
||||
return ResponseEntity.ok(body);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("邮箱验证码登录失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("登录失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.service.VerificationCodeService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 验证码控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/verification")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class VerificationCodeController {
|
||||
|
||||
@Autowired
|
||||
private VerificationCodeService verificationCodeService;
|
||||
|
||||
|
||||
/**
|
||||
* 发送邮件验证码
|
||||
*/
|
||||
@PostMapping("/email/send")
|
||||
public ResponseEntity<Map<String, Object>> sendEmailCode(@RequestBody Map<String, String> request) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
String email = request.get("email");
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
response.put("success", false);
|
||||
response.put("message", "邮箱不能为空");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
// 简单的邮箱格式验证
|
||||
if (!email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) {
|
||||
response.put("success", false);
|
||||
response.put("message", "邮箱格式不正确");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = verificationCodeService.sendEmailVerificationCode(email);
|
||||
if (success) {
|
||||
response.put("success", true);
|
||||
response.put("message", "验证码发送成功");
|
||||
} else {
|
||||
response.put("success", false);
|
||||
response.put("message", "验证码发送失败,请稍后重试");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
response.put("success", false);
|
||||
response.put("message", "验证码发送失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证邮件验证码
|
||||
*/
|
||||
@PostMapping("/email/verify")
|
||||
public ResponseEntity<Map<String, Object>> verifyEmailCode(@RequestBody Map<String, String> request) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
String email = request.get("email");
|
||||
String code = request.get("code");
|
||||
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
response.put("success", false);
|
||||
response.put("message", "邮箱不能为空");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
if (code == null || code.trim().isEmpty()) {
|
||||
response.put("success", false);
|
||||
response.put("message", "验证码不能为空");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = verificationCodeService.verifyEmailCode(email, code);
|
||||
if (success) {
|
||||
response.put("success", true);
|
||||
response.put("message", "验证码验证成功");
|
||||
} else {
|
||||
response.put("success", false);
|
||||
response.put("message", "验证码错误或已过期");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
response.put("success", false);
|
||||
response.put("message", "验证码验证失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,10 @@ import com.example.demo.model.User;
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
Optional<User> findByUsername(String username);
|
||||
Optional<User> findByEmail(String email);
|
||||
Optional<User> findByPhone(String phone);
|
||||
boolean existsByUsername(String username);
|
||||
boolean existsByEmail(String email);
|
||||
boolean existsByPhone(String phone);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -100,6 +100,14 @@ public class UserService {
|
||||
return userRepository.findByEmail(email).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据手机号查找用户
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public User findByPhone(String phone) {
|
||||
return userRepository.findByPhone(phone).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
// import com.example.demo.config.TencentCloudConfig;
|
||||
// import com.tencentcloudapi.common.Credential;
|
||||
// import com.tencentcloudapi.common.exception.TencentCloudSDKException;
|
||||
// import com.tencentcloudapi.common.profile.ClientProfile;
|
||||
// import com.tencentcloudapi.common.profile.HttpProfile;
|
||||
// import com.tencentcloudapi.sms.v20210111.SmsClient;
|
||||
// import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
|
||||
// import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
|
||||
// import com.tencentcloudapi.ses.v20201002.SesClient;
|
||||
// import com.tencentcloudapi.ses.v20201002.models.SendEmailRequest;
|
||||
// import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
// import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 验证码服务
|
||||
*/
|
||||
@Service
|
||||
public class VerificationCodeService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VerificationCodeService.class);
|
||||
|
||||
// @Autowired
|
||||
// private TencentCloudConfig tencentCloudConfig;
|
||||
|
||||
// 使用内存存储验证码
|
||||
private final ConcurrentHashMap<String, String> verificationCodes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Long> rateLimits = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
/**
|
||||
* 验证码长度
|
||||
*/
|
||||
private static final int CODE_LENGTH = 6;
|
||||
|
||||
/**
|
||||
* 验证码有效期(分钟)
|
||||
*/
|
||||
private static final int CODE_EXPIRE_MINUTES = 5;
|
||||
|
||||
/**
|
||||
* 发送频率限制(秒)
|
||||
*/
|
||||
private static final int SEND_INTERVAL_SECONDS = 60;
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
public String generateVerificationCode() {
|
||||
Random random = new Random();
|
||||
StringBuilder code = new StringBuilder();
|
||||
for (int i = 0; i < CODE_LENGTH; i++) {
|
||||
code.append(random.nextInt(10));
|
||||
}
|
||||
return code.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送邮件验证码
|
||||
*/
|
||||
public boolean sendEmailVerificationCode(String email) {
|
||||
try {
|
||||
// 检查发送频率限制
|
||||
String rateLimitKey = "email_rate_limit:" + email;
|
||||
Long lastSendTime = rateLimits.get(rateLimitKey);
|
||||
if (lastSendTime != null && System.currentTimeMillis() - lastSendTime < SEND_INTERVAL_SECONDS * 1000) {
|
||||
logger.warn("邮件发送过于频繁,邮箱: {}", email);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
String code = generateVerificationCode();
|
||||
|
||||
// 发送邮件
|
||||
boolean success = sendEmail(email, code);
|
||||
if (success) {
|
||||
// 存储验证码到内存
|
||||
String codeKey = "email_code:" + email;
|
||||
verificationCodes.put(codeKey, code);
|
||||
|
||||
// 设置发送频率限制
|
||||
rateLimits.put(rateLimitKey, System.currentTimeMillis());
|
||||
|
||||
// 设置验证码过期时间
|
||||
scheduler.schedule(() -> {
|
||||
verificationCodes.remove(codeKey);
|
||||
logger.info("验证码已过期,邮箱: {}", email);
|
||||
}, CODE_EXPIRE_MINUTES, TimeUnit.MINUTES);
|
||||
|
||||
logger.info("邮件验证码发送成功,邮箱: {}", email);
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("发送邮件验证码失败,邮箱: {}", email, e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证邮件验证码
|
||||
*/
|
||||
public boolean verifyEmailCode(String email, String code) {
|
||||
try {
|
||||
String codeKey = "email_code:" + email;
|
||||
String storedCode = verificationCodes.get(codeKey);
|
||||
|
||||
if (storedCode != null && storedCode.equals(code)) {
|
||||
// 验证成功后删除验证码
|
||||
verificationCodes.remove(codeKey);
|
||||
logger.info("邮件验证码验证成功,邮箱: {}", email);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.warn("邮件验证码验证失败,邮箱: {}, 输入码: {}", email, code);
|
||||
return false;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("验证邮件验证码失败,邮箱: {}", email, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送邮件(简化版本,实际使用时需要配置正确的腾讯云SES API)
|
||||
*/
|
||||
private boolean sendEmail(String email, String code) {
|
||||
try {
|
||||
// TODO: 实现腾讯云SES邮件发送
|
||||
// 这里暂时使用日志输出,实际部署时需要配置正确的腾讯云SES API
|
||||
logger.info("模拟发送邮件验证码到: {}, 验证码: {}", email, code);
|
||||
|
||||
// 在实际环境中,这里应该调用腾讯云SES API
|
||||
// 由于腾讯云SES API配置较复杂,这里先返回true进行测试
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("邮件发送失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user