feat: 新增JWT刷新模式和JWT工具类解耦优化

This commit is contained in:
haoxr
2024-11-14 18:32:08 +08:00
parent c7621f90cb
commit 88ccaff448
8 changed files with 47 additions and 104 deletions

View File

@@ -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", "访问未授权"),

View File

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

View File

@@ -60,7 +60,7 @@ public class PermissionService {
);
if (!hasPermission) {
log.error("用户无操作权限");
log.error("用户无操作权限{}",requiredPerm);
}
return hasPermission;
}

View File

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

View File

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

View File

@@ -19,6 +19,6 @@ public class LoginResult {
private String refreshToken;
@Schema(description = "过期时间(单位:毫秒)")
private Long expires;
private Integer expiresIn;
}

View File

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

View File

@@ -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<UserMapper, User> implements Us
}
String newPassword = data.getNewPassword();
boolean result= this.update(new LambdaUpdateWrapper<User>()
boolean result = this.update(new LambdaUpdateWrapper<User>()
.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<UserMapper, User> 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;
}