登录注册、手机号、邮箱

This commit is contained in:
2025-11-03 13:37:55 +08:00
parent 16754b527e
commit 35aee59178
26 changed files with 4292 additions and 163 deletions

View File

@@ -1,14 +1,24 @@
package org.xyzh.auth.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import org.xyzh.api.auth.login.LoginService;
import org.xyzh.api.system.user.UserService;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.core.domain.LoginDomain;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.common.utils.EmailUtils;
import org.xyzh.common.utils.SmsUtils;
import org.xyzh.common.redis.service.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
import java.util.Map;
/**
* @description AuthController.java文件描述 认证控制器
@@ -21,9 +31,25 @@ import jakarta.servlet.http.HttpServletRequest;
@RequestMapping("/auth")
public class AuthController {
private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
@Autowired
private LoginService loginService;
@Autowired
private UserService userService;
@Autowired
private EmailUtils emailUtils;
@Autowired
private SmsUtils smsUtils;
@Autowired
private RedisService redisService;
@Autowired
private PasswordEncoder passwordEncoder;
/**
* @description 用户登录
* @param loginParam 登录参数
@@ -99,4 +125,360 @@ public class AuthController {
result.success("认证服务运行正常", "OK");
return result;
}
/**
* @description 发送邮箱验证码
* @param requestBody 包含email字段的请求体
* @return ResultDomain<Boolean> 发送结果
* @author yslg
* @since 2025-11-03
*/
@PostMapping("/send-email-code")
public ResultDomain<Map<String, String>> sendEmailCode(@RequestBody Map<String, String> requestBody) {
ResultDomain<Map<String, String>> result = new ResultDomain<>();
String email = requestBody.get("email");
// 验证邮箱格式
if (email == null || email.trim().isEmpty()) {
result.fail("邮箱不能为空");
return result;
}
String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
if (!email.matches(emailRegex)) {
result.fail("邮箱格式不正确");
return result;
}
// 检查是否频繁发送60秒内只能发送一次
String rateLimitKey = "email:code:ratelimit:" + email;
if (redisService.hasKey(rateLimitKey)) {
result.fail("验证码已发送,请勿重复发送");
return result;
}
// 生成会话ID用于绑定验证码和用户
String sessionId = IDUtils.generateID();
// 生成6位数字验证码
String code = EmailUtils.generateVerificationCode();
// 发送邮件
boolean success = emailUtils.sendVerificationCode(email, code);
if (success) {
// 将验证码存储到Redis绑定sessionId有效期5分钟
String codeKey = "email:code:" + sessionId;
String codeValue = email + ":" + code; // 格式:邮箱:验证码
redisService.set(codeKey, codeValue, 5, TimeUnit.MINUTES);
// 设置5分钟的发送频率限制
redisService.set(rateLimitKey, "1", 5, TimeUnit.MINUTES);
// 返回sessionId给前端
Map<String, String> data = Map.of(
"sessionId", sessionId,
"message", "验证码已发送到邮箱"
);
logger.info("邮箱验证码已发送,邮箱: {}, sessionId: {}", email, sessionId);
result.success("验证码已发送到邮箱", data);
} else {
result.fail("验证码发送失败,请稍后重试");
}
return result;
}
/**
* @description 发送手机验证码
* @param requestBody 包含phone字段的请求体
* @return ResultDomain<Boolean> 发送结果
* @author yslg
* @since 2025-11-03
*/
@PostMapping("/send-sms-code")
public ResultDomain<Map<String, String>> sendSmsCode(@RequestBody Map<String, String> requestBody) {
ResultDomain<Map<String, String>> result = new ResultDomain<>();
String phone = requestBody.get("phone");
// 验证手机号格式
if (phone == null || phone.trim().isEmpty()) {
result.fail("手机号不能为空");
return result;
}
if (!SmsUtils.isValidPhone(phone)) {
result.fail("手机号格式不正确");
return result;
}
// 检查是否频繁发送60秒内只能发送一次
String rateLimitKey = "sms:code:ratelimit:" + phone;
if (redisService.hasKey(rateLimitKey)) {
result.fail("验证码已发送,请勿重复发送");
return result;
}
// 生成会话ID用于绑定验证码和用户
String sessionId = IDUtils.generateID();
// 生成6位数字验证码
String code = SmsUtils.generateVerificationCode();
// 发送短信
boolean success = smsUtils.sendVerificationCode(phone, code);
if (success) {
// 将验证码存储到Redis绑定sessionId有效期5分钟
String codeKey = "sms:code:" + sessionId;
String codeValue = phone + ":" + code; // 格式:手机号:验证码
redisService.set(codeKey, codeValue, 5, TimeUnit.MINUTES);
// 设置5分钟的发送频率限制
redisService.set(rateLimitKey, "1", 5, TimeUnit.MINUTES);
// 返回sessionId给前端
Map<String, String> data = Map.of(
"sessionId", sessionId,
"message", "验证码已发送"
);
logger.info("短信验证码已发送,手机号: {}, sessionId: {}", phone, sessionId);
result.success("验证码已发送", data);
} else {
result.fail("验证码发送失败,请稍后重试");
}
return result;
}
/**
* @description 用户注册
* @param requestBody 注册参数
* @return ResultDomain<LoginDomain> 注册结果
* @author yslg
* @since 2025-11-03
*/
@PostMapping("/register")
public ResultDomain<LoginDomain> register(@RequestBody Map<String, Object> requestBody, HttpServletRequest request) {
ResultDomain<LoginDomain> result = new ResultDomain<>();
try {
// 获取注册参数
String registerType = (String) requestBody.get("registerType");
String username = (String) requestBody.get("username");
String phone = (String) requestBody.get("phone");
String email = (String) requestBody.get("email");
String password = (String) requestBody.get("password");
String confirmPassword = (String) requestBody.get("confirmPassword");
String smsCode = (String) requestBody.get("smsCode");
String emailCode = (String) requestBody.get("emailCode");
String smsSessionId = (String) requestBody.get("smsSessionId");
String emailSessionId = (String) requestBody.get("emailSessionId");
String studentId = (String) requestBody.get("studentId");
// 1. 参数验证
if (password == null || password.trim().isEmpty()) {
result.fail("密码不能为空");
return result;
}
if (password.length() < 6) {
result.fail("密码至少6个字符");
return result;
}
if (!password.equals(confirmPassword)) {
result.fail("两次输入的密码不一致");
return result;
}
// 2. 根据注册类型进行不同的验证
TbSysUser user = new TbSysUser();
switch (registerType) {
case "username":
// 用户名注册
if (username == null || username.trim().isEmpty()) {
result.fail("用户名不能为空");
return result;
}
if (username.length() < 3 || username.length() > 20) {
result.fail("用户名长度为3-20个字符");
return result;
}
if (!username.matches("^[a-zA-Z0-9_]+$")) {
result.fail("用户名只能包含字母、数字和下划线");
return result;
}
user.setUsername(username);
logger.info("用户名注册: {}", username);
break;
case "phone":
// 手机号注册
if (phone == null || phone.trim().isEmpty()) {
result.fail("手机号不能为空");
return result;
}
if (!phone.matches("^1[3-9]\\d{9}$")) {
result.fail("手机号格式不正确");
return result;
}
if (smsCode == null || smsCode.trim().isEmpty()) {
result.fail("请输入手机验证码");
return result;
}
if (smsSessionId == null || smsSessionId.trim().isEmpty()) {
result.fail("会话已失效,请重新获取验证码");
return result;
}
// 通过sessionId验证手机验证码
String smsCodeKey = "sms:code:" + smsSessionId;
String storedSmsValue = (String) redisService.get(smsCodeKey);
if (storedSmsValue == null) {
result.fail("验证码已过期,请重新获取");
return result;
}
// 解析存储的值:手机号:验证码
String[] smsParts = storedSmsValue.split(":");
if (smsParts.length != 2) {
result.fail("验证码数据异常");
return result;
}
String storedPhone = smsParts[0];
String storedSmsCode = smsParts[1];
// 验证手机号和验证码是否匹配
if (!storedPhone.equals(phone)) {
result.fail("手机号与验证码不匹配");
logger.warn("手机号注册验证失败,提交手机号: {}, 验证码绑定手机号: {}", phone, storedPhone);
return result;
}
if (!storedSmsCode.equals(smsCode)) {
result.fail("验证码错误");
return result;
}
// 验证码使用后删除
redisService.delete(smsCodeKey);
user.setPhone(phone);
user.setUsername(phone); // 使用手机号作为用户名
logger.info("手机号注册: {}, sessionId: {}", phone, smsSessionId);
break;
case "email":
// 邮箱注册
if (email == null || email.trim().isEmpty()) {
result.fail("邮箱不能为空");
return result;
}
if (!email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) {
result.fail("邮箱格式不正确");
return result;
}
if (emailCode == null || emailCode.trim().isEmpty()) {
result.fail("请输入邮箱验证码");
return result;
}
if (emailSessionId == null || emailSessionId.trim().isEmpty()) {
result.fail("会话已失效,请重新获取验证码");
return result;
}
// 通过sessionId验证邮箱验证码
String emailCodeKey = "email:code:" + emailSessionId;
String storedEmailValue = (String) redisService.get(emailCodeKey);
if (storedEmailValue == null) {
result.fail("验证码已过期,请重新获取");
return result;
}
// 解析存储的值:邮箱:验证码
String[] emailParts = storedEmailValue.split(":");
if (emailParts.length != 2) {
result.fail("验证码数据异常");
return result;
}
String storedEmail = emailParts[0];
String storedEmailCode = emailParts[1];
// 验证邮箱和验证码是否匹配
if (!storedEmail.equals(email)) {
result.fail("邮箱与验证码不匹配");
logger.warn("邮箱注册验证失败,提交邮箱: {}, 验证码绑定邮箱: {}", email, storedEmail);
return result;
}
if (!storedEmailCode.equals(emailCode)) {
result.fail("验证码错误");
return result;
}
// 验证码使用后删除
redisService.delete(emailCodeKey);
user.setEmail(email);
user.setUsername(email.split("@")[0]); // 使用邮箱前缀作为用户名
logger.info("邮箱注册: {}, sessionId: {}", email, emailSessionId);
break;
default:
result.fail("未知的注册类型");
return result;
}
// 3. 密码加密
String encryptedPassword = passwordEncoder.encode(password);
user.setPassword(encryptedPassword);
// 4. 设置用户状态为正常
user.setStatus(0);
// 5. 调用UserService注册用户
ResultDomain<TbSysUser> registerResult = userService.registerUser(user);
if (!registerResult.isSuccess()) {
result.fail(registerResult.getMessage());
return result;
}
logger.info("用户注册成功: {}", user.getUsername());
// 6. 注册成功后自动登录
LoginParam loginParam = new LoginParam();
loginParam.setUsername(user.getUsername());
loginParam.setPassword(password);
loginParam.setLoginType("password");
if (phone != null && !phone.trim().isEmpty()) {
loginParam.setPhone(phone);
}
if (email != null && !email.trim().isEmpty()) {
loginParam.setEmail(email);
}
ResultDomain<LoginDomain> loginResult = loginService.login(loginParam, request);
if (loginResult.isSuccess()) {
result.success("注册成功", loginResult.getData());
} else {
// 注册成功但登录失败,返回注册成功信息
result.success("注册成功,请登录", loginResult.getData());
}
} catch (Exception e) {
logger.error("用户注册失败", e);
result.fail("注册失败: " + e.getMessage());
}
return result;
}
}

