diff --git a/docs/images/qr/wechat-app.jpg b/docs/images/qr/wechat-app.jpg new file mode 100644 index 00000000..73a907b2 Binary files /dev/null and b/docs/images/qr/wechat-app.jpg differ diff --git a/docs/images/qr/wechat-mp.jpg b/docs/images/qr/wechat-mp.jpg deleted file mode 100644 index 8c06ee37..00000000 Binary files a/docs/images/qr/wechat-mp.jpg and /dev/null differ diff --git a/pom.xml b/pom.xml index 6bbc8410..100bc70f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.youlai youlai-boot - 4.3.0 + 4.3.1 基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。 diff --git a/src/main/java/com/youlai/boot/framework/security/exception/CaptchaValidationException.java b/src/main/java/com/youlai/boot/framework/security/exception/SmsCaptchaException.java similarity index 56% rename from src/main/java/com/youlai/boot/framework/security/exception/CaptchaValidationException.java rename to src/main/java/com/youlai/boot/framework/security/exception/SmsCaptchaException.java index b7c68cc4..8fca2cd9 100644 --- a/src/main/java/com/youlai/boot/framework/security/exception/CaptchaValidationException.java +++ b/src/main/java/com/youlai/boot/framework/security/exception/SmsCaptchaException.java @@ -3,13 +3,13 @@ package com.youlai.boot.framework.security.exception; import org.springframework.security.core.AuthenticationException; /** - * 验证码校验异常 + * 短信验证码异常 * * @author Ray.Hao * @since 2025/3/1 */ -public class CaptchaValidationException extends AuthenticationException { - public CaptchaValidationException(String msg) { +public class SmsCaptchaException extends AuthenticationException { + public SmsCaptchaException(String msg) { super(msg); } -} \ No newline at end of file +} diff --git a/src/main/java/com/youlai/boot/framework/security/exception/TokenInvalidException.java b/src/main/java/com/youlai/boot/framework/security/exception/TokenInvalidException.java new file mode 100644 index 00000000..a4bb93c2 --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/security/exception/TokenInvalidException.java @@ -0,0 +1,21 @@ +package com.youlai.boot.framework.security.exception; + +import com.youlai.boot.common.result.ResultCode; +import lombok.Getter; + +/** + * Token 无效异常(access_token 或 refresh_token 过期/无效) + * + * @author Ray.Hao + * @since 4.3.1 + */ +@Getter +public class TokenInvalidException extends RuntimeException { + + private final ResultCode resultCode; + + public TokenInvalidException(ResultCode resultCode) { + super(resultCode.getMsg()); + this.resultCode = resultCode; + } +} diff --git a/src/main/java/com/youlai/boot/framework/security/provider/SmsAuthenticationProvider.java b/src/main/java/com/youlai/boot/framework/security/provider/SmsAuthenticationProvider.java index ec5c63f1..3450bd7a 100644 --- a/src/main/java/com/youlai/boot/framework/security/provider/SmsAuthenticationProvider.java +++ b/src/main/java/com/youlai/boot/framework/security/provider/SmsAuthenticationProvider.java @@ -3,7 +3,7 @@ package com.youlai.boot.framework.security.provider; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.RedisConstants; -import com.youlai.boot.framework.security.exception.CaptchaValidationException; +import com.youlai.boot.framework.security.exception.SmsCaptchaException; import com.youlai.boot.framework.security.model.SmsAuthenticationToken; import com.youlai.boot.framework.security.model.SysUserDetails; import com.youlai.boot.framework.security.model.UserAuthInfo; @@ -62,11 +62,11 @@ public class SmsAuthenticationProvider implements AuthenticationProvider { // 参数校验 if (StrUtil.isBlank(mobile)) { log.warn("短信验证码登录失败:手机号为空"); - throw new CaptchaValidationException("手机号不能为空"); + throw new SmsCaptchaException("手机号不能为空"); } if (StrUtil.isBlank(inputVerifyCode)) { log.warn("短信验证码登录失败:验证码为空,手机号={}", mobile); - throw new CaptchaValidationException("验证码不能为空"); + throw new SmsCaptchaException("验证码不能为空"); } // 根据手机号获取用户信息 @@ -89,12 +89,12 @@ public class SmsAuthenticationProvider implements AuthenticationProvider { if (cachedVerifyCode == null) { log.warn("短信验证码登录失败:验证码已过期,手机号={}", mobile); - throw new CaptchaValidationException("验证码已过期,请重新获取"); + throw new SmsCaptchaException("验证码已过期,请重新获取"); } if (!StrUtil.equals(inputVerifyCode, cachedVerifyCode)) { log.warn("短信验证码登录失败:验证码错误,手机号={}", mobile); - throw new CaptchaValidationException("验证码错误"); + throw new SmsCaptchaException("验证码错误"); } // 验证成功后删除验证码,防止重复使用 diff --git a/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java b/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java index 10966bd5..1b153f64 100644 --- a/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java +++ b/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java @@ -12,9 +12,9 @@ import cn.hutool.jwt.JWTUtil; import com.youlai.boot.common.constant.JwtClaimConstants; 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.framework.security.config.SecurityProperties; +import com.youlai.boot.framework.security.exception.TokenInvalidException; import com.youlai.boot.framework.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.RoleDataScope; import org.apache.commons.lang3.StringUtils; @@ -300,7 +300,7 @@ public class JwtTokenManager implements TokenManager { public AuthenticationToken refreshToken(String refreshToken) { boolean isValid = validateRefreshToken(refreshToken); if (!isValid) { - throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + throw new TokenInvalidException(ResultCode.REFRESH_TOKEN_INVALID); } Authentication authentication = parseToken(refreshToken); int accessTokenExpiration = securityProperties.getSession().getAccessTokenTimeToLive(); diff --git a/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java b/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java index 1616bbee..f58ab324 100644 --- a/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java +++ b/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java @@ -5,9 +5,9 @@ import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; 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.framework.security.config.SecurityProperties; +import com.youlai.boot.framework.security.exception.TokenInvalidException; import com.youlai.boot.framework.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.UserSession; import com.youlai.boot.framework.security.model.SysUserDetails; @@ -147,7 +147,7 @@ public class RedisTokenManager implements TokenManager { UserSession userSession = (UserSession) redisTemplate.opsForValue() .get(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); if (userSession == null) { - throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + throw new TokenInvalidException(ResultCode.REFRESH_TOKEN_INVALID); } Object oldAccessTokenValue = redisTemplate.opsForValue().get(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userSession.getUserId())); // 删除旧的访问令牌记录 diff --git a/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java b/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java index 7be6bbce..9b74c532 100644 --- a/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java +++ b/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import tools.jackson.core.JacksonException; import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.common.result.Result; import com.youlai.boot.common.result.ResultCode; +import com.youlai.boot.framework.security.exception.TokenInvalidException; import jakarta.servlet.ServletException; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; @@ -222,6 +223,17 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION); } + /** + * 处理 Token 无效异常 + *

+ * 当 access_token 或 refresh_token 过期/无效时,返回 401。 + */ + @ExceptionHandler(TokenInvalidException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Result handleTokenInvalidException(TokenInvalidException e) { + return Result.failed(e.getResultCode()); + } + /** * 处理业务异常 *