Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:添加 jwt refresh_token校验 #83

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public interface SecurityConstants {
*/
String JWT_JTI = "jti";

/**
* JWT refresh_token 标识
*/
String JWT_ATI = "ati";

/**
* JWT ID 唯一标识
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -37,8 +34,6 @@
@RequiredArgsConstructor
public class GatewaySecurityFilter implements GlobalFilter, Ordered {

private final RedisTemplate redisTemplate;

@Value("${spring.profiles.active}")
private String env;

Expand Down Expand Up @@ -79,12 +74,12 @@ public Mono<Void> 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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()
Expand All @@ -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;
}

/**
* 自定义未授权响应
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Jwt> {

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);
}
}