View File

@@ -118,23 +118,17 @@ public class LoginServiceImpl implements LoginService {
}
if (loginType.equals("password")) {
// 验证凭据(密码或验证码)
if (!strategy.verifyCredential(loginParam.getPassword(), user.getPassword())) {
result.fail("密码错误");
logLoginAttempt(loginParam, user, false, loginAttempt, "密码错误");
return result;
}
}
else {
// 验证凭据(验证码)
String storedCaptcha = (String) redisService.get("captcha:" + loginParam.getPhone());
if (!strategy.verifyCredential(loginParam.getCaptcha(), storedCaptcha)) {
result.fail("验证码错误");
logLoginAttempt(loginParam, user, false, loginAttempt, "验证码错误");
} else if (loginType.equals("email") || loginType.equals("phone")) {
if (!strategy.verifyCaptchaWithSession(loginParam)) {
result.fail("验证码错误或已过期");
logLoginAttempt(loginParam, user, false, loginAttempt, "验证码验证失败");
return result;
}
// 验证码使用后删除
redisService.delete("captcha:" + loginParam.getPhone());
}
// 构建登录域对象
@@ -142,9 +136,12 @@ public class LoginServiceImpl implements LoginService {
// 生成JWT令牌
loginDomain.setToken(jwtTokenUtil.generateToken(loginDomain));
// 将LoginDomain存储到Redis中
// 将LoginDomain存储到Redis中根据rememberMe设置不同的过期时间
String redisKey = "login:token:" + user.getID();
redisService.set(redisKey, loginDomain, 24 * 60 * 60, TimeUnit.SECONDS);
long expireTime = loginParam.isRememberMe()
? 7 * 24 * 60 * 60 // rememberMe: 7天
: 24 * 60 * 60; // 不rememberMe: 1天
redisService.set(redisKey, loginDomain, expireTime, TimeUnit.SECONDS);
// 登录成功后清除失败次数并记录成功日志
redisService.delete(attemptKey);
@@ -280,6 +277,16 @@ public class LoginServiceImpl implements LoginService {
if (user != null) {
loginLog.setUserID(user.getID());
loginLog.setUsername(user.getUsername());
}else{
if (loginParam.getLoginType().equals("password")) {
loginLog.setUsername(loginParam.getUsername());
}else if (loginParam.getLoginType().equals("email")) {
loginLog.setUsername(loginParam.getEmail());
}else if (loginParam.getLoginType().equals("phone")) {
loginLog.setUsername(loginParam.getPhone());
}else if (loginParam.getLoginType().equals("wechat")) {
loginLog.setUsername(loginParam.getWechatID());
}
}
// 注意:实际生产中不应记录密码
// loginLog.setPassword(loginParam.getPassword());

View File

@@ -47,4 +47,16 @@ public interface LoginStrategy {
* @since 2025-09-28
*/
boolean verifyCredential(String inputCredential, String storedCredential);
/**
* @description 验证验证码从Redis获取并验证SessionID
* @param loginParam 登录参数
* @return boolean 是否验证成功
* @author yslg
* @since 2025-11-03
*/
default boolean verifyCaptchaWithSession(LoginParam loginParam) {
// 默认实现:不支持验证码登录
return false;
}
}

View File

@@ -2,14 +2,12 @@ package org.xyzh.auth.strategy.impl;
import org.springframework.stereotype.Component;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.api.system.user.UserService;
import org.xyzh.common.redis.service.RedisService;
/**
* @description EmailLoginStrategy.java文件描述 邮箱登录策略
@@ -34,23 +32,72 @@ public class EmailLoginStrategy implements LoginStrategy {
@Override
public boolean validate(LoginParam loginParam) {
return loginParam.getEmail() != null && !loginParam.getEmail().trim().isEmpty()
&& loginParam.getPassword() != null && !loginParam.getPassword().trim().isEmpty();
if (loginParam.getEmail() == null || loginParam.getEmail().trim().isEmpty()) {
return false;
}
// 密码登录或验证码登录都可以
return (loginParam.getPassword() != null && !loginParam.getPassword().trim().isEmpty())
|| (loginParam.getCaptcha() != null && !loginParam.getCaptcha().trim().isEmpty());
}
@Override
public TbSysUser findUser(LoginParam loginParam) {
TbSysUser filter = new TbSysUser();
filter.setEmail(loginParam.getEmail());
List<TbSysUser> users = userService.getUserByFilter(filter).getDataList();
if(users.isEmpty()) {
TbSysUser user = userService.getLoginUser(filter).getData();
if(user == null) {
return null;
}
return users.get(0);
return user;
}
@Autowired
private RedisService redisService;
@Override
public boolean verifyCredential(String inputCredential, String storedCredential) {
// 密码验证
return passwordEncoder.matches(inputCredential, storedCredential);
}
@Override
public boolean verifyCaptchaWithSession(LoginParam loginParam) {
String captchaId = loginParam.getCaptchaId();
String inputCaptcha = loginParam.getCaptcha();
String email = loginParam.getEmail();
// 验证参数
if (captchaId == null || captchaId.trim().isEmpty()) {
return false;
}
if (inputCaptcha == null || inputCaptcha.trim().isEmpty()) {
return false;
}
// 从Redis获取验证码
String codeKey = "email:code:" + captchaId;
String storedValue = (String) redisService.get(codeKey);
if (storedValue == null) {
return false;
}
// 解析存储的值:邮箱:验证码
String[] parts = storedValue.split(":");
if (parts.length != 2) {
return false;
}
String storedEmail = parts[0];
String storedCaptcha = parts[1];
// 验证邮箱和验证码是否匹配
if (!storedEmail.equals(email) || !storedCaptcha.equals(inputCaptcha)) {
return false;
}
// 验证码使用后删除
redisService.delete(codeKey);
return true;
}
}

View File

@@ -7,6 +7,9 @@ import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.utils.NonUtils;
import org.xyzh.common.utils.validation.ValidationUtils;
import org.xyzh.common.utils.validation.method.EmailValidateMethod;
import org.xyzh.common.utils.validation.method.PhoneValidateMethod;
import org.xyzh.api.system.user.UserService;
import java.util.List;
@@ -53,15 +56,15 @@ public class PasswordLoginStrategy implements LoginStrategy {
@Override
public TbSysUser findUser(LoginParam loginParam) {
TbSysUser filter = new TbSysUser();
if (NonUtils.isNotEmpty(loginParam.getUsername())) {
EmailValidateMethod emailValidateMethod = new EmailValidateMethod();
PhoneValidateMethod phoneValidateMethod = new PhoneValidateMethod();
if(emailValidateMethod.validate(loginParam.getUsername())){
filter.setEmail(loginParam.getUsername());
}else if (phoneValidateMethod.validate(loginParam.getUsername())){
filter.setPhone(loginParam.getUsername());
}else{
filter.setUsername(loginParam.getUsername());
}
if (NonUtils.isNotEmpty(loginParam.getEmail())) {
filter.setEmail(loginParam.getEmail());
}
if (NonUtils.isNotEmpty(loginParam.getPhone())) {
filter.setPhone(loginParam.getPhone());
}
filter.setPassword(passwordEncoder.encode(loginParam.getPassword()));
TbSysUser user = userService.getLoginUser(filter).getData();
if(user == null) {

View File

@@ -7,8 +7,7 @@ import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.api.system.user.UserService;
import java.util.List;
import org.xyzh.common.redis.service.RedisService;
/**
* @description PhoneLoginStrategy.java文件描述 手机号登录策略
@@ -33,27 +32,72 @@ public class PhoneLoginStrategy implements LoginStrategy {
@Override
public boolean validate(LoginParam loginParam) {
return loginParam.getPhone() != null && !loginParam.getPhone().trim().isEmpty()
&& (loginParam.getPassword() != null || loginParam.getCaptcha() != null);
if (loginParam.getPhone() == null || loginParam.getPhone().trim().isEmpty()) {
return false;
}
// 密码登录或验证码登录都可以
return (loginParam.getPassword() != null && !loginParam.getPassword().trim().isEmpty())
|| (loginParam.getCaptcha() != null && !loginParam.getCaptcha().trim().isEmpty());
}
@Override
public TbSysUser findUser(LoginParam loginParam) {
TbSysUser filter = new TbSysUser();
filter.setPhone(loginParam.getPhone());
List<TbSysUser> users = userService.getUserByFilter(filter).getDataList();
if(users.isEmpty()) {
TbSysUser user = userService.getLoginUser(filter).getData();
if(user == null) {
return null;
}
return users.get(0);
return user;
}
@Autowired
private RedisService redisService;
@Override
public boolean verifyCredential(String inputCredential, String storedCredential) {
// 手机号登录可能使用验证码,如果有验证码则跳过密码验证
if (inputCredential == null) {
return true; // 假设验证码已经在其他地方验证过了
}
// 密码验证
return passwordEncoder.matches(inputCredential, storedCredential);
}
@Override
public boolean verifyCaptchaWithSession(LoginParam loginParam) {
String captchaId = loginParam.getCaptchaId();
String inputCaptcha = loginParam.getCaptcha();
String phone = loginParam.getPhone();
// 验证参数
if (captchaId == null || captchaId.trim().isEmpty()) {
return false;
}
if (inputCaptcha == null || inputCaptcha.trim().isEmpty()) {
return false;
}
// 从Redis获取验证码
String codeKey = "sms:code:" + captchaId;
String storedValue = (String) redisService.get(codeKey);
if (storedValue == null) {
return false;
}
// 解析存储的值:手机号:验证码
String[] parts = storedValue.split(":");
if (parts.length != 2) {
return false;
}
String storedPhone = parts[0];
String storedCaptcha = parts[1];
// 验证手机号和验证码是否匹配
if (!storedPhone.equals(phone) || !storedCaptcha.equals(inputCaptcha)) {
return false;
}
// 验证码使用后删除
redisService.delete(codeKey);
return true;
}
}

View File

@@ -8,8 +8,6 @@ import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.api.system.user.UserService;
import java.util.List;
/**
* @description UsernameLoginStrategy.java文件描述 用户名登录策略
* @filename UsernameLoginStrategy.java
@@ -41,11 +39,11 @@ public class UsernameLoginStrategy implements LoginStrategy {
public TbSysUser findUser(LoginParam loginParam) {
TbSysUser filter = new TbSysUser();
filter.setUsername(loginParam.getUsername());
List<TbSysUser> users = userService.getUserByFilter(filter).getDataList();
if(users.isEmpty()) {
TbSysUser user = userService.getLoginUser(filter).getData();
if(user == null) {
return null;
}
return users.get(0);
return user;
}
@Override

View File

@@ -7,8 +7,6 @@ import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.api.system.user.UserService;
import java.util.List;
/**
* @description WechatLoginStrategy.java文件描述 微信登录策略
* @filename WechatLoginStrategy.java
@@ -37,11 +35,11 @@ public class WechatLoginStrategy implements LoginStrategy {
public TbSysUser findUser(LoginParam loginParam) {
TbSysUser filter = new TbSysUser();
filter.setWechatID(loginParam.getWechatID());
List<TbSysUser> users = userService.getUserByFilter(filter).getDataList();
if(users.isEmpty()) {
TbSysUser user = userService.getLoginUser(filter).getData();
if(user == null) {
return null;
}
return users.get(0);
return user;
}
@Override