feat: 新增JWT刷新模式和JWT工具类解耦优化
This commit is contained in:
@@ -36,7 +36,10 @@ public enum ResultCode implements IResultCode, Serializable {
|
|||||||
VERIFY_CODE_ERROR("A0214", "验证码错误"),
|
VERIFY_CODE_ERROR("A0214", "验证码错误"),
|
||||||
|
|
||||||
TOKEN_INVALID("A0230", "token无效或已过期"),
|
TOKEN_INVALID("A0230", "token无效或已过期"),
|
||||||
TOKEN_ACCESS_FORBIDDEN("A0231", "token已被禁止访问"),
|
REFRESH_TOKEN_INVALID("A0231", "刷新token无效或已过期"),
|
||||||
|
|
||||||
|
TOKEN_ACCESS_FORBIDDEN("A0232", "token已被禁止访问"),
|
||||||
|
|
||||||
|
|
||||||
AUTHORIZED_ERROR("A0300", "访问权限异常"),
|
AUTHORIZED_ERROR("A0300", "访问权限异常"),
|
||||||
ACCESS_UNAUTHORIZED("A0301", "访问未授权"),
|
ACCESS_UNAUTHORIZED("A0301", "访问未授权"),
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class ResponseUtils {
|
|||||||
public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {
|
public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {
|
||||||
// 根据不同的结果码设置HTTP状态
|
// 根据不同的结果码设置HTTP状态
|
||||||
int status = switch (resultCode) {
|
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();
|
case TOKEN_ACCESS_FORBIDDEN -> HttpStatus.FORBIDDEN.value();
|
||||||
default -> HttpStatus.BAD_REQUEST.value();
|
default -> HttpStatus.BAD_REQUEST.value();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public class PermissionService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
log.error("用户无操作权限");
|
log.error("用户无操作权限:{}",requiredPerm);
|
||||||
}
|
}
|
||||||
return hasPermission;
|
return hasPermission;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,63 +29,17 @@ import java.util.stream.Collectors;
|
|||||||
* @author Ray Hao
|
* @author Ray Hao
|
||||||
* @since 2.6.0
|
* @since 2.6.0
|
||||||
*/
|
*/
|
||||||
@Component
|
|
||||||
public class JwtUtils {
|
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
|
* 生成 JWT Token
|
||||||
*
|
*
|
||||||
* @param authentication 用户认证信息
|
* @param authentication 用户认证信息
|
||||||
|
* @param expiration 有效期(秒)
|
||||||
|
* @param key HS256(HmacSHA256)密钥
|
||||||
* @return Token 字符串
|
* @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();
|
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
|
||||||
|
|
||||||
@@ -136,31 +90,5 @@ public class JwtUtils {
|
|||||||
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
|
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);
|
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;
|
private String refreshToken;
|
||||||
|
|
||||||
@Schema(description = "过期时间(单位:毫秒)")
|
@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.captcha.generator.CodeGenerator;
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
import cn.hutool.jwt.JWT;
|
import cn.hutool.jwt.JWT;
|
||||||
import cn.hutool.jwt.JWTPayload;
|
import cn.hutool.jwt.JWTPayload;
|
||||||
import cn.hutool.jwt.JWTUtil;
|
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.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -65,8 +67,11 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
// 执行用户认证,认证成功返回的Authentication是SysUserDetailsService#loadUserByUsername获取到的的UserDetails
|
// 执行用户认证,认证成功返回的Authentication是SysUserDetailsService#loadUserByUsername获取到的的UserDetails
|
||||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||||
// 认证成功后生成JWT令牌
|
// 认证成功后生成JWT令牌
|
||||||
String accessToken = JwtUtils.createAccessToken(authentication);
|
Integer accessTokenExpiration = securityProperties.getJwt().getAccessTokenExpiration();
|
||||||
String refreshToken = JwtUtils.createRefreshToken(authentication);
|
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(如日志记录)中获取当前用户信息
|
// 将认证信息存入Security上下文,便于在AOP(如日志记录)中获取当前用户信息
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
// 返回包含JWT令牌的登录结果
|
// 返回包含JWT令牌的登录结果
|
||||||
@@ -74,6 +79,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
.tokenType("Bearer")
|
.tokenType("Bearer")
|
||||||
.accessToken(accessToken)
|
.accessToken(accessToken)
|
||||||
.refreshToken(refreshToken)
|
.refreshToken(refreshToken)
|
||||||
|
.expiresIn(accessTokenExpiration)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,8 +89,26 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
@Override
|
@Override
|
||||||
public void logout() {
|
public void logout() {
|
||||||
String token = SecurityUtils.getTokenFromRequest();
|
String token = SecurityUtils.getTokenFromRequest();
|
||||||
if (StrUtil.isNotBlank(token)) {
|
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
|
||||||
SecurityUtils.invalidateToken(token);
|
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);
|
boolean isValidate = jwt.setKey(securityProperties.getJwt().getKey().getBytes()).validate(0);
|
||||||
|
|
||||||
if (!isValidate || redisTemplate.hasKey(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jwt.getPayloads().getStr(JWTPayload.JWT_ID))) {
|
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());
|
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()
|
return LoginResult.builder()
|
||||||
.tokenType("Bearer")
|
.tokenType("Bearer")
|
||||||
.accessToken(newAccessToken)
|
.accessToken(newAccessToken)
|
||||||
.refreshToken(refreshToken) // 保持刷新令牌不变
|
.refreshToken(refreshToken)
|
||||||
|
.expiresIn(securityProperties.getJwt().getAccessTokenExpiration())
|
||||||
.build();
|
.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.RedisConstants;
|
||||||
import com.youlai.boot.common.constant.SystemConstants;
|
import com.youlai.boot.common.constant.SystemConstants;
|
||||||
import com.youlai.boot.core.security.util.JwtUtils;
|
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.system.enums.ContactType;
|
||||||
import com.youlai.boot.common.model.Option;
|
import com.youlai.boot.common.model.Option;
|
||||||
import com.youlai.boot.shared.mail.service.MailService;
|
import com.youlai.boot.shared.mail.service.MailService;
|
||||||
@@ -321,13 +322,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
.eq(User::getId, userId)
|
.eq(User::getId, userId)
|
||||||
.set(User::getPassword, passwordEncoder.encode(newPassword))
|
.set(User::getPassword, passwordEncoder.encode(newPassword))
|
||||||
);
|
);
|
||||||
if(result){
|
|
||||||
String token = SecurityUtils.getTokenFromRequest();
|
|
||||||
if (StrUtil.isNotBlank(token)) {
|
|
||||||
SecurityUtils.invalidateToken(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user