All Downloads are FREE. Search and download functionalities are using the official Maven repository.

top.dcenter.ums.security.jwt.JwtContext Maven / Gradle / Ivy

Go to download

ums-jwt feature: JWT 创建(通过接口自定义 Claims, 通过配置设置算法等), 校验(通过接口自定义校验规则), 刷新(自动刷新, 直接拒绝, 通过 refreshToken 刷新), 刷新的 JWT 使旧 JWT 失效引发的并发访问问题及黑名单.

There is a newer version: 2.2.42
Show newest version
/*
 * MIT License
 * Copyright (c) 2020-2029 YongWu zheng (dcenter.top and gitee.com/pcore and github.com/ZeroOrInfinity)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package top.dcenter.ums.security.jwt;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import top.dcenter.ums.security.common.enums.ErrorCodeEnum;
import top.dcenter.ums.security.core.api.service.UmsUserDetailsService;
import top.dcenter.ums.security.jwt.api.cache.service.JwtCacheTransformService;
import top.dcenter.ums.security.jwt.api.id.service.JwtIdService;
import top.dcenter.ums.security.jwt.claims.service.GenerateClaimsSetService;
import top.dcenter.ums.security.jwt.decoder.UmsNimbusJwtDecoder;
import top.dcenter.ums.security.jwt.enums.JwtCustomClaimNames;
import top.dcenter.ums.security.jwt.enums.JwtRefreshHandlerPolicy;
import top.dcenter.ums.security.jwt.exception.JwtCreateException;
import top.dcenter.ums.security.jwt.exception.JwtExpiredException;
import top.dcenter.ums.security.jwt.exception.JwtInvalidException;
import top.dcenter.ums.security.jwt.exception.MismatchRefreshJwtPolicyException;
import top.dcenter.ums.security.jwt.exception.RefreshTokenInvalidException;
import top.dcenter.ums.security.jwt.exception.SaveRefreshTokenException;
import top.dcenter.ums.security.jwt.properties.BearerTokenProperties;
import top.dcenter.ums.security.jwt.properties.JwtBlacklistProperties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;

import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.ofNullable;
import static org.springframework.data.redis.connection.RedisStringCommands.SetOption.SET_IF_ABSENT;
import static org.springframework.data.redis.connection.RedisStringCommands.SetOption.UPSERT;
import static org.springframework.util.StringUtils.hasText;
import static org.springframework.web.context.request.RequestAttributes.SCOPE_SESSION;
import static top.dcenter.ums.security.common.utils.ReflectionUtil.invokeToJsonObjectMethod;
import static top.dcenter.ums.security.core.mdc.utils.MdcUtil.getMdcTraceId;
import static top.dcenter.ums.security.jwt.enums.JwtRefreshHandlerPolicy.AUTO_RENEW;
import static top.dcenter.ums.security.jwt.enums.JwtRefreshHandlerPolicy.REFRESH_TOKEN;

/**
 * JWT 上下文
 * @author YongWu zheng
 * @version V2.0  Created by 2020.12.8 15:40
 */
@SuppressWarnings("FieldMayBeFinal")
@Slf4j
public final class JwtContext {

    private JwtContext() {}

    public static final String KEY_ALGORITHM = "RSA";
    public static final int KEY_SIZE = 2048;
    public static final String BEARER = "bearer ";
    /**
     * 临时存储 jwt refresh token 的值
     */
    public static final String TEMPORARY_JWT_REFRESH_TOKEN = "TEMPORARY_JWT_REFRESH_TOKEN";

    /**
     * {@link JWSHeader#toJSONObject()} 的 {@link Method}, 增加对 nimbus-jose-jwt:9.x.x/8.x.x 的兼容性.
     */
    private static final Method TO_JSON_OBJECT_METHOD = ReflectionUtils.findMethod(JWSHeader.class, "toJSONObject");

