服务启动

This commit is contained in:
2025-12-05 18:24:21 +08:00
parent a8233ceb72
commit 133209691e
39 changed files with 2526 additions and 30 deletions

View File

@@ -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>

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -10,7 +10,9 @@ urban-lifeline:
enabled: false # 认证服务自己不需要认证
whitelist:
- /** # 认证服务的所有接口都放行
security:
aes:
secret-key: 1234567890qwer
# ================== Spring ==================
spring:
application: