refactor: 升级 SpringBoot 3.5.0 版本和代码结构优化

This commit is contained in:
Ray.Hao
2025-06-11 20:47:02 +08:00
parent 4c492784f3
commit af8b5c847d
27 changed files with 246 additions and 80 deletions

View File

@@ -0,0 +1,115 @@
package com.youlai.boot.auth.controller;
import com.youlai.boot.auth.model.CaptchaInfo;
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.auth.service.AuthService;
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.common.annotation.Log;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 认证控制层
*
* @author Ray.Hao
* @since 2022/10/16
*/
@Tag(name = "01.认证中心")
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthController {
private final AuthService authService;
@Operation(summary = "获取验证码")
@GetMapping("/captcha")
public Result<CaptchaInfo> getCaptcha() {
CaptchaInfo captcha = authService.getCaptcha();
return Result.success(captcha);
}
@Operation(summary = "账号密码登录")
@PostMapping("/login")
@Log(value = "登录", module = LogModuleEnum.LOGIN)
public Result<AuthenticationToken> login(
@Parameter(description = "用户名", example = "admin") @RequestParam String username,
@Parameter(description = "密码", example = "123456") @RequestParam String password
) {
AuthenticationToken authenticationToken = authService.login(username, password);
return Result.success(authenticationToken);
}
@Operation(summary = "短信验证码登录")
@PostMapping("/login/sms")
@Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN)
public Result<AuthenticationToken> loginBySms(
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile,
@Parameter(description = "验证码", example = "1234") @RequestParam String code
) {
AuthenticationToken loginResult = authService.loginBySms(mobile, code);
return Result.success(loginResult);
}
@Operation(summary = "发送登录短信验证码")
@PostMapping("/sms/code")
public Result<Void> sendLoginVerifyCode(
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile
) {
authService.sendSmsLoginCode(mobile);
return Result.success();
}
@Operation(summary = "微信授权登录(Web)")
@PostMapping("/login/wechat")
@Log(value = "微信登录", module = LogModuleEnum.LOGIN)
public Result<AuthenticationToken> loginByWechat(
@Parameter(description = "微信授权码", example = "code") @RequestParam String code
) {
AuthenticationToken loginResult = authService.loginByWechat(code);
return Result.success(loginResult);
}
@Operation(summary = "微信小程序登录(Code)")
@PostMapping("/wx/miniapp/code-login")
public Result<AuthenticationToken> loginByWxMiniAppCode(@RequestBody @Valid WxMiniAppCodeLoginDTO loginDTO) {
AuthenticationToken token = authService.loginByWxMiniAppCode(loginDTO);
return Result.success(token);
}
@Operation(summary = "微信小程序登录(手机号)")
@PostMapping("/wx/miniapp/phone-login")
public Result<AuthenticationToken> loginByWxMiniAppPhone(@RequestBody @Valid WxMiniAppPhoneLoginDTO loginDTO) {
AuthenticationToken token = authService.loginByWxMiniAppPhone(loginDTO);
return Result.success(token);
}
@Operation(summary = "退出登录")
@DeleteMapping("/logout")
@Log(value = "退出登录", module = LogModuleEnum.LOGIN)
public Result<?> logout() {
authService.logout();
return Result.success();
}
@Operation(summary = "刷新令牌")
@PostMapping("/refresh-token")
public Result<?> refreshToken(
@Parameter(description = "刷新令牌", example = "xxx.xxx.xxx") @RequestParam String refreshToken
) {
AuthenticationToken authenticationToken = authService.refreshToken(refreshToken);
return Result.success(authenticationToken);
}
}

View File

@@ -0,0 +1,27 @@
package com.youlai.boot.auth.enums;
/**
* EasyCaptcha 验证码类型枚举
*
* @author haoxr
* @since 2.5.1
*/
public enum CaptchaTypeEnum {
/**
* 圆圈干扰验证码
*/
CIRCLE,
/**
* GIF验证码
*/
GIF,
/**
* 干扰线验证码
*/
LINE,
/**
* 扭曲干扰验证码
*/
SHEAR
}

View File

@@ -0,0 +1,24 @@
package com.youlai.boot.auth.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
* 验证码信息
*
* @author Ray。Hao
* @since 2023/03/24
*/
@Schema(description = "验证码信息")
@Data
@Builder
public class CaptchaInfo {
@Schema(description = "验证码缓存 Key")
private String captchaKey;
@Schema(description = "验证码图片Base64字符串")
private String captchaBase64;
}

View File

@@ -0,0 +1,22 @@
package com.youlai.boot.auth.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
/**
* 微信小程序Code登录请求参数
*
* @author 有来技术团队
* @since 2.0.0
*/
@Schema(description = "微信小程序Code登录请求参数")
@Data
public class WxMiniAppCodeLoginDTO {
@Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "code不能为空")
private String code;
}

View File

@@ -0,0 +1,28 @@
package com.youlai.boot.auth.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
/**
* 微信小程序手机号登录请求参数
*
* @author 有来技术团队
* @since 2.0.0
*/
@Schema(description = "微信小程序手机号登录请求参数")
@Data
public class WxMiniAppPhoneLoginDTO {
@Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "code不能为空")
private String code;
@Schema(description = "包括敏感数据在内的完整用户信息的加密数据")
private String encryptedData;
@Schema(description = "加密算法的初始向量")
private String iv;
}

View File

@@ -0,0 +1,84 @@
package com.youlai.boot.auth.service;
import com.youlai.boot.auth.model.CaptchaInfo;
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
/**
* 认证服务接口
*
* @author Ray.Hao
* @since 2.4.0
*/
public interface AuthService {
/**
* 登录
*
* @param username 用户名
* @param password 密码
* @return 登录结果
*/
AuthenticationToken login(String username, String password);
/**
* 登出
*/
void logout();
/**
* 获取验证码
*
* @return 验证码
*/
CaptchaInfo getCaptcha();
/**
* 刷新令牌
*
* @param refreshToken 刷新令牌
* @return 登录结果
*/
AuthenticationToken refreshToken(String refreshToken);
/**
* 微信小程序登录
*
* @param code 微信登录code
* @return 登录结果
*/
AuthenticationToken loginByWechat(String code);
/**
* 微信小程序Code登录
*
* @param loginDTO 登录参数
* @return 访问令牌
*/
AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDTO);
/**
* 微信小程序手机号登录
*
* @param loginDTO 登录参数
* @return 访问令牌
*/
AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO);
/**
* 发送短信验证码
*
* @param mobile 手机号
*/
void sendSmsLoginCode(String mobile);
/**
* 短信验证码登录
*
* @param mobile 手机号
* @param code 验证码
* @return 登录结果
*/
AuthenticationToken loginBySms(String mobile, String code);
}

View File