    /**
     * JSON Web Signature (JWS) signer.
* 如果支持 JWT 功能, 通过 {@code top.dcenter.ums.security.jwt.config.JwtAutoConfiguration#afterPropertiesSet()} * 方法注入. */ private volatile static JWSSigner signer = null; /** * JSON Web Signature (JWS) algorithm.
* 如果支持 JWT 功能, 通过 {@code top.dcenter.ums.security.jwt.config.JwtAutoConfiguration#afterPropertiesSet()} * 方法注入. */ private volatile static String jwsAlgorithm = null; /** * JSON Web Signature (JWS) kid.
* 如果支持 JWT 功能, 通过 {@code top.dcenter.ums.security.jwt.config.JwtAutoConfiguration#afterPropertiesSet()} * 方法注入. */ private volatile static String kid = null; /** * JWT 的有效期, 默认: 1 小时 * 如果支持 JWT 功能, 通过 {@code top.dcenter.ums.security.jwt.config.JwtAutoConfiguration#afterPropertiesSet()} * 方法注入. */ private volatile static Duration timeout = Duration.ofHours(1); /** * JWT 不同服务器间的时钟偏差, 时钟可能存在偏差, 设置时钟偏移量以消除不同服务器间的时钟偏差的影响, 默认: 0 秒. * 注意: 此默认值适合 "单服务器" 应用, "多服务器" 应用请更改此值. * 如果支持 JWT 功能, 通过 {@code top.dcenter.ums.security.jwt.config.JwtAutoConfiguration#afterPropertiesSet()} * 方法注入. */ private volatile static Duration clockSkew = Duration.ofSeconds(0); /** * JWT 刷新处理策略. * 如果支持 JWT 功能, 通过 {@code top.dcenter.ums.security.jwt.config.JwtAutoConfiguration#afterPropertiesSet()} * 方法注入. */ private volatile static JwtRefreshHandlerPolicy refreshHandlerPolicy = null; /** * JWT 的 BearerToken 属性 * 如果支持 JWT 功能, 通过 {@code top.dcenter.ums.security.jwt.config.JwtAutoConfiguration#afterPropertiesSet()} * 方法注入. */ private volatile static BearerTokenProperties bearerToken = null; /** * RedisConnectionFactory * 如果支持 JWT 功能, 通过 {@code top.dcenter.ums.security.jwt.config.JwtAutoConfiguration#afterPropertiesSet()} * 方法注入. */ private volatile static RedisConnectionFactory redisConnectionFactory = null; /** * JwtBlacklistProperties * 如果支持 JWT 功能, 通过 {@code top.dcenter.ums.security.jwt.config.JwtAutoConfiguration#afterPropertiesSet()} * 方法注入. */ private volatile static JwtBlacklistProperties blacklistProperties = null; /** * {@link JwtIdService} * 如果支持 JWT 功能, 通过 {@code top.dcenter.ums.security.jwt.config.JwtAutoConfiguration#afterPropertiesSet()} * 方法注入. */ private volatile static JwtIdService jwtIdService = null; /** * {@link JwtCacheTransformService} * 如果支持 JWT 功能, 通过 {@code top.dcenter.ums.security.jwt.config.JwtAutoConfiguration#afterPropertiesSet()} * 方法注入. */ private volatile static JwtCacheTransformService jwtCacheTransformService = null; /** * principalClaimName * 如果支持 JWT 功能, 通过 {@code top.dcenter.ums.security.jwt.config.JwtAutoConfiguration#afterPropertiesSet()} * 方法注入. */ private volatile static String principalClaimName = null; // ====================== JWK 相关 ====================== /** * 生成 {@link JWK}, 与下面的方式等效: *
     * // Generate 2048-bit RSA key pair in JWK format, attach some metadata
     * RSAKey jwk = new RSAKeyGenerator(2048)
     *         .keyUse(KeyUse.SIGNATURE) // indicate the intended use of the key
     *         .keyID(kid) // give the key a unique ID
     *         .generate();
     * 
* * @param kid Sets the ID (kid) of the JWK. The key ID can be used to match a specific key. * This can be used, for instance, to choose a key within a JWKSet during key rollover. * The key ID may also correspond to a JWS/JWE kid header parameter value. * @param keyUse Enumeration of public key uses. Represents the use parameter in a JSON Web Key (JWK). * Public JWK use values: {@link KeyUse#SIGNATURE} {@link KeyUse#ENCRYPTION} * @return 返回 {@link JWK} * @throws NoSuchAlgorithmException 异常 */ @SuppressWarnings("unused") public static JWK generateJwk(String kid, KeyUse keyUse) throws NoSuchAlgorithmException { // Generate 2048-bit RSA key pair KeyPairGenerator gen = KeyPairGenerator.getInstance(KEY_ALGORITHM); gen.initialize(KEY_SIZE); KeyPair keyPair = gen.generateKeyPair(); // Convert to JWK format return new RSAKey.Builder((RSAPublicKey)keyPair.getPublic()) .privateKey((RSAPrivateKey)keyPair.getPrivate()) .keyUse(keyUse) .keyID(kid) .build(); } /** * 生成 {@link JWK}, 与下面的方式等效: *
     * // Generate 2048-bit RSA key pair in JWK format, attach some metadata
     * RSAKey jwk = new RSAKeyGenerator(2048)
     *         .keyUse(KeyUse.SIGNATURE) // indicate the intended use of the key
     *         .keyID(kid) // give the key a unique ID
     *         .generate();
     * 
* @param keyPair 此类是密钥对(公共密钥和私有密钥)的简单持有者。 它不强制执行任何安全性,并且在初始化时应将其视为私钥. * @param kid Sets the ID (kid) of the JWK. The key ID can be used to match a specific key. * This can be used, for instance, to choose a key within a JWKSet during key rollover. * The key ID may also correspond to a JWS/JWE kid header parameter value. * @param keyUse Enumeration of public key uses. Represents the use parameter in a JSON Web Key (JWK). * Public JWK use values: {@link KeyUse#SIGNATURE} {@link KeyUse#ENCRYPTION} * @return 返回 {@link JWK} */ public static JWK generateJwk(KeyPair keyPair, String kid, KeyUse keyUse) { // Convert to JWK format return new RSAKey.Builder((RSAPublicKey)keyPair.getPublic()) .privateKey((RSAPrivateKey)keyPair.getPrivate()) .keyUse(keyUse) .keyID(kid) .build(); } // ====================== 创建 Jwt 或 Jwt 转 Authentication 相关 ====================== /** * 用户登录成功时调用此方法来生成 jwt. * 如果符合条件, 生成 {@link Jwt} 并转换为 {@link JwtAuthenticationToken}, 不符合条件则原样返回. * @param authentication authentication * @param generateClaimsSetService generateClaimsSetService * @return 如果符合条件, 返回 {@link JwtAuthenticationToken}, 不符合条件则原样返回 {@link Authentication} * @throws JwtCreateException 转换异常 */ @NonNull public static Authentication createJwtAndToJwtAuthenticationToken(@NonNull Authentication authentication, @Nullable GenerateClaimsSetService generateClaimsSetService) throws JwtCreateException { // 生成 Jwt 并转换为 JwtAuthenticationToken if (nonNull(generateClaimsSetService) && isSupportCreateJwt(authentication)) { try { // 是否生成 refreshToken JWT Jwt refreshTokenJwt = null; if (REFRESH_TOKEN.equals(JwtContext.refreshHandlerPolicy)) { // 生成 refreshToken 并缓存进 redis refreshTokenJwt = generateRefreshToken(authentication.getName()); } // 生成 JWT JWTClaimsSet claimsSet = generateClaimsSetService.generateClaimsSet(authentication, refreshTokenJwt); Jwt jwt = createJwt(claimsSet); setBearerTokenAndRefreshTokenToHeader(jwt, refreshTokenJwt, FALSE); /* 转换为 JwtAuthenticationToken, 再根据 isSetContext 保存 jwtAuthenticationToken 到 SecurityContext, 如果 JwtBlacklistProperties.getEnable() = false, 则同时保存到 redis 缓存 */ JwtAuthenticationToken authenticationToken = toJwtAuthenticationToken(jwt, generateClaimsSetService.getJwtAuthenticationConverter(), FALSE); // 删除 reAuth 标志. removeReAuthFlag(authentication.getName()); return authenticationToken; } catch (Exception e) { String msg = String.format("创建 jwt token 失败: %s", authentication); log.error(msg, e); throw new JwtCreateException(ErrorCodeEnum.CREATE_JWT_ERROR, getMdcTraceId()); } } // 原样返回 return authentication; } // ====================== 刷新 Jwt 相关 ====================== /** * 重置旧 {@link Jwt} 的 exp/iat/nbf 的时间戳并返回重新签名后的 {@link Jwt}, * 注意: {@link Jwt} 中 tokenValue 的"日期"都以"时间戳"表示且"时间戳"以秒为单位 * @param jwt 过期的 {@link Jwt} * @param jwtDecoder {@link UmsNimbusJwtDecoder} * @return 返回 重置旧 {@link Jwt} 的 exp/iat/nbf 的时间戳并重新签名后的 {@link Jwt} * @throws ParseException 重置 jwt 异常 * @throws JOSEException 重置 jwt 异常 * @throws JwtInvalidException jwt 失效异常 */ @NonNull public static Jwt resetJwtExpOfAutoRenewPolicy(@NonNull Jwt jwt, @NonNull UmsNimbusJwtDecoder jwtDecoder, @NonNull JwtRefreshHandlerPolicy policy) throws ParseException, JOSEException, JwtInvalidException { Jwt newJwt; if (!AUTO_RENEW.equals(policy)) { throw new MismatchRefreshJwtPolicyException(ErrorCodeEnum.REFRESH_JWT_POLICY_MISMATCH, getMdcTraceId()); } // 1. 校验 jwt 是否在黑名单, 是否需要重新认证(reAuth) BlacklistType blacklistType = jtiInTheBlacklist(jwt); /* - newJwtString == null 表示不在黑名单中, - newJwtString != null 表示在黑名单中, 但缓存中存储有新的且有效 JWT 则返回新的 jwt 字符串, - 抛出异常表示在黑名单中, 但缓存中没有新的 jwt 字符串, 则需要重新认证. */ String newJwtString = inBlacklistAndHasNewJwt(blacklistType); if (newJwtString != null) { newJwt = jwtDecoder.decodeNotValidate(removeBearerForJwtTokenString(newJwtString)); // 新的 jwt 设置 header setBearerTokenAndRefreshTokenToHeader(newJwt, null, FALSE); return newJwt; } if (isNull(signer)) { // 当 signer 未 null 时, 表明只是解析 JWT 的场景, 不支持重新刷新 JWT, 需要重新认证 throw new JwtInvalidException(ErrorCodeEnum.JWT_INVALID, getMdcTraceId()); } // 2. 重置jwt Map claims = jwt.getClaims(); // 重置 jti claims.put(JwtClaimNames.JTI, jwtIdService.generateJtiId()); // 重置 exp/iat/nbf 的时间戳 Instant now = Instant.now(); claims.put(JwtClaimNames.EXP, now.plusSeconds(timeout.getSeconds())); Instant iat = (Instant) claims.get(JwtClaimNames.IAT); Instant nbf = (Instant) claims.get(JwtClaimNames.NBF); long nowEpochSecond = now.getEpochSecond(); if (nonNull(iat)) { claims.put(JwtClaimNames.IAT, nowEpochSecond); if (nonNull(nbf)) { claims.put(JwtClaimNames.NBF, nowEpochSecond + (nbf.getEpochSecond() - iat.getEpochSecond())); } } // 转换其他 Claims 的 Instant 类型为以秒记的时间戳 claims.entrySet() .stream() .filter((entry -> entry.getValue() instanceof Instant)) .forEach(entry -> entry.setValue(((Instant) entry.getValue()).getEpochSecond())); newJwt = createJwt(getJwsHeader(), toJwtClaimsSet(claims)); // 3. 添加黑名单 addBlacklist(jwt, newJwt); // 4. 新的 jwt 设置 header, 并检查 refresh token 是否需要重新生成. setBearerTokenAndRefreshTokenToHeader(newJwt, null, FALSE); return newJwt; } /** * 根据 refresh token 获取新 Jwt 处理器 * * @param refreshToken refresh token * @param alwaysRefresh 如果 alwaysRefresh = false, oldJwt 剩余有效期没在 ums.jwt.remainingRefreshInterval * 的时间内, 原样返回 oldJwt, 如果 ums.jwt.alwaysRefresh = true, * 每次通过 refreshToken 刷新 jwt 则总是返回 newJwt. * @param request {@link HttpServletRequest} * @param jwtDecoder {@link UmsNimbusJwtDecoder} * @param umsUserDetailsService {@link UmsUserDetailsService} * @param generateClaimsSetService {@link GenerateClaimsSetService} * @return 返回新的 {@link Jwt} * @throws JwtCreateException Jwt 创建 异常 * @throws RefreshTokenInvalidException refreshToken 失效异常 */ @NonNull public static Jwt generateJwtByRefreshToken(@NonNull String refreshToken, @NonNull Boolean alwaysRefresh, @NonNull HttpServletRequest request, @NonNull UmsNimbusJwtDecoder jwtDecoder, @NonNull UmsUserDetailsService umsUserDetailsService, @NonNull GenerateClaimsSetService generateClaimsSetService) throws JwtCreateException, RefreshTokenInvalidException, JwtInvalidException { // 1. 获取 userId 且判断 refreshToken 是否有效 Jwt refreshTokenJwt = jwtDecoder.decodeRefreshTokenOfJwt(removeBearerForJwtTokenString(refreshToken)); String userIdByRefreshToken = getUserIdByRefreshToken(refreshTokenJwt); if (!hasText(userIdByRefreshToken)) { throw new RefreshTokenInvalidException(ErrorCodeEnum.JWT_REFRESH_TOKEN_INVALID, getMdcTraceId()); } // 2. 获取用户信息, 并创建 Authentication UserDetails userDetails = umsUserDetailsService.loadUserByUserId(userIdByRefreshToken); // 3. 检查旧的 jwt 是否在刷新的时间访问内() Jwt newJwt = null; Jwt oldJwt = getJwtByRequest(request, jwtDecoder); /* 如果 alwaysRefresh = false, oldJwt 剩余有效期没在 ums.jwt.remainingRefreshInterval 的时间内, 原样返回 oldJwt, 如果 ums.jwt.alwaysRefresh = true, 每次通过 refreshToken 刷新 jwt 则总是返回 newJwt. */ if (!alwaysRefresh && nonNull(oldJwt)) { // 检查是否需要刷新 Instant expiresAt = oldJwt.getExpiresAt(); if (nonNull(expiresAt)) { long remainingRefreshInterval = jwtDecoder.getRemainingRefreshInterval().getSeconds(); boolean toRefresh = (expiresAt.getEpochSecond() - Instant.now().getEpochSecond()) < remainingRefreshInterval; // 没有在指定的刷新时间间隔内, 不执行刷新操作 if (!toRefresh) { newJwt = oldJwt; } } } // 4. 创建 JWT if (isNull(newJwt)) { JWTClaimsSet jwtClaimsSet = generateClaimsSetService.generateClaimsSet(userDetails, refreshTokenJwt); try { newJwt = createJwt(jwtClaimsSet); } catch (JOSEException | ParseException e) { log.error(e.getMessage(), e); throw new JwtCreateException(ErrorCodeEnum.CREATE_JWT_ERROR, getMdcTraceId()); } } if (!blacklistProperties.getEnable()) { /* 转换为 JwtAuthenticationToken, 再根据 isSetContext 保存 jwtAuthenticationToken 到 SecurityContext, 如果 JwtBlacklistProperties.getEnable() = false, 则同时保存到 redis 缓存 */ toJwtAuthenticationToken(newJwt, generateClaimsSetService.getJwtAuthenticationConverter(), TRUE); } // 5. oldJwt != null 且与 newJwt 不相等, 则 oldJwt 添加黑名单, 以解决刷新 jwt 而引发的并发访问问题. if (nonNull(oldJwt) && !Objects.equals(newJwt, oldJwt)) { setOldJwtToBlacklist(refreshToken, userIdByRefreshToken, oldJwt, newJwt); } // 6. 设置 jwt 到 header setBearerTokenAndRefreshTokenToHeader(newJwt, refreshTokenJwt, TRUE); return newJwt; } /** * 从 session 中获取 name 为 {@link #TEMPORARY_JWT_REFRESH_TOKEN} 的 值, * 注意: 只有在用户登录成功后且支持 refreshToken 功能,才可以获取到 refreshToken. * 只能获取一次, 获取后自动删除 session 的 refreshToken * @return 返回 jwt refresh token, 如果不存在则返回 null */ @Nullable public static String getJwtRefreshTokenFromSession() { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); String refreshToken = (String) requestAttributes.getAttribute(TEMPORARY_JWT_REFRESH_TOKEN, SCOPE_SESSION); if (nonNull(refreshToken)) { requestAttributes.removeAttribute(TEMPORARY_JWT_REFRESH_TOKEN, SCOPE_SESSION); } return refreshToken; } /** * 根据 jwtToken 字符串从 redis 中获取 {@link JwtAuthenticationToken}. * 当 jwtTokenString 失效时返回 null, * 当 {@link JwtBlacklistProperties#getEnable()} 为 true 时返回 null, * @param jwt {@link Jwt} * @return 返回 {@link JwtCacheTransformService#getClazz()} 的类型; 当 jwtTokenString 失效时返回 null, * 当 {@link JwtBlacklistProperties#getEnable()} 为 true 时返回 null. */ @Nullable public static Object getTokenInfoFromRedis(@NonNull Jwt jwt) throws SerializationException { try (RedisConnection connection = getConnection()) { if (blacklistProperties.getEnable()) { return null; } byte[] authBytes = connection.get(getTokenKey(jwt)); if (isNull(authBytes)) { return null; } try { return jwtCacheTransformService.deserialize(authBytes); } catch (Exception e) { log.error(e.getMessage(), e); throw e; } } } /** * 是否通过 refreshToken 刷新 jwt * @return 返回 true 表示通过 refreshToken 刷新 jwt */ @NonNull public static Boolean isRefreshJwtByRefreshToken() { if (isNull(refreshHandlerPolicy)) { return FALSE; } boolean isRefreshPolicy = REFRESH_TOKEN.equals(refreshHandlerPolicy); if (!isRefreshPolicy) { return FALSE; } ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); String refreshToken = (String) requestAttributes.getAttribute(TEMPORARY_JWT_REFRESH_TOKEN, SCOPE_SESSION); return StringUtils.hasText(refreshToken); } // ====================== jwt 黑名单 相关 ====================== /** * 返回 null 表示不在黑名单中, 返回不为 null 则表示在黑名单中, 但缓存中存储有新的且有效 JWT 则返回新的 jwt 字符串. * 注意: 必须先调用 {@link #jtiInTheBlacklist(Jwt)}, 再调用此方法. * @param blacklistType {@link BlacklistType} * @return 返回 null 表示不在黑名单中, 返回不为 null 则表示在黑名单中, 但缓存中存储有新的且有效 JWT 则返回新的 jwt 字符串. * @throws JwtInvalidException 表示在黑名单中, 但缓存中没有新的 jwt 字符串, 则需要重新认证. */ @Nullable public static String inBlacklistAndHasNewJwt(@NonNull BlacklistType blacklistType) throws JwtInvalidException { if (blacklistType.isInBlacklist()) { String blackListValue = blacklistType.getOneTimeNewJwtValue(); // 在黑名单中, 但缓存中存储有新的且有效 JWT 则返回新的 jwt 字符串 return ofNullable(blackListValue) // 在黑名单中, 但缓存中没有新的 jwt 字符串, 则需要重新认证, 抛出异常 .orElseThrow(() -> new JwtInvalidException(ErrorCodeEnum.JWT_INVALID, getMdcTraceId())); } // 不在黑名单中, 则返回 null return null; } /** * jti 是否在黑名单中;
* 此方法已调用 {@link BlacklistType#getBlacklistType(String)}, 如果返回值为 * {@link BlacklistType#IN_BLACKLIST_AND_HAS_NEW_JWT}, 可调用 {@link BlacklistType#getOneTimeNewJwtValue()} 获取新的 * jwt 字符串. * @param jwt {@link Jwt} * @return 返回 {@link BlacklistType} */ @NonNull public static BlacklistType jtiInTheBlacklist(@NonNull Jwt jwt) { // 不支持黑名单逻辑 try (RedisConnection connection = getConnection()) { if (!blacklistProperties.getEnable()) { Boolean exists = connection.exists(getTokenKey(jwt)); if (nonNull(exists) && exists) { return BlacklistType.NOT_IN_BLACKLIST; } else { return BlacklistType.IN_BLACKLIST; } } // 支持黑名单逻辑 // 从 redis jwt 黑名单中获取 jti 的值 byte[] result = connection.get(getBlacklistKey(jwt.getId())); String blacklistValue; if (isNull(result)) { blacklistValue = null; } else { blacklistValue = new String(result, StandardCharsets.UTF_8); } return BlacklistType.getBlacklistType(blacklistValue); } } /** * 检查 refreshJwt 是否在黑名单中
* @param refreshJwt refresh jwt * @return 返回 true 表示在黑名单, 返回 false 表示不在黑名单 */ @NonNull public static Boolean isRefreshJwtInTheBlacklist(@NonNull Jwt refreshJwt) { try (RedisConnection connection = getConnection()) { // 不支持黑名单逻辑 if (!blacklistProperties.getEnable()) { Boolean exists = connection.exists(getRefreshTokenKey(refreshJwt.getClaimAsString(principalClaimName))); return isNull(exists) || !exists; } // 支持黑名单逻辑 // 从 redis jwt 黑名单中获取 jti 的值 Boolean exists = connection.exists(getBlacklistKey(refreshJwt.getId())); return nonNull(exists) && exists; } } /** * 添加 refreshToken 到 黑名单 * @param refreshTokenJwt refresh token jwt */ public static void addBlacklistForRefreshToken(@NonNull Jwt refreshTokenJwt) { String userId = refreshTokenJwt.getClaimAsString(principalClaimName); try (RedisConnection connection = getConnection()) { // 不支持黑名单逻辑 if (!blacklistProperties.getEnable()) { connection.del(getRefreshTokenKey(userId)); return; } // 支持黑名单逻辑 Instant expiresAt = refreshTokenJwt.getExpiresAt(); if (isNull(expiresAt)) { return; } Instant now = Instant.now(); // jwt 还在有效期内, 放入黑名单 if (expiresAt.isAfter(now.minus(clockSkew))) { connection.set(getBlacklistKey(refreshTokenJwt.getId()), BlacklistType.IN_BLACKLIST.name().getBytes(StandardCharsets.UTF_8), Expiration.seconds(expiresAt.getEpochSecond() - now.getEpochSecond() + clockSkew.getSeconds()), SET_IF_ABSENT); } } } /** * 添加黑名单 * @param oldJwt 要加入黑名单的 {@link Jwt} */ public static void addBlacklistForReAuth(@NonNull Jwt oldJwt) { addBlacklist(oldJwt, BlacklistType.IN_BLACKLIST.name().getBytes(StandardCharsets.UTF_8), TRUE); } /** * jwt 还在有效期内, 添加黑名单 * @param oldJwt 要加入黑名单的 {@link Jwt} * @param newJwt 替换 oldJwt 的 {@link Jwt}, 用于旧 jwt 失效引发的并发访问问题. */ private static void addBlacklist(@NonNull Jwt oldJwt, @NonNull Jwt newJwt) { addBlacklist(oldJwt, newJwt.getTokenValue().getBytes(StandardCharsets.UTF_8), FALSE); } // ====================== 辅助 相关 ====================== /** * 从 request 中获取 refreshToken 或 bearerToken * * @param request {@link HttpServletRequest} * @param parameterName request 中的参数名称 * @param headerName request 中的 header 的名称 * @return 返回 refreshToken 或 bearerToken, 如果 request 不存在对应的值则返回 null. */ @Nullable public static String getRefreshTokenOrBearerToken(@NonNull HttpServletRequest request, @NonNull String parameterName, @NonNull String headerName) { if (isNull(bearerToken)) { return null; } String token; Boolean allowFormEncodedBodyParameter = bearerToken.getAllowFormEncodedBodyParameter(); Boolean allowUriQueryParameter = bearerToken.getAllowUriQueryParameter(); if (allowFormEncodedBodyParameter || allowUriQueryParameter) { // 从 request 中获取 refreshToken 或 bearerToken token = request.getParameter(parameterName); } else { // 从 header 中获取 refreshToken 或 bearerToken token = request.getHeader(headerName); } return token; } /** * 获取 JWT 不同服务器间的时钟偏差, 通过 {@code ums.jwt.clockSkew } 属性设置. * @return 返回时钟偏差的 {@link Duration} */ @NonNull public static Duration getClockSkew() { return clockSkew; } /** * 获取 jwt 字符串, 如果 authentication 不是 {@link AbstractOAuth2TokenAuthenticationToken} 或 * {@link BearerTokenProperties#getAllowFormEncodedBodyParameter()} = false 则返回 null * @param authentication authentication * @return 返回 jwt 字符串, 如果 authentication 不是 {@link AbstractOAuth2TokenAuthenticationToken} 或 * {@link BearerTokenProperties#getAllowFormEncodedBodyParameter()} = false 则返回 null */ @Nullable public static String getJwtStringIfAllowBodyParameter(@NonNull Authentication authentication) { if (isNull(bearerToken)) { return null; } Boolean allowFormEncodedBodyParameter = bearerToken.getAllowFormEncodedBodyParameter(); if ((authentication instanceof AbstractOAuth2TokenAuthenticationToken) && allowFormEncodedBodyParameter) { //noinspection unchecked AbstractOAuth2TokenAuthenticationToken jwtAuthenticationToken = ((AbstractOAuth2TokenAuthenticationToken) authentication); return jwtAuthenticationToken.getToken().getTokenValue(); } return null; } /** * 获取 jwt 的有效期 * @param authentication {@link Authentication} * @return 返回 jwt 的有效期, 如果不支持 jwt 直接返回 null */ @Nullable public static Long getJwtExpiresInByAuthentication(Authentication authentication) { if (isNull(timeout) && isNull(bearerToken)) { return null; } if ((authentication instanceof AbstractOAuth2TokenAuthenticationToken)) { return timeout.minusSeconds(1L).getSeconds(); } return null; } // ====================== ReAuthService 相关 ====================== /** * 添加需要用户重新登录认证的标志, 注意: 需要用户重新登录认证时调用. * @param userId 用户 ID * @return 返回 true 表示设置成功, 返回 false 表示设置失败. */ @NonNull public static Boolean addReAuthFlag(@NonNull String userId) { try (RedisConnection connection = getConnection()) { Boolean result = connection.set(getReAuthKey(userId), "1".getBytes(StandardCharsets.UTF_8), Expiration.seconds(blacklistProperties.getRefreshTokenTtl().getSeconds()), SET_IF_ABSENT); if (isNull(result)) { return FALSE; } return result; } } /** * 添加需要用户重新登录认证的标志, 注意: 需要用户重新登录认证时调用. * @param userId 用户 ID * @return 返回 true 表示设置成功, 返回 false 表示设置失败. */ @NonNull public static Boolean isReAuth(@NonNull String userId) { try (RedisConnection connection = getConnection()) { Boolean exists = connection.exists(getReAuthKey(userId)); return nonNull(exists) && exists; } } // ====================== 内部私有方法 ====================== // ====================== ReAuthService 私有方法 ====================== /** * 删除需要用户重新登录认证的标志以及对应的锁标志, 注意: 需要用户登录成功时调用. * @param userId 用户 ID */ private static void removeReAuthFlag(@NonNull String userId) { try (RedisConnection connection = getConnection()) { connection.del(getReAuthKey(userId), getDelAllTokenInfoInRedisLockKey(userId)); } } // ====================== 黑名单 与 redis 私有方法 ====================== /** * 保存 {@link Authentication} 到 redis 缓存, {@link Jwt} 的 jwtTokenString 作为 key. * 保存 authentication 到 *
     *     SecurityContextHolder.getContext().setAuthentication(authentication);
     * 
* @param authentication {@link Authentication} * @param jwt {@link Jwt} * @param isSetContext authentication 是否设置到 SecurityContext */ private static void saveTokenSessionToRedis(@NonNull Authentication authentication, @NonNull Jwt jwt, @SuppressWarnings("SameParameterValue") @NonNull Boolean isSetContext) { // 如果不支持 jwt 黑名单, 添加到 redis 缓存 if (!blacklistProperties.getEnable()) { byte[] tokenValue = jwtCacheTransformService.serialize(authentication); Instant expiresAt = jwt.getExpiresAt(); if (isNull(expiresAt)) { return; } if (isSetContext) { SecurityContextHolder.getContext().setAuthentication(authentication); } final Duration ttl = Duration.ofSeconds(expiresAt.getEpochSecond() - Instant.now().getEpochSecond()); try (RedisConnection connection = getConnection()) { connection.set(getTokenKey(jwt), tokenValue, Expiration.from(ttl), UPSERT); } } } /** * 获取旧的 jwt, 未失效则添加黑名单, 以解决刷新 jwt 而引发的并发访问问题. * @param refreshToken refreshToken * @param userIdByRefreshToken userIdByRefreshToken * @param oldJwt 旧的 {@link Jwt} * @param newJwt 新的 {@link Jwt} */ private static void setOldJwtToBlacklist(@NonNull String refreshToken, @NonNull String userIdByRefreshToken, @NonNull Jwt oldJwt, @NonNull Jwt newJwt) { try (RedisConnection connection = getConnection()) { // 不支持 jwt 黑名单逻辑 if (!blacklistProperties.getEnable()) { // 删除 redis 中的 oldJwt 缓存 connection.del(getTokenKey(oldJwt)); return; } // 校验 refresh 的 jwt 与 旧 jwt 是否相同 userId Object oldUserId = oldJwt.getClaim(principalClaimName); if (!Objects.equals(oldUserId, userIdByRefreshToken)) { log.error("oldUserId: {} 与 userIdByRefreshToken: {} 不匹配, refreshToken: {}", oldUserId, userIdByRefreshToken, refreshToken); // userId 与 refreshToken 不匹配, 删除 refreshToken connection.del(getRefreshTokenKey(userIdByRefreshToken)); throw new RefreshTokenInvalidException(ErrorCodeEnum.JWT_REFRESH_TOKEN_INVALID, getMdcTraceId()); } addBlacklist(oldJwt, newJwt); } } /** * jwt 还在有效期内, 添加黑名单, 如果不支持黑名单直接删除 redis 中 oldJwt 对应的 key, * 如果 isReAuth 为 true 则也把 refreshToken 放入黑名单. * @param oldJwt 要加入黑名单的 {@link Jwt} * @param value newJwt 的 byte 数组 或 "IN_BLACKLIST", 用于旧 jwt 失效引发的并发访问问题. * @param isReAuth 是否重新认证 */ private static void addBlacklist(@NonNull Jwt oldJwt, @NonNull byte[] value, @NonNull Boolean isReAuth) { String userId = oldJwt.getClaimAsString(principalClaimName); boolean isReAuthAndRefreshPolicy = isReAuth && REFRESH_TOKEN.equals(refreshHandlerPolicy); try (RedisConnection connection = getConnection()) { // 不支持黑名单逻辑 if (!blacklistProperties.getEnable()) { if (isReAuthAndRefreshPolicy) { connection.del(getRefreshTokenKey(userId)); // 删除同一用户下的所有客户端登录信息 delAllTokenInfoInRedisByUserId(userId, connection); } else { connection.del(getTokenKey(oldJwt)); } return; } // 支持黑名单逻辑 Instant expiresAt = oldJwt.getExpiresAt(); if (isNull(expiresAt)) { return; } Instant now = Instant.now(); // 旧的 jwt 还在有效期内, 放入黑名单 if (expiresAt.isAfter(now.minus(clockSkew))) { connection.set(getBlacklistKey(oldJwt.getId()), value, Expiration.seconds(expiresAt.getEpochSecond() - now.getEpochSecond() + clockSkew.getSeconds()), SET_IF_ABSENT); } // 如果需要重新认证, 对 refreshToken 也一并加入黑名单. if (isReAuthAndRefreshPolicy) { String rJti = oldJwt.getClaimAsString(JwtCustomClaimNames.REFRESH_TOKEN_JTI.getClaimName()); connection.set(getBlacklistKey(rJti), value, Expiration.seconds(blacklistProperties.getRefreshTokenTtl().getSeconds()), SET_IF_ABSENT); } } } /** * 删除 userId 用户所有客户端在 redis 的 tokenInfo; 发生错误未做处理, 待扩展. * @param userId 用户唯一 ID * @param connection {@link RedisConnection}, 在此方法中不会关闭吃连接, 需要调用方关闭 */ private static void delAllTokenInfoInRedisByUserId(String userId, RedisConnection connection) { // 用于防止用户并发访问时重复执行删除动作(scan) if (getDelAllTokenInfoInRedisLock(userId, connection)) { ScanOptions options = ScanOptions.scanOptions() .count(1000) .match(blacklistProperties.getTokenInfoPrefix() .concat(userId) .concat(":*")) .build(); try (Cursor cursor = connection.scan(options)) { ArrayList tokenInfoKeyList = new ArrayList<>(); while (cursor.hasNext()) { tokenInfoKeyList.add(cursor.next()); } // 删除同一用户所有客户端的 tokenInfo if (!CollectionUtils.isEmpty(tokenInfoKeyList)) { connection.del(tokenInfoKeyList.toArray(new byte[0][0])); } } catch (IOException e) { log.error(e.getMessage(), e); connection.del(getDelAllTokenInfoInRedisLockKey(userId)); // 待扩展: 删除同一用户所有客户端的 tokenInfo 发生错误, 未做处理 } } } // ====================== redis 获取指定 key 或 RedisConnection 私有方法 ====================== /** * 获取 jwt 黑名单 redis key * @param jti {@link JwtClaimNames#JTI} * @return 返回 jwt 黑名单 redis key. */ @NonNull private static byte[] getBlacklistKey(String jti) { return blacklistProperties.getBlacklistPrefix().concat(jti).getBytes(StandardCharsets.UTF_8); } /** * 获取是否需要重新登录认证的 redis key * @param userId 用户 Id * @return 返回 获取是否需要重新登录认证的 redis key */ @NonNull private static byte[] getReAuthKey(String userId) { return blacklistProperties.getReAuthPrefix().concat(userId).getBytes(StandardCharsets.UTF_8); } /** * 获取删除 redis 中指定用户的所有 TokenInfo 的 redis 锁的 key * @param userId 用户 Id * @return 返回 删除 redis 中指定用户的所有 TokenInfo 的 redis 锁的 key */ @NonNull private static byte[] getDelAllTokenInfoInRedisLockKey(String userId) { return blacklistProperties.getReAuthPrefix().concat("LOCK:" + userId).getBytes(StandardCharsets.UTF_8); } /** * JWT + SESSION 模式时: * 获取删除 redis 中指定用户的所有 TokenInfo 的 redis 锁, 用于防止重复执行删除动作(含scan), * @param userId 用户 Id * @param connection {@link RedisConnection}, 在此方法中不会关闭吃连接, 需要调用方关闭 * @return 返回 true 表示执行 删除 redis 中指定用户的所有 TokenInfo 操作, 返回 false 表示不需要执行 删除 redis 中指定用户的所有 TokenInfo 操作. */ @NonNull private static Boolean getDelAllTokenInfoInRedisLock(String userId, RedisConnection connection) { byte[] lockKey = getDelAllTokenInfoInRedisLockKey(userId); Boolean lock = connection.setNX(lockKey, "1".getBytes(StandardCharsets.UTF_8)); return ofNullable(lock).orElse(false); } /** * 获取 jwt refresh token redis key * @param userId 用户 Id * @return 返回 refresh token redis key */ @NonNull private static byte[] getRefreshTokenKey(String userId) { return blacklistProperties.getRefreshTokenPrefix().concat(userId).getBytes(StandardCharsets.UTF_8); } /** * 获取 jwt token redis key: TokenInfoPrefix + userId:jti * @param jwt {@link Jwt} * @return 返回 token 的 redis key: TokenInfoPrefix + userId:jti */ @NonNull private static byte[] getTokenKey(Jwt jwt) { return blacklistProperties.getTokenInfoPrefix() .concat(jwt.getClaimAsString(principalClaimName) + ":" + jwt.getId()) .getBytes(StandardCharsets.UTF_8); } /** * 获取 {@link RedisConnection} * @return 返回 {@link RedisConnection} */ private static RedisConnection getConnection() { return redisConnectionFactory.getConnection(); } // ====================== refreshToken 辅助私有方法 ====================== /** * 从 request 中获取 jwt * @param request {@link HttpServletRequest} * @param jwtDecoder {@link UmsNimbusJwtDecoder} * @return 返回 {@link Jwt}, 如果 request 中无 jwt 字符串或 jwt 字符串已失效, 则返回 null */ @Nullable private static Jwt getJwtByRequest(@NonNull HttpServletRequest request, @NonNull UmsNimbusJwtDecoder jwtDecoder) { Jwt oldJwt; // 获取 jwt 字符串 String jwtString = getRefreshTokenOrBearerToken(request, bearerToken.getBearerTokenParameterName(), bearerToken.getBearerTokenHeaderName()); if (!hasText(jwtString)) { return null; } // 转换为 jwt try { oldJwt = jwtDecoder.decodeNotRefreshToken(removeBearerForJwtTokenString(jwtString)); } catch (JwtException | JwtInvalidException | JwtExpiredException e) { oldJwt = null; } catch (Exception e) { log.error(e.getMessage(), e); oldJwt = null; } // 返回 jwt return oldJwt; } /** * 根据 refreshToken 获取 UserId, 并校验 refreshToken 的有效性 * @param refreshTokenJwt refreshToken * @return 如果缓存中存在 refreshToken, 表示 refreshToken 有效, 返回 userId, 如果返回 null, 表示 refreshToken 无效. * @throws JwtInvalidException refreshToken 失效 */ @Nullable private static String getUserIdByRefreshToken(@NonNull Jwt refreshTokenJwt) throws JwtInvalidException { String userId = refreshTokenJwt.getClaimAsString(principalClaimName); try (RedisConnection connection = getConnection()) { // 不支持 jwt 黑名单逻辑 if (!blacklistProperties.getEnable()) { Boolean exists = connection.exists(getRefreshTokenKey(userId)); if (isNull(exists) || !exists) { return null; } return userId; } // 支持 jwt 黑名单逻辑 Boolean exists = connection.exists(getBlacklistKey(refreshTokenJwt.getId())); if (nonNull(exists) && exists) { // 在黑名单 return null; } return userId; } } /** * 生成 refresh token 且与 userId 一起保存到 redis 中 * @param userId 用户id * @return 返回 refresh token */ @NonNull private static Jwt generateRefreshToken(@NonNull String userId) { String refreshToken; Jwt jwt; // 创建 refreshToken jwt JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(); builder.claim(JwtClaimNames.JTI, jwtIdService.generateJtiId()); builder.claim(principalClaimName, userId); builder.claim(JwtClaimNames.EXP, Instant.now().plusSeconds(blacklistProperties.getRefreshTokenTtl().getSeconds()).toEpochMilli()); try { jwt = createJwt(builder.build()); refreshToken = jwt.getTokenValue(); } catch (JOSEException | ParseException e) { throw new JwtCreateException(ErrorCodeEnum.CREATE_JWT_ERROR, getMdcTraceId()); } // 如果需要缓存到 redis 则缓存 if (!saveRefreshToken(userId, refreshToken)) { throw new SaveRefreshTokenException(ErrorCodeEnum.SAVE_REFRESH_TOKEN_ERROR, getMdcTraceId()); } return jwt; } /** * 保存 refresh token , 如果返回 false, 视为 refresh token 重复, 需重新生成 refresh token, * 注意: 不支持 jwt 黑名单则缓存 refreshToken 到 redis, key 为 refreshTokenPrefix + userId. * @param userId 用户 id * @param refreshToken refresh token * @return 返回 true, 表示保存 refresh token 成功, 如果返回 false, 视为 refresh token 重复, 需重新生成 refresh token . */ @NonNull private static Boolean saveRefreshToken(@NonNull String userId, @NonNull String refreshToken) { try (RedisConnection connection = getConnection()) { // 不支持 jwt 黑名单则缓存 refreshToken 到 redis if (!blacklistProperties.getEnable() && REFRESH_TOKEN.equals(refreshHandlerPolicy)) { Boolean isSuccess = connection.set(getRefreshTokenKey(userId), refreshToken.getBytes(StandardCharsets.UTF_8), Expiration.from(blacklistProperties.getRefreshTokenTtl().minusSeconds(1L)), UPSERT); if (isNull(isSuccess)) { return false; } return isSuccess; } // 支持 jwt 黑名单直接返回 return true; } } // ====================== 通用辅助私有方法 ====================== /** * 去除 jwtTokenString 的 "bearer " 前缀, 如果没有 "bearer " 前缀则原样返回 * @param jwtTokenString jwtTokenString * @return 返回去除了 "bearer " 前缀的 jwtTokenString, 如果没有 "bearer " 前缀则原样返回 */ @NonNull private static String removeBearerForJwtTokenString(@NonNull String jwtTokenString) { if (jwtTokenString.startsWith(BEARER)) { return jwtTokenString.replaceFirst(BEARER, ""); } return jwtTokenString; } /** * 设置 bearerToken 与 refreshToken 到 Header
* 注意:
* 1. {@link BearerTokenProperties#getAllowFormEncodedBodyParameter()} = false 才会设置 bearerToken 与 refreshToken 到 Header.
* 2. refreshToken 只有在 {@link JwtRefreshHandlerPolicy#REFRESH_TOKEN} 时才回设置. * @param jwt {@link Jwt} * @param refreshTokenJwt refreshToken {@link Jwt}, 可以为 null 值. * @param isGenerateJwtByRefreshToken is generate jwt by refresh token */ private static void setBearerTokenAndRefreshTokenToHeader(@NonNull Jwt jwt, @Nullable Jwt refreshTokenJwt, @NonNull Boolean isGenerateJwtByRefreshToken) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); HttpServletResponse response = requestAttributes.getResponse(); // 获取 bearerToken String bearerToken = BEARER + jwt.getTokenValue(); if (isNull(response) || isNull(JwtContext.bearerToken)) { throw new IllegalStateException("HttpServletResponse is closed or does not support setting bearer token to header"); } if (nonNull(refreshTokenJwt)) { // 设置 refreshToken 到 header if (REFRESH_TOKEN.equals(JwtContext.refreshHandlerPolicy)) { // 缓存进 redis String refreshTokenHeaderName = JwtContext.bearerToken.getRefreshTokenHeaderName(); String refreshTokenJwtValue = refreshTokenJwt.getTokenValue(); if (!JwtContext.bearerToken.getAllowFormEncodedBodyParameter()) { // 设置到请求头 response.setHeader(refreshTokenHeaderName, refreshTokenJwtValue); } if (isGenerateJwtByRefreshToken) { // 临时设置到 session, 再通过认证成功处理器获取 refresh token 通过 json 返回 requestAttributes.setAttribute(TEMPORARY_JWT_REFRESH_TOKEN, refreshTokenJwtValue, SCOPE_SESSION); } } } // 当 jwt 刷新策略为 JwtRefreshHandlerPolicy#AUTO_RENEW 时, 刷新的 jwt 直接设置到 header 中, 前端可以从相应的 header 中获取. boolean isAutoRenewPolicy = AUTO_RENEW.equals(JwtContext.refreshHandlerPolicy); if (!JwtContext.bearerToken.getAllowFormEncodedBodyParameter() || isAutoRenewPolicy) { String bearerTokenHeaderName = JwtContext.bearerToken.getBearerTokenHeaderName(); response.setHeader(bearerTokenHeaderName, bearerToken); } } /** * 是否支持创建 jwt * @param authentication {@link Authentication} * @return 支持 jwt 则返回 true, 否则返回 false */ @NonNull private static Boolean isSupportCreateJwt(@NonNull Authentication authentication) { // 是否支持 jwt return nonNull(signer) && !(authentication instanceof AbstractOAuth2TokenAuthenticationToken); } // ====================== 创建 jwt 或 Authentication 辅助私有方法 ====================== /** * 转换为 {@link JwtAuthenticationToken}, 再根据 isSetToContext 保存 jwtAuthenticationToken 到 SecurityContext, * 如果 {@link JwtBlacklistProperties#getEnable()} = false, 则同时保存到 redis 缓存 * @param jwt {@link Jwt} * @param converter {@link JwtAuthenticationConverter} * @param isSetToContext authentication 是否设置到 SecurityContext * @return 则返回 {@link JwtAuthenticationToken} */ @NonNull private static JwtAuthenticationToken toJwtAuthenticationToken(@NonNull Jwt jwt, @NonNull JwtAuthenticationConverter converter, @NonNull Boolean isSetToContext) { JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) converter.convert(jwt); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (nonNull(authentication)) { jwtAuthenticationToken.setDetails(authentication.getDetails()); } // 如果不支持 jwt 黑名单, 添加到 redis 缓存 if (!blacklistProperties.getEnable()) { saveTokenSessionToRedis(jwtAuthenticationToken, jwt, isSetToContext); } else if (isSetToContext) { SecurityContextHolder.getContext().setAuthentication(jwtAuthenticationToken); } return jwtAuthenticationToken; } /** * map 转换为 {@link JWTClaimsSet} * @param claims claims map * @return 返回 {@link JWTClaimsSet} */ @NonNull private static JWTClaimsSet toJwtClaimsSet(@NonNull Map claims) { final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(); claims.forEach(builder::claim); return builder.build(); } /** * 生成 {@link Jwt} * @param claimsSet JWT Payload(负载), 注意: {@link JWTClaimsSet} 中"日期"都以"时间戳"表示且"时间戳"以秒为单位 * @return 返回 {@link Jwt} * @throws JOSEException 转换异常 */ @NonNull private static Jwt createJwt(@NonNull JWTClaimsSet claimsSet) throws JOSEException, ParseException { requireNonNull(signer, "JWSSigner 不存在, 不支持创建 JWT 功能"); JWSHeader jwsHeader = getJwsHeader(); return createJwt(jwsHeader, claimsSet); } /** * 生成 {@link Jwt} * @param jwsHeader {@link JWSHeader} * @param claimsSet JWT Payload(负载), 注意: {@link JWTClaimsSet} 中"日期"都以"时间戳"表示且"时间戳"以秒为单位 * @return 返回 {@link Jwt} * @throws JOSEException 转换异常 */ @NonNull private static Jwt createJwt(@NonNull JWSHeader jwsHeader, @NonNull JWTClaimsSet claimsSet) throws JOSEException, ParseException { requireNonNull(signer, "signer 不存在, 不支持 JWT 功能"); SignedJWT signedjwt = new SignedJWT(jwsHeader, claimsSet); // Compute the RSA signature signedjwt.sign(signer); // To serialize to compact form, produces something like // eyJhbGciOiJSUzI1NiJ9.SW4gUlNBIHdlIHRydXN0IQ.IRMQENi4nJyp4er2L // mZq3ivwoAjqa1uUkSBKFIX7ATndFF5ivnt-m8uApHO4kfIFOrW7w2Ezmlg3Qd // maXlS9DhN0nUk_hGI3amEjkKd0BWYCB8vfUbUv0XGjQip78AI4z1PrFRNidm7 // -jPDm5Iq0SZnjKjCNS5Q15fokXZc8u0A String tokenValue = signedjwt.serialize(); Long issueTime = claimsSet.getLongClaim(JwtClaimNames.IAT); Instant issueAt = null; if (nonNull(issueTime)) { issueAt = Instant.ofEpochSecond(issueTime); } try { return new Jwt(tokenValue, issueAt, Instant.ofEpochSecond(claimsSet.getLongClaim(JwtClaimNames.EXP)), invokeToJsonObjectMethod(jwsHeader, TO_JSON_OBJECT_METHOD), claimsSet.getClaims()); } catch (IllegalAccessException | InvocationTargetException e) { throw new JOSEException(e.getMessage()); } } /** * 生成 JwsHeader * @return 返回 {@link JWSHeader} */ @NonNull private static JWSHeader getJwsHeader() { requireNonNull(jwsAlgorithm, "未设置 jwsAlgorithm, 不支持 JWT 功能"); JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(jwsAlgorithm)); builder.type(JOSEObjectType.JWT); if (hasText(kid)) { builder.keyID(kid); } return builder.build(); } // ====================== 内部类 ====================== /** * 黑名单类型 * @author YongWu zheng * @version V2.0 Created by 2020.12.17 23:25 */ public enum BlacklistType { /** * 不在黑名单 */ NOT_IN_BLACKLIST { @Override @Nullable public String getOneTimeNewJwtValue() { return null; } @Override @NonNull public Boolean isInBlacklist() { return false; } }, /** * 在黑名单中 */ IN_BLACKLIST { @Override @Nullable public String getOneTimeNewJwtValue() { return null; } @Override @NonNull public Boolean isInBlacklist() { return true; } }, /** * 在黑名单中, 但缓存中存储有新的且有效 JWT */ IN_BLACKLIST_AND_HAS_NEW_JWT { @Override @Nullable public String getOneTimeNewJwtValue() { try { return JWT_VALUE.get(); } finally { JWT_VALUE.remove(); } } @Override @NonNull public Boolean isInBlacklist() { return true; } }; private static final ThreadLocal JWT_VALUE = new ThreadLocal<>(); /** * 通过 blacklistValue 来获取 {@link BlacklistType} * @param blacklistValue 存储在 jwt 缓存黑名单中的值 * @return 返回 {@link BlacklistType} */ @NonNull public static BlacklistType getBlacklistType(@Nullable String blacklistValue) { if (!hasText(blacklistValue)) { return NOT_IN_BLACKLIST; } if (IN_BLACKLIST.name().equals(blacklistValue)) { return IN_BLACKLIST; } JWT_VALUE.set(blacklistValue); return IN_BLACKLIST_AND_HAS_NEW_JWT; } /** * 获取一次性的新的 jwt 字符串.
* 调用一次 {@link #getBlacklistType(String)} 方法后, 只能在第一次调用此方法时才能获取到有效返回值, 第二次(n次)调用返回 null 值.
* 示例:
* 1. BlacklistType 为 IN_BLACKLIST_AND_HAS_NEW_JWT 的情况 *
         *     // type = IN_BLACKLIST_AND_HAS_NEW_JWT
         *     BlacklistType type = getBlacklistType("xxxxx.xxxxxx.xxxxxx");
         *     // token = "xxxxx.xxxxxx.xxxxxx"
         *     String token = type.getOneTimeNewJwtValue();
         *     // token = null
         *     token = type.getOneTimeNewJwtValue();
         *
         *     // type = IN_BLACKLIST_AND_HAS_NEW_JWT
         *     type = getBlacklistType("xxxxx.xxxxxx.xxxxxx2");
         *     // token = "xxxxx.xxxxxx.xxxxxx2"
         *     token = type.getOneTimeNewJwtValue();
         *     // token = null
         *     token = type.getOneTimeNewJwtValue();
         * 
* 2. BlacklistType 为 IN_BLACKLIST 或 NOT_IN_BLACKLIST 的情况 *
         *     // type = NOT_IN_BLACKLIST
         *     BlacklistType type = getBlacklistType(null);
         *     // newJwtToken = null
         *     String newJwtToken = type.getOneTimeNewJwtValue();
         *     // newJwtToken = null
         *     newJwtToken = type.getOneTimeNewJwtValue();
         *
         *     // type = IN_BLACKLIST
         *     type = getBlacklistType("IN_BLACKLIST");
         *     // newJwtToken = null
         *     newJwtToken = type.getOneTimeNewJwtValue();
         *     // newJwtToken = null
         *     newJwtToken = type.getOneTimeNewJwtValue();
         * 
* @return 返回新的 jwt token 或 null 值 */ @Nullable public abstract String getOneTimeNewJwtValue(); /** * 是否在黑名单中 * @return 返回 true 表示在黑名单中. */ @NonNull public abstract Boolean isInBlacklist(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy