diff --git a/youlai-common/common-core/src/main/java/com/youlai/common/constant/SecurityConstants.java b/youlai-common/common-core/src/main/java/com/youlai/common/constant/SecurityConstants.java index a1620a726..616619e0b 100644 --- a/youlai-common/common-core/src/main/java/com/youlai/common/constant/SecurityConstants.java +++ b/youlai-common/common-core/src/main/java/com/youlai/common/constant/SecurityConstants.java @@ -31,6 +31,11 @@ public interface SecurityConstants { */ String JWT_JTI = "jti"; + /** + * JWT refresh_token 标识 + */ + String JWT_ATI = "ati"; + /** * JWT ID 唯一标识 */ diff --git a/youlai-gateway/src/main/java/com/youlai/gateway/filter/GatewaySecurityFilter.java b/youlai-gateway/src/main/java/com/youlai/gateway/filter/GatewaySecurityFilter.java index 7eed47ae7..b9ff13e0c 100644 --- a/youlai-gateway/src/main/java/com/youlai/gateway/filter/GatewaySecurityFilter.java +++ b/youlai-gateway/src/main/java/com/youlai/gateway/filter/GatewaySecurityFilter.java @@ -1,8 +1,6 @@ package com.youlai.gateway.filter; import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; import com.nimbusds.jose.JWSObject; import com.youlai.common.constant.SecurityConstants; import com.youlai.common.result.ResultCode; @@ -15,7 +13,6 @@ import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; @@ -37,8 +34,6 @@ @RequiredArgsConstructor public class GatewaySecurityFilter implements GlobalFilter, Ordered { - private final RedisTemplate redisTemplate; - @Value("${spring.profiles.active}") private String env; @@ -79,12 +74,12 @@ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 解析JWT获取jti,以jti为key判断redis的黑名单列表是否存在,存在则拦截访问 token = StrUtil.replaceIgnoreCase(token, SecurityConstants.JWT_PREFIX, Strings.EMPTY); String payload = StrUtil.toString(JWSObject.parse(token).getPayload()); - JSONObject jsonObject = JSONUtil.parseObj(payload); + /*JSONObject jsonObject = JSONUtil.parseObj(payload); String jti = jsonObject.getStr(SecurityConstants.JWT_JTI); Boolean isBlack = redisTemplate.hasKey(SecurityConstants.TOKEN_BLACKLIST_PREFIX + jti); if (isBlack) { return ResponseUtils.writeErrorInfo(response, ResultCode.TOKEN_ACCESS_FORBIDDEN); - } + }*/ // 存在token且不在黑名单中,request写入JWT的载体信息传递给微服务 request = exchange.getRequest().mutate() diff --git a/youlai-gateway/src/main/java/com/youlai/gateway/security/ResourceServerConfig.java b/youlai-gateway/src/main/java/com/youlai/gateway/security/ResourceServerConfig.java index f9f2342a5..31c402b74 100644 --- a/youlai-gateway/src/main/java/com/youlai/gateway/security/ResourceServerConfig.java +++ b/youlai-gateway/src/main/java/com/youlai/gateway/security/ResourceServerConfig.java @@ -3,6 +3,8 @@ import cn.hutool.core.codec.Base64; import cn.hutool.core.convert.Convert; import cn.hutool.core.io.IoUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.nimbusds.jwt.proc.BadJWTException; import com.youlai.common.constant.SecurityConstants; import com.youlai.common.result.ResultCode; import com.youlai.gateway.util.ResponseUtils; @@ -16,10 +18,13 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; @@ -63,8 +68,9 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { .oauth2ResourceServer() .jwt() .jwtAuthenticationConverter(jwtAuthenticationConverter()) - .publicKey(rsaPublicKey()) // 本地加载公钥 - //.jwkSetUri() // 远程获取公钥,默认读取的key是spring.security.oauth2.resourceserver.jwt.jwk-set-uri +// .publicKey(rsaPublicKey()) // 本地加载公钥 + .jwtDecoder(jwtDecoder()) + //.jwkSetUri() // 远程获取公钥,默认读取的key是spring.security.oauth2.resourceserver.jwt.jwk-set-uri ; http.oauth2ResourceServer().authenticationEntryPoint(authenticationEntryPoint()); http.authorizeExchange() @@ -79,6 +85,45 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http.build(); } + /** + * 1. 添加 jwt 类型(access_token/refresh_token) 校验 + * 2. 将 com.youlai.gateway.filter.GatewaySecurityFilter#filter(org.springframework.web.server.ServerWebExchange, org.springframework.cloud.gateway.filter.GatewayFilterChain) + * 黑名单部分校验提取到此处 + * + * @return + */ + @Bean + public ReactiveJwtDecoder jwtDecoder() { + NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder + .withPublicKey(rsaPublicKey()) // 本地公钥获取 + /** + * 方式1 使用 预留自定义校验方式对jwt进行校验 + * 此种校验方式 会将抛出的异常信息进行包装, + * 无法直接获取到异常内容,但是 无论是非法的 access_token或者 是token 在黑名单内, + * 以上两种情况 系统都认为该token非法 ,对于程序没有影响 + */ + .jwtProcessorCustomizer(jwtProcessor -> jwtProcessor.setJWTClaimsSetVerifier(((claimsSet, context) -> { + // 包含 ATI 说明此 token是 refresh token, 该类型token应仅作为获取刷新token时使用 + if (claimsSet.getClaims().containsKey(SecurityConstants.JWT_ATI)) { + throw new BadJWTException("非法的AccessToken");// 此处异常信息不是很重要 还需要把变量抽取出来吗? + } + // 调用过注销接口 黑名单校验 + if (SpringUtil.getBean("redisTemplate", RedisTemplate.class).hasKey(SecurityConstants.TOKEN_BLACKLIST_PREFIX + claimsSet.getJWTID())) { + throw new BadJWTException("token已被禁止访问"); + } + }))) +// .withJwkSetUri() //远程获取公钥, + .build(); + + // 方式2. 添加jwt类型以及 黑名单校验 + /*jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>( + Arrays.asList( + new JwtTimestampValidator(), + new JwtValidator(SpringUtil.getBean("redisTemplate", RedisTemplate.class))) + ));*/ + return jwtDecoder; + } + /** * 自定义未授权响应 */ diff --git a/youlai-gateway/src/main/java/com/youlai/gateway/security/jwt/JwtValidator.java b/youlai-gateway/src/main/java/com/youlai/gateway/security/jwt/JwtValidator.java new file mode 100644 index 000000000..6f50f95c2 --- /dev/null +++ b/youlai-gateway/src/main/java/com/youlai/gateway/security/jwt/JwtValidator.java @@ -0,0 +1,47 @@ +package com.youlai.gateway.security.jwt; + +import com.youlai.common.constant.SecurityConstants; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; + +/** + * @author yeziz2Z + */ +public class JwtValidator implements OAuth2TokenValidator { + + private RedisTemplate redisTemplate; + + public JwtValidator(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + /** + * 因为校验不多 并没有将该校验 写在两个类中 + * 参考 系统默认的两个 validator:JwtTimestampValidator,JwtIssuerValidator + * @param token + * @return + */ + @Override + public OAuth2TokenValidatorResult validate(Jwt token) { + // 包含 ATI 说明此 token是 refresh token, 该类型token应仅作为获取刷新token时使用 + if (token.getClaims().containsKey(SecurityConstants.JWT_ATI)) { + OAuth2Error oAuth2Error = createOAuth2Error("Encoded token is not a access token"); + return OAuth2TokenValidatorResult.failure(oAuth2Error); + } + // 调用过注销接口 黑名单校验 + if (redisTemplate.hasKey(SecurityConstants.TOKEN_BLACKLIST_PREFIX + token.getId())) { + OAuth2Error oAuth2Error = createOAuth2Error("token已被禁止访问"); + return OAuth2TokenValidatorResult.failure(oAuth2Error); + } + return OAuth2TokenValidatorResult.success(); + } + + private OAuth2Error createOAuth2Error(String reason) { + return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, reason, + null); + } +}