feat: 新增JWT刷新模式和JWT工具类解耦优化
This commit is contained in:
@@ -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", "访问未授权"),
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ public class PermissionService {
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
log.error("用户无操作权限");
|
||||
log.error("用户无操作权限:{}",requiredPerm);
|
||||
}
|
||||
return hasPermission;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ public class LoginResult {
|
||||
private String refreshToken;
|
||||
|
||||
@Schema(description = "过期时间(单位:毫秒)")
|
||||
private Long expires;
|
||||
private Integer expiresIn;
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user