服务启动
This commit is contained in:
@@ -23,10 +23,26 @@
|
||||
<groupId>org.xyzh.apis</groupId>
|
||||
<artifactId>api-auth</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xyzh.apis</groupId>
|
||||
<artifactId>api-system</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xyzh.apis</groupId>
|
||||
<artifactId>api-message</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xyzh.common</groupId>
|
||||
<artifactId>common-auth</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xyzh.common</groupId>
|
||||
<artifactId>common-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xyzh.common</groupId>
|
||||
<artifactId>common-utils</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,433 @@
|
||||
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.service.AuthService;
|
||||
import org.xyzh.api.message.service.MessageService;
|
||||
import org.xyzh.api.system.service.SysUserService;
|
||||
import org.xyzh.api.system.vo.SysUserVO;
|
||||
import org.xyzh.auth.utils.CapcatUtils;
|
||||
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.sys.TbSysUserDTO;
|
||||
import org.xyzh.common.utils.IDUtils;
|
||||
import org.xyzh.common.utils.validation.method.PhoneValidateMethod;
|
||||
import org.xyzh.common.redis.service.RedisService;
|
||||
import org.apache.dubbo.config.annotation.DubboReference;
|
||||
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文件描述 认证控制器
|
||||
* @filename AuthController.java
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
* @since 2025-09-28
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
public class AuthController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
|
||||
|
||||
@Autowired
|
||||
private AuthService authService;
|
||||
|
||||
@DubboReference(version = "1.0.0", group = "system", timeout = 5000, check = false)
|
||||
private SysUserService userService;
|
||||
|
||||
@DubboReference(version = "1.0.0", group = "message", timeout = 5000, check = false)
|
||||
private MessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
/**
|
||||
* @description 用户登录
|
||||
* @param loginParam 登录参数
|
||||
* @return ResultDomain<LoginDomain> 登录结果
|
||||
* @author yslg
|
||||
* @since 2025-09-28
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public ResultDomain<LoginDomain> login(@RequestBody LoginParam loginParam, HttpServletRequest request) {
|
||||
return authService.login(loginParam, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 用户退出登录
|
||||
* @param loginDomain 登录域对象
|
||||
* @return ResultDomain<String> 退出结果
|
||||
* @author yslg
|
||||
* @since 2025-09-28
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public ResultDomain<LoginDomain> logout(HttpServletRequest request) {
|
||||
return authService.logout(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取验证码
|
||||
* @return ResultDomain<String> 验证码
|
||||
* @author yslg
|
||||
* @since 2025-09-28
|
||||
*/
|
||||
@GetMapping("/captcha")
|
||||
public ResultDomain<String> getCaptcha() {
|
||||
// TODO: 实现验证码生成逻辑
|
||||
// 生成验证码会话ID,用于验证时匹配
|
||||
String captchaId = IDUtils.generateID();
|
||||
String captchaData = captchaId + ":captcha-placeholder"; // 格式: ID:验证码内容
|
||||
|
||||
return ResultDomain.success("验证码获取成功", captchaData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 刷新令牌
|
||||
* @param token 原令牌
|
||||
* @return ResultDomain<String> 新令牌
|
||||
* @author yslg
|
||||
* @since 2025-09-28
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
public ResultDomain<String> refreshToken(@RequestHeader("Authorization") String token) {
|
||||
// TODO: 实现令牌刷新逻辑
|
||||
// 为新令牌生成唯一ID
|
||||
String newTokenId = IDUtils.generateID();
|
||||
String newToken = "new-token-" + newTokenId; // 临时占位符
|
||||
|
||||
return ResultDomain.success("令牌刷新成功", newToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 健康检查
|
||||
* @return ResultDomain<String> 健康状态
|
||||
* @author yslg
|
||||
* @since 2025-09-28
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
public ResultDomain<String> health() {
|
||||
return ResultDomain.success("认证服务运行正常", "OK");
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
String email = requestBody.get("email");
|
||||
|
||||
// 验证邮箱格式
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
return ResultDomain.failure("邮箱不能为空");
|
||||
}
|
||||
|
||||
String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
|
||||
if (!email.matches(emailRegex)) {
|
||||
return ResultDomain.failure("邮箱格式不正确");
|
||||
}
|
||||
|
||||
// 检查是否频繁发送(60秒内只能发送一次)
|
||||
String rateLimitKey = "email:code:ratelimit:" + email;
|
||||
if (redisService.hasKey(rateLimitKey)) {
|
||||
return ResultDomain.failure("验证码已发送,请勿重复发送");
|
||||
}
|
||||
|
||||
// 生成会话ID(用于绑定验证码和用户)
|
||||
String sessionId = IDUtils.generateID();
|
||||
|
||||
// 生成6位数字验证码
|
||||
String code = CapcatUtils.generateVerificationCode();
|
||||
|
||||
// 发送邮件
|
||||
boolean success = messageService.sendEmailVerificationCode(email, code).getSuccess();
|
||||
|
||||
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);
|
||||
return ResultDomain.success("验证码已发送到邮箱", data);
|
||||
} else {
|
||||
return ResultDomain.failure("验证码发送失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
String phone = requestBody.get("phone");
|
||||
|
||||
// 验证手机号格式
|
||||
if (phone == null || phone.trim().isEmpty()) {
|
||||
return ResultDomain.failure("手机号不能为空");
|
||||
}
|
||||
PhoneValidateMethod validateMethod = new PhoneValidateMethod();
|
||||
if (!validateMethod.validate(phone)) {
|
||||
return ResultDomain.failure("手机号格式不正确");
|
||||
}
|
||||
|
||||
// 检查是否频繁发送(60秒内只能发送一次)
|
||||
String rateLimitKey = "sms:code:ratelimit:" + phone;
|
||||
if (redisService.hasKey(rateLimitKey)) {
|
||||
return ResultDomain.failure("验证码已发送,请勿重复发送");
|
||||
}
|
||||
|
||||
// 生成会话ID(用于绑定验证码和用户)
|
||||
String sessionId = IDUtils.generateID();
|
||||
|
||||
// 生成6位数字验证码
|
||||
String code = CapcatUtils.generateVerificationCode();
|
||||
|
||||
// 发送短信
|
||||
boolean success = messageService.sendPhoneVerificationCode(phone, code).getSuccess();
|
||||
|
||||
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);
|
||||
return ResultDomain.success("验证码已发送", data);
|
||||
} else {
|
||||
return ResultDomain.failure("验证码发送失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
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()) {
|
||||
return ResultDomain.failure("密码不能为空");
|
||||
}
|
||||
|
||||
if (password.length() < 6) {
|
||||
return ResultDomain.failure("密码至少6个字符");
|
||||
}
|
||||
|
||||
if (!password.equals(confirmPassword)) {
|
||||
return ResultDomain.failure("两次输入的密码不一致");
|
||||
}
|
||||
|
||||
// 2. 根据注册类型进行不同的验证
|
||||
SysUserVO user = new SysUserVO();
|
||||
|
||||
switch (registerType) {
|
||||
case "username":
|
||||
// 用户名注册
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
return ResultDomain.failure("用户名不能为空");
|
||||
}
|
||||
if (username.length() < 3 || username.length() > 20) {
|
||||
return ResultDomain.failure("用户名长度为3-20个字符");
|
||||
}
|
||||
if (!username.matches("^[a-zA-Z0-9_]+$")) {
|
||||
return ResultDomain.failure("用户名只能包含字母、数字和下划线");
|
||||
}
|
||||
user.setUsername(username);
|
||||
logger.info("用户名注册: {}", username);
|
||||
break;
|
||||
|
||||
case "phone":
|
||||
// 手机号注册
|
||||
if (phone == null || phone.trim().isEmpty()) {
|
||||
return ResultDomain.failure("手机号不能为空");
|
||||
}
|
||||
if (!phone.matches("^1[3-9]\\d{9}$")) {
|
||||
return ResultDomain.failure("手机号格式不正确");
|
||||
}
|
||||
if (smsCode == null || smsCode.trim().isEmpty()) {
|
||||
return ResultDomain.failure("请输入手机验证码");
|
||||
}
|
||||
if (smsSessionId == null || smsSessionId.trim().isEmpty()) {
|
||||
return ResultDomain.failure("会话已失效,请重新获取验证码");
|
||||
}
|
||||
|
||||
// 通过sessionId验证手机验证码
|
||||
String smsCodeKey = "sms:code:" + smsSessionId;
|
||||
String storedSmsValue = (String) redisService.get(smsCodeKey);
|
||||
if (storedSmsValue == null) {
|
||||
return ResultDomain.failure("验证码已过期,请重新获取");
|
||||
}
|
||||
|
||||
// 解析存储的值:手机号:验证码
|
||||
String[] smsParts = storedSmsValue.split(":");
|
||||
if (smsParts.length != 2) {
|
||||
return ResultDomain.failure("验证码数据异常");
|
||||
}
|
||||
|
||||
String storedPhone = smsParts[0];
|
||||
String storedSmsCode = smsParts[1];
|
||||
|
||||
// 验证手机号和验证码是否匹配
|
||||
if (!storedPhone.equals(phone)) {
|
||||
logger.warn("手机号注册验证失败,提交手机号: {}, 验证码绑定手机号: {}", phone, storedPhone);
|
||||
return ResultDomain.failure("手机号与验证码不匹配");
|
||||
}
|
||||
if (!storedSmsCode.equals(smsCode)) {
|
||||
return ResultDomain.failure("验证码错误");
|
||||
}
|
||||
|
||||
// 验证码使用后删除
|
||||
redisService.delete(smsCodeKey);
|
||||
|
||||
user.setPhone(phone);
|
||||
user.setUsername(phone); // 使用手机号作为用户名
|
||||
logger.info("手机号注册: {}, sessionId: {}", phone, smsSessionId);
|
||||
break;
|
||||
|
||||
case "email":
|
||||
// 邮箱注册
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
return ResultDomain.failure("邮箱不能为空");
|
||||
}
|
||||
if (!email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) {
|
||||
return ResultDomain.failure("邮箱格式不正确");
|
||||
}
|
||||
if (emailCode == null || emailCode.trim().isEmpty()) {
|
||||
return ResultDomain.failure("请输入邮箱验证码");
|
||||
}
|
||||
if (emailSessionId == null || emailSessionId.trim().isEmpty()) {
|
||||
return ResultDomain.failure("会话已失效,请重新获取验证码");
|
||||
}
|
||||
|
||||
// 通过sessionId验证邮箱验证码
|
||||
String emailCodeKey = "email:code:" + emailSessionId;
|
||||
String storedEmailValue = (String) redisService.get(emailCodeKey);
|
||||
if (storedEmailValue == null) {
|
||||
return ResultDomain.failure("验证码已过期,请重新获取");
|
||||
}
|
||||
|
||||
// 解析存储的值:邮箱:验证码
|
||||
String[] emailParts = storedEmailValue.split(":");
|
||||
if (emailParts.length != 2) {
|
||||
return ResultDomain.failure("验证码数据异常");
|
||||
}
|
||||
|
||||
String storedEmail = emailParts[0];
|
||||
String storedEmailCode = emailParts[1];
|
||||
|
||||
// 验证邮箱和验证码是否匹配
|
||||
if (!storedEmail.equals(email)) {
|
||||
logger.warn("邮箱注册验证失败,提交邮箱: {}, 验证码绑定邮箱: {}", email, storedEmail);
|
||||
return ResultDomain.failure("邮箱与验证码不匹配");
|
||||
}
|
||||
if (!storedEmailCode.equals(emailCode)) {
|
||||
return ResultDomain.failure("验证码错误");
|
||||
}
|
||||
|
||||
// 验证码使用后删除
|
||||
redisService.delete(emailCodeKey);
|
||||
|
||||
user.setEmail(email);
|
||||
user.setUsername(email.split("@")[0]); // 使用邮箱前缀作为用户名
|
||||
logger.info("邮箱注册: {}, sessionId: {}", email, emailSessionId);
|
||||
break;
|
||||
|
||||
default:
|
||||
return ResultDomain.failure("未知的注册类型");
|
||||
}
|
||||
|
||||
// 3. 设置密码(明文,Service层会加密)
|
||||
user.setPassword(password);
|
||||
|
||||
// 4. 设置用户状态为正常
|
||||
user.setStatus("0");
|
||||
|
||||
// 5. 调用UserService注册用户(Service层会加密密码)
|
||||
ResultDomain<TbSysUserDTO> registerResult = userService.registerUser(user);
|
||||
|
||||
if (!registerResult.getSuccess()) {
|
||||
return ResultDomain.failure(registerResult.getMessage());
|
||||
}
|
||||
|
||||
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 = authService.login(loginParam, request);
|
||||
|
||||
if (loginResult.getSuccess()) {
|
||||
return ResultDomain.success("注册成功", loginResult.getData());
|
||||
} else {
|
||||
// 注册成功但登录失败,返回注册成功信息
|
||||
return ResultDomain.success("注册成功,请登录", loginResult.getData());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("用户注册失败", e);
|
||||
return ResultDomain.failure("注册失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.xyzh.auth.exception;
|
||||
import org.xyzh.common.core.exception.BaseException;
|
||||
|
||||
public class AuthException extends BaseException {
|
||||
|
||||
public AuthException(Integer code, String description, String message){
|
||||
super(code, description, message);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import org.xyzh.api.auth.service.AuthService;
|
||||
import org.xyzh.common.core.domain.LoginDomain;
|
||||
import org.xyzh.common.core.domain.LoginParam;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
|
||||
import org.apache.dubbo.config.annotation.DubboService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -17,6 +17,12 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
* @copyright yslg
|
||||
* @since 2025-11-09
|
||||
*/
|
||||
@DubboService(
|
||||
version = "1.0.0",
|
||||
group = "auth",
|
||||
timeout = 3000,
|
||||
retries = 0
|
||||
)
|
||||
public class AuthServiceImpl implements AuthService{
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuthServiceImpl.class);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.xyzh.auth.strategy;
|
||||
|
||||
import org.xyzh.common.core.domain.LoginParam;
|
||||
import org.xyzh.common.dto.sys.TbSysUserDTO;
|
||||
|
||||
/**
|
||||
* @description LoginStrategy.java文件描述 登录策略接口
|
||||
* @filename LoginStrategy.java
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
public interface LoginStrategy {
|
||||
|
||||
/**
|
||||
* @description 支持的登录类型
|
||||
* @return String 登录类型
|
||||
* @author yslg
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
String getLoginType();
|
||||
|
||||
/**
|
||||
* @description 验证登录参数
|
||||
* @param loginParam 登录参数
|
||||
* @return boolean 是否有效
|
||||
* @author yslg
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
boolean validate(LoginParam loginParam);
|
||||
|
||||
/**
|
||||
* @description 根据登录参数查找用户
|
||||
* @param loginParam 登录参数
|
||||
* @return TbSysUser 用户对象
|
||||
* @author yslg
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
TbSysUserDTO findUser(LoginParam loginParam);
|
||||
|
||||
/**
|
||||
* @description 验证凭据(密码或验证码)
|
||||
* @param inputCredential 输入凭据(密码或验证码)
|
||||
* @param storedCredential 存储凭据(密码或验证码)
|
||||
* @return boolean 是否匹配
|
||||
* @author yslg
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
boolean verifyCredential(String inputCredential, String storedCredential);
|
||||
|
||||
/**
|
||||
* @description 验证验证码(从Redis获取并验证SessionID)
|
||||
* @param loginParam 登录参数
|
||||
* @return boolean 是否验证成功
|
||||
* @author yslg
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
default boolean verifyCaptchaWithSession(LoginParam loginParam) {
|
||||
// 默认实现:不支持验证码登录
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.xyzh.auth.strategy;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.xyzh.auth.exception.AuthException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @description LoginStrategyFactory.java文件描述 登录策略工厂
|
||||
* @filename LoginStrategyFactory.java
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
@Component
|
||||
public class LoginStrategyFactory {
|
||||
|
||||
private final Map<String, LoginStrategy> strategies;
|
||||
|
||||
public LoginStrategyFactory(List<LoginStrategy> loginStrategies) {
|
||||
this.strategies = loginStrategies.stream()
|
||||
.collect(Collectors.toMap(LoginStrategy::getLoginType, Function.identity()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取登录策略
|
||||
* @param loginType 登录类型
|
||||
* @return LoginStrategy 登录策略
|
||||
* @author yslg
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
public LoginStrategy getStrategy(String loginType) {
|
||||
LoginStrategy strategy = strategies.get(loginType);
|
||||
if (strategy == null) {
|
||||
throw new AuthException(1,"UNSUPPORTED_LOGIN_TYPE", "不支持的登录类型: " + loginType);
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取所有支持的登录类型
|
||||
* @return Set<String> 登录类型集合
|
||||
* @author yslg
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
public java.util.Set<String> getSupportedLoginTypes() {
|
||||
return strategies.keySet();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package org.xyzh.auth.strategy.impl;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
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.sys.TbSysUserDTO;
|
||||
import org.xyzh.api.system.service.SysUserService;
|
||||
import org.xyzh.api.system.vo.SysUserVO;
|
||||
import org.xyzh.common.redis.service.RedisService;
|
||||
|
||||
/**
|
||||
* @description EmailLoginStrategy.java文件描述 邮箱登录策略
|
||||
* @filename EmailLoginStrategy.java
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
@Component
|
||||
public class EmailLoginStrategy implements LoginStrategy {
|
||||
|
||||
@Autowired
|
||||
private SysUserService userService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public String getLoginType() {
|
||||
return "email";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(LoginParam loginParam) {
|
||||
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 TbSysUserDTO findUser(LoginParam loginParam) {
|
||||
TbSysUserDTO filter = new TbSysUserDTO();
|
||||
filter.setEmail(loginParam.getEmail());
|
||||
TbSysUserDTO user = userService.getLoginUser(filter).getData();
|
||||
if(user == null) {
|
||||
return null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.xyzh.auth.strategy.impl;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
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.sys.TbSysUserDTO;
|
||||
import org.xyzh.common.utils.NonUtils;
|
||||
import org.xyzh.common.utils.validation.method.EmailValidateMethod;
|
||||
import org.xyzh.common.utils.validation.method.PhoneValidateMethod;
|
||||
import org.xyzh.api.system.service.SysUserService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* @description PasswordLoginStrategy.java文件描述 密码登录策略
|
||||
* @filename PasswordLoginStrategy.java
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
@Component
|
||||
public class PasswordLoginStrategy implements LoginStrategy {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PasswordLoginStrategy.class);
|
||||
|
||||
@Autowired
|
||||
private SysUserService userService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public String getLoginType() {
|
||||
return "password";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(LoginParam loginParam) {
|
||||
if (NonUtils.isEmpty(loginParam.getPassword())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NonUtils.isEmpty(loginParam.getUsername()) && NonUtils.isEmpty(loginParam.getEmail()) && NonUtils.isEmpty(loginParam.getPhone())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TbSysUserDTO findUser(LoginParam loginParam) {
|
||||
TbSysUserDTO filter = new TbSysUserDTO();
|
||||
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());
|
||||
}
|
||||
// 【优化】删除无用的密码编码,SQL查询不使用password字段
|
||||
// 密码验证在 verifyCredential() 方法中进行
|
||||
TbSysUserDTO user = userService.getLoginUser(filter).getData();
|
||||
if(user == null) {
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyCredential(String inputCredential, String storedCredential) {
|
||||
// 使用BCrypt的matches方法验证密码(内部会自动处理salt)
|
||||
return passwordEncoder.matches(inputCredential, storedCredential);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.xyzh.auth.strategy.impl;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
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.sys.TbSysUserDTO;
|
||||
import org.xyzh.api.system.service.SysUserService;
|
||||
import org.xyzh.common.redis.service.RedisService;
|
||||
|
||||
/**
|
||||
* @description PhoneLoginStrategy.java文件描述 手机号登录策略
|
||||
* @filename PhoneLoginStrategy.java
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
@Component
|
||||
public class PhoneLoginStrategy implements LoginStrategy {
|
||||
|
||||
@Autowired
|
||||
private SysUserService userService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public String getLoginType() {
|
||||
return "phone";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(LoginParam loginParam) {
|
||||
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 TbSysUserDTO findUser(LoginParam loginParam) {
|
||||
TbSysUserDTO filter = new TbSysUserDTO();
|
||||
filter.setPhone(loginParam.getPhone());
|
||||
TbSysUserDTO user = userService.getLoginUser(filter).getData();
|
||||
if(user == null) {
|
||||
return null;
|
||||
}
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.xyzh.auth.strategy.impl;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
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.sys.TbSysUserDTO;
|
||||
import org.xyzh.api.system.service.SysUserService;
|
||||
|
||||
/**
|
||||
* @description UsernameLoginStrategy.java文件描述 用户名登录策略
|
||||
* @filename UsernameLoginStrategy.java
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
@Component
|
||||
public class UsernameLoginStrategy implements LoginStrategy {
|
||||
|
||||
@Autowired
|
||||
private SysUserService userService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public String getLoginType() {
|
||||
return "username";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(LoginParam loginParam) {
|
||||
return loginParam.getUsername() != null && !loginParam.getUsername().trim().isEmpty()
|
||||
&& loginParam.getPassword() != null && !loginParam.getPassword().trim().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TbSysUserDTO findUser(LoginParam loginParam) {
|
||||
TbSysUserDTO filter = new TbSysUserDTO();
|
||||
filter.setUsername(loginParam.getUsername());
|
||||
TbSysUserDTO user = userService.getLoginUser(filter).getData();
|
||||
if(user == null) {
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyCredential(String inputCredential, String storedCredential) {
|
||||
return passwordEncoder.matches(inputCredential, storedCredential);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.xyzh.auth.strategy.impl;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
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.sys.TbSysUserDTO;
|
||||
import org.xyzh.api.system.service.SysUserService;
|
||||
|
||||
/**
|
||||
* @description WechatLoginStrategy.java文件描述 微信登录策略
|
||||
* @filename WechatLoginStrategy.java
|
||||
* @author yslg
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-05
|
||||
*/
|
||||
@Component
|
||||
public class WechatLoginStrategy implements LoginStrategy {
|
||||
|
||||
@Autowired
|
||||
private SysUserService userService;
|
||||
|
||||
|
||||
@Override
|
||||
public String getLoginType() {
|
||||
return "wechat";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(LoginParam loginParam) {
|
||||
return loginParam.getWechatId() != null && !loginParam.getWechatId().trim().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TbSysUserDTO findUser(LoginParam loginParam) {
|
||||
TbSysUserDTO filter = new TbSysUserDTO();
|
||||
filter.setWechatId(loginParam.getWechatId());
|
||||
TbSysUserDTO user = userService.getLoginUser(filter).getData();
|
||||
if(user == null) {
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyCredential(String inputCredential, String storedCredential) {
|
||||
// 微信登录通常不需要密码验证,通过微信授权码验证
|
||||
// 这里可以添加微信授权验证逻辑
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.xyzh.auth.utils;
|
||||
|
||||
public class CapcatUtils {
|
||||
/**
|
||||
* 生成6位数字验证码
|
||||
* @return 验证码
|
||||
*/
|
||||
public static String generateVerificationCode() {
|
||||
return String.valueOf((int)((Math.random() * 9 + 1) * 100000));
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@ urban-lifeline:
|
||||
enabled: false # 认证服务自己不需要认证
|
||||
whitelist:
|
||||
- /** # 认证服务的所有接口都放行
|
||||
|
||||
security:
|
||||
aes:
|
||||
secret-key: 1234567890qwer
|
||||
# ================== Spring ==================
|
||||
spring:
|
||||
application:
|
||||
|
||||
Reference in New Issue
Block a user