package com.youlai.boot.security.token; import cn.hutool.core.convert.Convert; import cn.hutool.core.date.DateUtil; 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; 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.core.exception.BusinessException; import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.config.property.SecurityProperties; import com.youlai.boot.security.model.AuthenticationToken; import org.apache.commons.lang3.StringUtils; import com.youlai.boot.security.model.SysUserDetails; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Service; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * JWT Token 管理器 *

* 用于生成、解析、校验、刷新 JWT Token * * @author Ray.Hao * @since 2024/11/15 */ @ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") @Service public class JwtTokenManager implements TokenManager { private final SecurityProperties securityProperties; private final RedisTemplate redisTemplate; private final byte[] secretKey; public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { this.securityProperties = securityProperties; this.redisTemplate = redisTemplate; this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); } /** * 生成令牌 * * @param authentication 认证信息 * @return 令牌响应对象 */ @Override public AuthenticationToken generateToken(Authentication authentication) { int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); String accessToken = generateToken(authentication, accessTokenTimeToLive); String refreshToken = generateToken(authentication, refreshTokenTimeToLive, true); return AuthenticationToken.builder() .accessToken(accessToken) .refreshToken(refreshToken) .tokenType("Bearer") .expiresIn(accessTokenTimeToLive) .build(); } /** * 解析令牌 * * @param token JWT Token * @return Authentication 对象 */ @Override public Authentication parseToken(String token) { JWT jwt = JWTUtil.parseToken(token); JSONObject payloads = jwt.getPayloads(); SysUserDetails userDetails = new SysUserDetails(); userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 // 角色集合 Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) .stream() .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) .collect(Collectors.toSet()); return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); } /** * 校验令牌 * * @param token JWT Token * @return 是否有效 */ @Override public boolean validateToken(String token) { return validateToken(token, false); } /** * 校验刷新令牌 * * @param refreshToken JWT Token * @return 验证结果 */ @Override public boolean validateRefreshToken(String refreshToken) { return validateToken(refreshToken, true); } /** * 校验令牌 * * @param token JWT Token * @param validateRefreshToken 是否校验刷新令牌 * @return 是否有效 */ private boolean validateToken(String token, boolean validateRefreshToken) { JWT jwt = JWTUtil.parseToken(token); // 检查 Token 是否有效(验签 + 是否过期) boolean isValid = jwt.setKey(secretKey).validate(0); if (isValid) { JSONObject payloads = jwt.getPayloads(); // 1. 校验刷新令牌类型(仅在校验刷新令牌场景启用) String jti = payloads.getStr(JWTPayload.JWT_ID); if (validateRefreshToken) { //刷新token需要校验token类别 boolean isRefreshToken = payloads.getBool(JwtClaimConstants.TOKEN_TYPE); if (!isRefreshToken) { return false; } } // 2. 校验安全版本号(用于按用户维度失效历史 Token) // 场景示例:用户修改密码、被管理员强制下线、手动“踢所有端”后,将用户安全版本号 +1,旧版本 Token 全部失效 Long userId = payloads.getLong(JwtClaimConstants.USER_ID); if (userId != null) { // 老版本 Token 可能没有 SECURITY_VERSION 声明,视为 0 版本 Integer tokenVersionRaw = payloads.getInt(JwtClaimConstants.SECURITY_VERSION); int tokenVersion = tokenVersionRaw != null ? tokenVersionRaw : 0; String versionKey = StrUtil.format(RedisConstants.Auth.USER_SECURITY_VERSION, userId); Integer currentVersionRaw = (Integer) redisTemplate.opsForValue().get(versionKey); int currentVersion = currentVersionRaw != null ? currentVersionRaw : 0; // 如果当前版本号比 Token 携带的版本号新,则认为该 Token 已失效 if (tokenVersion < currentVersion) { return false; } } // 3. 判断是否在黑名单中,如果在,则返回 false 标识Token无效 // 场景示例:单点退出登录、后台手动注销某个会话、封禁账号后立即阻断当前 Token 等 if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) { return false; } } return isValid; } /** * 将令牌加入黑名单 * * @param token JWT Token */ @Override public void invalidateToken(String token) { if (StringUtils.isBlank(token)) { return; } if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); } JWT jwt = JWTUtil.parseToken(token); JSONObject payloads = jwt.getPayloads(); Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); // 黑名单Token Key String blacklistTokenKey = StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, payloads.getStr(JWTPayload.JWT_ID)); if (expirationAt != null) { int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); if (expirationAt < currentTimeSeconds) { // Token已过期,直接返回 return; } // 计算Token剩余时间,将其加入黑名单(值使用简单布尔标记即可) int expirationIn = expirationAt - currentTimeSeconds; redisTemplate.opsForValue().set(blacklistTokenKey, Boolean.TRUE, expirationIn, TimeUnit.SECONDS); } else { // 永不过期的Token永久加入黑名单 redisTemplate.opsForValue().set(blacklistTokenKey, Boolean.TRUE); } } /** * 失效指定用户的所有会话 *

* 通过提升用户的安全版本号,使携带旧版本号的 Token 在后续校验时全部失效 */ @Override public void invalidateUserSessions(Long userId) { if (userId == null) { return; } String versionKey = StrUtil.format(RedisConstants.Auth.USER_SECURITY_VERSION, userId); // 递增版本号 redisTemplate.opsForValue().increment(versionKey); } /** * 刷新令牌 * * @param refreshToken 刷新令牌 * @return 令牌响应对象 */ @Override public AuthenticationToken refreshToken(String refreshToken) { boolean isValid = validateRefreshToken(refreshToken); if (!isValid) { throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); } Authentication authentication = parseToken(refreshToken); int accessTokenExpiration = securityProperties.getSession().getAccessTokenTimeToLive(); String newAccessToken = generateToken(authentication, accessTokenExpiration); return AuthenticationToken.builder() .accessToken(newAccessToken) .refreshToken(refreshToken) .tokenType("Bearer") .expiresIn(accessTokenExpiration) .build(); } /** * 生成 JWT Token * * @param authentication 认证信息 * @param ttl 过期时间 * @return JWT Token */ private String generateToken(Authentication authentication, int ttl) { return generateToken(authentication, ttl, false); } /** * 生成 JWT Token * * @param authentication 认证信息 * @param ttl 过期时间 * @param isRefreshToken 类型是否为刷新token * @return JWT Token */ private String generateToken(Authentication authentication, int ttl, boolean isRefreshToken) { SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); Map payload = new HashMap<>(); payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 // claims 中添加角色信息 Set roles = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toSet()); payload.put(JwtClaimConstants.AUTHORITIES, roles); Date now = new Date(); payload.put(JWTPayload.ISSUED_AT, now); payload.put(JwtClaimConstants.TOKEN_TYPE, false); if (isRefreshToken) { payload.put(JwtClaimConstants.TOKEN_TYPE, true); } // 设置安全版本号:不存在时默认为 0 String versionKey = StrUtil.format(RedisConstants.Auth.USER_SECURITY_VERSION, userDetails.getUserId()); Integer currentVersion = (Integer) redisTemplate.opsForValue().get(versionKey); int securityVersion = currentVersion != null ? currentVersion : 0; payload.put(JwtClaimConstants.SECURITY_VERSION, securityVersion); // 设置过期时间 -1 表示永不过期 if (ttl != -1) { Date expiresAt = DateUtil.offsetSecond(now, ttl); payload.put(JWTPayload.EXPIRES_AT, expiresAt); } payload.put(JWTPayload.SUBJECT, authentication.getName()); payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); return JWTUtil.createToken(payload, secretKey); } }