From 88ccaff448b919c936a1ff3c25ddd274aa1547a3 Mon Sep 17 00:00:00 2001 From: haoxr <1490493387@qq.com> Date: Thu, 14 Nov 2024 18:32:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9EJWT=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=92=8CJWT=E5=B7=A5=E5=85=B7=E7=B1=BB?= =?UTF-8?q?=E8=A7=A3=E8=80=A6=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../youlai/boot/common/result/ResultCode.java | 5 +- .../boot/common/util/ResponseUtils.java | 2 +- .../security/service/PermissionService.java | 2 +- .../boot/core/security/util/JwtUtils.java | 78 +------------------ .../core/security/util/SecurityUtils.java | 9 --- .../boot/shared/auth/model/LoginResult.java | 2 +- .../auth/service/impl/AuthServiceImpl.java | 41 ++++++++-- .../system/service/impl/UserServiceImpl.java | 12 +-- 8 files changed, 47 insertions(+), 104 deletions(-) diff --git a/src/main/java/com/youlai/boot/common/result/ResultCode.java b/src/main/java/com/youlai/boot/common/result/ResultCode.java index fd51e37a..3b92e4ab 100644 --- a/src/main/java/com/youlai/boot/common/result/ResultCode.java +++ b/src/main/java/com/youlai/boot/common/result/ResultCode.java @@ -36,7 +36,10 @@ public enum ResultCode implements IResultCode, Serializable { VERIFY_CODE_ERROR("A0214", "验证码错误"), TOKEN_INVALID("A0230", "token无效或已过期"), - TOKEN_ACCESS_FORBIDDEN("A0231", "token已被禁止访问"), + REFRESH_TOKEN_INVALID("A0231", "刷新token无效或已过期"), + + TOKEN_ACCESS_FORBIDDEN("A0232", "token已被禁止访问"), + AUTHORIZED_ERROR("A0300", "访问权限异常"), ACCESS_UNAUTHORIZED("A0301", "访问未授权"), diff --git a/src/main/java/com/youlai/boot/common/util/ResponseUtils.java b/src/main/java/com/youlai/boot/common/util/ResponseUtils.java index 6ae77b87..4ae24191 100644 --- a/src/main/java/com/youlai/boot/common/util/ResponseUtils.java +++ b/src/main/java/com/youlai/boot/common/util/ResponseUtils.java @@ -30,7 +30,7 @@ public class ResponseUtils { public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { // 根据不同的结果码设置HTTP状态 int status = switch (resultCode) { - case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + case ACCESS_UNAUTHORIZED, TOKEN_INVALID,REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); case TOKEN_ACCESS_FORBIDDEN -> HttpStatus.FORBIDDEN.value(); default -> HttpStatus.BAD_REQUEST.value(); }; diff --git a/src/main/java/com/youlai/boot/core/security/service/PermissionService.java b/src/main/java/com/youlai/boot/core/security/service/PermissionService.java index 363bcd6c..b6eb02fa 100644 --- a/src/main/java/com/youlai/boot/core/security/service/PermissionService.java +++ b/src/main/java/com/youlai/boot/core/security/service/PermissionService.java @@ -60,7 +60,7 @@ public class PermissionService { ); if (!hasPermission) { - log.error("用户无操作权限"); + log.error("用户无操作权限:{}",requiredPerm); } return hasPermission; } diff --git a/src/main/java/com/youlai/boot/core/security/util/JwtUtils.java b/src/main/java/com/youlai/boot/core/security/util/JwtUtils.java index b02171dc..fb439a84 100644 --- a/src/main/java/com/youlai/boot/core/security/util/JwtUtils.java +++ b/src/main/java/com/youlai/boot/core/security/util/JwtUtils.java @@ -29,63 +29,17 @@ import java.util.stream.Collectors; * @author Ray Hao * @since 2.6.0 */ -@Component public class JwtUtils { - - /** - * JWT 加解密使用的密钥 - */ - private static byte[] key; - - - /** - * 访问令牌过期时间,单位:秒 - */ - private static int accessTokenExpiration; - - /** - * 刷新令牌过期时间,单位:秒 - */ - private static int refreshTokenExpiration; - - private static StringRedisTemplate redisTemplate; - - - - @Autowired - public JwtUtils( - @Value("${security.jwt.key}") String key, - @Value("${security.jwt.access-token-expiration}") int accessTokenExpiration, - @Value("${security.jwt.refresh-token-expiration}") int refreshTokenExpiration, - StringRedisTemplate redisTemplate - ) { - JwtUtils.key = key.getBytes(); - JwtUtils.accessTokenExpiration = accessTokenExpiration; - JwtUtils.refreshTokenExpiration = refreshTokenExpiration; - JwtUtils.redisTemplate = redisTemplate; - } - /** - * 生成访问令牌(JWT Token) - * - * @param authentication 用户认证信息 - * @return Token 字符串 - */ - public static String createAccessToken(Authentication authentication) { - return createToken(authentication, accessTokenExpiration); - } - - public static String createRefreshToken(Authentication authentication) { - return createToken(authentication, refreshTokenExpiration); - } - /** * 生成 JWT Token * * @param authentication 用户认证信息 + * @param expiration 有效期(秒) + * @param key HS256(HmacSHA256)密钥 * @return Token 字符串 */ - public static String createToken(Authentication authentication, int expiration) { + public static String createToken(Authentication authentication, int expiration,byte[] key) { SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); @@ -136,31 +90,5 @@ public class JwtUtils { return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); } - /** - * 将 Token 加入黑名单 - */ - public static void addTokenToBlacklist(String token) { - if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) { - token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); - JSONObject payloads = JWTUtil.parseToken(token).getPayloads(); - String jti = payloads.getStr(JWTPayload.JWT_ID); - Long expiration = payloads.getLong(JWTPayload.EXPIRES_AT); - - if (expiration != null) { - long currentTimeSeconds = System.currentTimeMillis() / 1000; - if (expiration < currentTimeSeconds) { - // Token已过期,直接返回 - return; - } - // 计算Token剩余时间,将其加入黑名单 - long ttl = expiration - currentTimeSeconds; - redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null, ttl, TimeUnit.SECONDS); - } else { - // 永不过期的Token永久加入黑名单 - redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null); - } - } - } - } diff --git a/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java b/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java index 47b39ea4..4344ea49 100644 --- a/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java +++ b/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java @@ -120,14 +120,5 @@ public class SecurityUtils { return request.getHeader(HttpHeaders.AUTHORIZATION); } - /** - * 将 Token 加入黑名单并清空 Spring Security 上下文 - * - * @param token 要失效的 Token - */ - public static void invalidateToken(String token) { - JwtUtils.addTokenToBlacklist(token); - SecurityContextHolder.clearContext(); - } } diff --git a/src/main/java/com/youlai/boot/shared/auth/model/LoginResult.java b/src/main/java/com/youlai/boot/shared/auth/model/LoginResult.java index f34a22b8..7ae334a9 100644 --- a/src/main/java/com/youlai/boot/shared/auth/model/LoginResult.java +++ b/src/main/java/com/youlai/boot/shared/auth/model/LoginResult.java @@ -19,6 +19,6 @@ public class LoginResult { private String refreshToken; @Schema(description = "过期时间(单位:毫秒)") - private Long expires; + private Integer expiresIn; } diff --git a/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java index 217b19b9..cfd4d43a 100644 --- a/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java @@ -5,6 +5,7 @@ import cn.hutool.captcha.CaptchaUtil; import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; import cn.hutool.jwt.JWT; import cn.hutool.jwt.JWTPayload; import cn.hutool.jwt.JWTUtil; @@ -26,6 +27,7 @@ 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.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @@ -65,8 +67,11 @@ public class AuthServiceImpl implements AuthService { // 执行用户认证,认证成功返回的Authentication是SysUserDetailsService#loadUserByUsername获取到的的UserDetails Authentication authentication = authenticationManager.authenticate(authenticationToken); // 认证成功后生成JWT令牌 - String accessToken = JwtUtils.createAccessToken(authentication); - String refreshToken = JwtUtils.createRefreshToken(authentication); + Integer accessTokenExpiration = securityProperties.getJwt().getAccessTokenExpiration(); + Integer refreshTokenExpiration = securityProperties.getJwt().getRefreshTokenExpiration(); + byte[] key = securityProperties.getJwt().getKey().getBytes(); + String accessToken = JwtUtils.createToken(authentication, accessTokenExpiration, key); + String refreshToken = JwtUtils.createToken(authentication, refreshTokenExpiration, key); // 将认证信息存入Security上下文,便于在AOP(如日志记录)中获取当前用户信息 SecurityContextHolder.getContext().setAuthentication(authentication); // 返回包含JWT令牌的登录结果 @@ -74,6 +79,7 @@ public class AuthServiceImpl implements AuthService { .tokenType("Bearer") .accessToken(accessToken) .refreshToken(refreshToken) + .expiresIn(accessTokenExpiration) .build(); } @@ -83,8 +89,26 @@ public class AuthServiceImpl implements AuthService { @Override public void logout() { String token = SecurityUtils.getTokenFromRequest(); - if (StrUtil.isNotBlank(token)) { - SecurityUtils.invalidateToken(token); + if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); + JSONObject payloads = JWTUtil.parseToken(token).getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + Long expiration = payloads.getLong(JWTPayload.EXPIRES_AT); + + if (expiration != null) { + long currentTimeSeconds = System.currentTimeMillis() / 1000; + if (expiration < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + long ttl = expiration - currentTimeSeconds; + redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null, ttl, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null); + } + SecurityContextHolder.clearContext(); } } @@ -148,19 +172,22 @@ public class AuthServiceImpl implements AuthService { boolean isValidate = jwt.setKey(securityProperties.getJwt().getKey().getBytes()).validate(0); if (!isValidate || redisTemplate.hasKey(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jwt.getPayloads().getStr(JWTPayload.JWT_ID))) { - throw new BusinessException(ResultCode.TOKEN_INVALID); + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); } Authentication authentication = JwtUtils.getAuthentication(jwt.getPayloads()); // 创建新的访问令牌 - String newAccessToken = JwtUtils.createAccessToken(authentication); + Integer accessTokenExpiration = securityProperties.getJwt().getAccessTokenExpiration(); + byte[] key = securityProperties.getJwt().getKey().getBytes(); + String newAccessToken = JwtUtils.createToken(authentication, accessTokenExpiration, key); // 返回新的访问令牌 return LoginResult.builder() .tokenType("Bearer") .accessToken(newAccessToken) - .refreshToken(refreshToken) // 保持刷新令牌不变 + .refreshToken(refreshToken) + .expiresIn(securityProperties.getJwt().getAccessTokenExpiration()) .build(); } diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java index a08e0cfc..5aea9fb9 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java @@ -11,6 +11,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SystemConstants; import com.youlai.boot.core.security.util.JwtUtils; +import com.youlai.boot.shared.auth.service.AuthService; import com.youlai.boot.system.enums.ContactType; import com.youlai.boot.common.model.Option; import com.youlai.boot.shared.mail.service.MailService; @@ -317,17 +318,10 @@ public class UserServiceImpl extends ServiceImpl implements Us } String newPassword = data.getNewPassword(); - boolean result= this.update(new LambdaUpdateWrapper() + boolean result = this.update(new LambdaUpdateWrapper() .eq(User::getId, userId) .set(User::getPassword, passwordEncoder.encode(newPassword)) ); - if(result){ - String token = SecurityUtils.getTokenFromRequest(); - if (StrUtil.isNotBlank(token)) { - SecurityUtils.invalidateToken(token); - } - } - return result; } @@ -376,7 +370,7 @@ public class UserServiceImpl extends ServiceImpl implements Us throw new BusinessException("不支持的联系方式类型"); } // 存入 redis 用于校验, 5分钟有效 - redisTemplate.opsForValue().set(verificationCodePrefix + contact, code, 5, TimeUnit.MINUTES ); + redisTemplate.opsForValue().set(verificationCodePrefix + contact, code, 5, TimeUnit.MINUTES); return true; }