@@ -0,0 +1,279 @@
package com.youlai.boot.auth.service.impl;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.auth.enums.CaptchaTypeEnum;
import com.youlai.boot.auth.model.CaptchaInfo;
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.config.property.CaptchaProperties;
import com.youlai.boot.core.security.extension.sms.SmsAuthenticationToken;
import com.youlai.boot.core.security.util.SecurityUtils;
import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
import com.youlai.boot.auth.service.AuthService;
import com.youlai.boot.core.security.token.TokenManager;
import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
import com.youlai.boot.shared.sms.service.SmsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import com.youlai.boot.core.security.extension.wx.WxMiniAppCodeAuthenticationToken;
import com.youlai.boot.core.security.extension.wx.WxMiniAppPhoneAuthenticationToken;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 认证服务实现类
*
* @author Ray.Hao
* @since 2.4.0
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AuthServiceImpl implements AuthService {
private final AuthenticationManager authenticationManager;
private final TokenManager tokenManager;
private final Font captchaFont;
private final CaptchaProperties captchaProperties;
private final CodeGenerator codeGenerator;
private final SmsService smsService;
private final RedisTemplate<String, Object> redisTemplate;
/**
* 用户名密码登录
*
* @param username 用户名
* @param password 密码
* @return 访问令牌
*/
@Override
public AuthenticationToken login(String username, String password) {
// 1. 创建用于密码认证的令牌(未认证)
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username.trim(), password);
// 2. 执行认证(认证中)
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
AuthenticationToken authenticationTokenResponse =
tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return authenticationTokenResponse;
}
/**
* 微信一键授权登录
*
* @param code 微信登录code
* @return 访问令牌
*/
@Override
public AuthenticationToken loginByWechat(String code) {
// 1. 创建用户微信认证的令牌(未认证)
WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(code);
// 2. 执行认证(认证中)
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
AuthenticationToken token = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return token;
}
/**
* 发送登录短信验证码
*
* @param mobile 手机号
*/
@Override
public void sendSmsLoginCode(String mobile) {
// 随机生成4位验证码
// String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
// TODO 为了方便测试,验证码固定为 1234实际开发中在配置了厂商短信服务后可以使用上面的随机验证码
String code = "1234";
// 发送短信验证码
Map<String, String> templateParams = new HashMap<>();
templateParams.put("code", code);
try {
smsService.sendSms(mobile, SmsTypeEnum.LOGIN, templateParams);
} catch (Exception e) {
log.error("发送短信验证码失败", e);
}
// 缓存验证码至Redis用于登录校验
redisTemplate.opsForValue().set(StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile), code, 5, TimeUnit.MINUTES);
}
/**
* 短信验证码登录
*
* @param mobile 手机号
* @param code 验证码
* @return 访问令牌
*/
@Override
public AuthenticationToken loginBySms(String mobile, String code) {
// 1. 创建用户短信验证码认证的令牌(未认证)
SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(mobile, code);
// 2. 执行认证(认证中)
Authentication authentication = authenticationManager.authenticate(smsAuthenticationToken);
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
AuthenticationToken authenticationToken = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return authenticationToken;
}
/**
* 注销登录
*/
@Override
public void logout() {
String token = SecurityUtils.getTokenFromRequest();
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX )) {
token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length());
// 将JWT令牌加入黑名单
tokenManager.invalidateToken(token);
// 清除Security上下文
SecurityContextHolder.clearContext();
}
}
/**
* 获取验证码
*
* @return 验证码
*/
@Override
public CaptchaInfo getCaptcha() {
String captchaType = captchaProperties.getType();
int width = captchaProperties.getWidth();
int height = captchaProperties.getHeight();
int interfereCount = captchaProperties.getInterfereCount();
int codeLength = captchaProperties.getCode().getLength();
AbstractCaptcha captcha;
if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
} else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength);
} else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
} else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
} else {
throw new IllegalArgumentException("Invalid captcha type: " + captchaType);
}
captcha.setGenerator(codeGenerator);
captcha.setTextAlpha(captchaProperties.getTextAlpha());
captcha.setFont(captchaFont);
String captchaCode = captcha.getCode();
String imageBase64Data = captcha.getImageBase64Data();
// 验证码文本缓存至Redis用于登录校验
String captchaKey = IdUtil.fastSimpleUUID();
redisTemplate.opsForValue().set(
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaKey),
captchaCode,
captchaProperties.getExpireSeconds(),
TimeUnit.SECONDS
);
return CaptchaInfo.builder()
.captchaKey(captchaKey)
.captchaBase64(imageBase64Data)
.build();
}
/**
* 刷新token
*
* @param refreshToken 刷新令牌
* @return 新的访问令牌
*/
@Override
public AuthenticationToken refreshToken(String refreshToken) {
// 验证刷新令牌
boolean isValidate = tokenManager.validateRefreshToken(refreshToken);
if (!isValidate) {
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
}
// 刷新令牌有效,生成新的访问令牌
return tokenManager.refreshToken(refreshToken);
}
/**
* 微信小程序Code登录
*
* @param loginDTO 登录参数
* @return 访问令牌
*/
@Override
public AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDTO) {
// 1. 创建微信小程序认证令牌(未认证)
WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(loginDTO.getCode());
// 2. 执行认证(认证中)
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
AuthenticationToken token = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return token;
}
/**
* 微信小程序手机号登录
*
* @param loginDTO 登录参数
* @return 访问令牌
*/
@Override
public AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO) {
// 创建微信小程序手机号认证Token
WxMiniAppPhoneAuthenticationToken authenticationToken = new WxMiniAppPhoneAuthenticationToken(
loginDTO.getCode(),
loginDTO.getEncryptedData(),
loginDTO.getIv()
);
// 执行认证
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 认证成功后生成JWT令牌并存入Security上下文
AuthenticationToken token = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return token;
}
}