0%

spring-gateway增加Token认证

JWT(Json Web Token)介绍

JWT是目前最流行的跨域身份验证解决方案,服务器直接对token可用性校验并取出保存的用户信息,单点登录的实现更便捷.

JWT 使用

MAVEN 添加如下依赖:

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

Token中可以保存很多信息,此处保存用户id以及生成token时间,其中token生成时间可用于

  • 修改密码后原token失效,必须重新登录
  • 其他平台/设备登录后token失效

具体代码代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
@Component
@PropertySource("classpath:jwt.properties")
public class JwtTokenManager implements Serializable {

private static final long serialVersionUID = -3301605591108950415L;

private static final String CLAIM_KEY_USER_ID = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;

public String getUserIdFromToken(String token) {
String userId;
try {
Claims claims = getClaimsFromToken(token,secret);
userId = claims.getSubject();
} catch (Exception e) {
userId = null;
}
return userId;
}

public Date getCreatedDateFromToken(String token) {
Date created;
try {
Claims claims = getClaimsFromToken(token,secret);
created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
} catch (Exception e) {
created = null;
}
return created;
}

public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
Claims claims = getClaimsFromToken(token,secret);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}

private Claims getClaimsFromToken(String token,String secret) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}

private Date generateExpirationDate(Long expiration) {
return new Date(System.currentTimeMillis() + expiration * 1000);
}

private Boolean isTokenExpired(String token ) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}

private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
private Boolean isCreatedBeforeLastLogin(Date created, Date lastLogin) {
return (lastLogin != null && created.before(lastLogin));
}
//生成Token
public String generateToken(String id) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USER_ID, id);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims,secret,expiration);
}

private String generateToken(Map<String, Object> claims ,String secret ,Long expiration) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate(expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}

public Boolean canTokenBeRefreshed(String token , Date lastPasswordReset) {
Date created = getCreatedDateFromToken(token);
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&& !isTokenExpired(token);
}

public String refreshToken(String token ) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token , secret);
claims.put(CLAIM_KEY_CREATED, new Date());
refreshedToken = generateToken(claims ,secret ,expiration);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
//校验token生成时间
public Boolean validateToken(String token,String userId,Date lastLoginDate ,Date lastPasswordRestDate) {
String id = getUserIdFromToken(token);
Date created = getCreatedDateFromToken(token);
return (
id.equals(userId)
&& !isTokenExpired(token)
&& !isCreatedBeforeLastPasswordReset(created, lastPasswordRestDate)
&& !isCreatedBeforeLastLogin(created, lastLoginDate));
}
}

Spring-gateway 增加security

MAVEN增加依赖:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>

增加securityConfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@EnableWebFluxSecurity
@Slf4j
public class SecurityConfig {

@Autowired
private CustomServerSecurityContextRepository customServerSecurityContextRepository;
@Autowired
private CustomHttpBasicServerAuthenticationEntryPoint customHttpBasicServerAuthenticationEntryPoint;

//security的鉴权排除的url列表
private static final String[] excludedAuthPages = {
GlobalConstant.Route.LOGIN_PATH,
"/**/*.html",
"/**/*.js",
"/**/*.css",
"/**/*.jpg",
"/**/*.jpeg",
"/**/*.png",
"/**/*.gif",
"/**/*.text",
"/**/*.txt",
"/**/*.woff2"
};



@Bean
SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception {
http
.authorizeExchange()
.pathMatchers(excludedAuthPages).permitAll() //无需进行权限过滤的请求路径
.anyExchange().authenticated()
.and()
.httpBasic()
.and()
.securityContextRepository(customServerSecurityContextRepository)
.exceptionHandling().authenticationEntryPoint(customHttpBasicServerAuthenticationEntryPoint) //基于http的接口请求鉴权失败
.and()
.csrf().disable()
.logout().disable();
return http.build();
}

}

Spring-gateway 增加过滤支持

网关统一处理并认证请求,对于非认证相关请求必须要携带合法的token,对于认证成功的请求响应正确的token(可由认证服务器直接返回认证成功的token)

Token校验过滤器

获取Header中的token信息,并校验合法性以及可用性,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Component
@Slf4j
public class CustomServerSecurityContextRepository implements ServerSecurityContextRepository {

@Autowired
private JwtTokenManager jwtTokenManager;
@Autowired
private JwtTokenAuthenticationService jwtTokenAuthenticationService;

@Override
public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
return Mono.empty();
}

@Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
String token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
UsernamePasswordAuthenticationToken authentication = null;
String userIdFromToken = null;
try {
if (Strings.isNotBlank(token)) {
userIdFromToken = jwtTokenManager.getUserIdFromToken(token);
if (Strings.isNotBlank(userIdFromToken)) {
UserDetails userDetails = jwtTokenAuthenticationService.loadUserByUserId(userIdFromToken);
authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(userDetails);
}
}
} catch (Exception e) {
log.error(String.format("CustomServerSecurityContextRepository 认证异常,token{%s}", token), e);
}
if (authentication != null) {
return Mono.just(new SecurityContextImpl(authentication));
} else {
return Mono.empty();
}
}
}

认证响应过滤器

在我的实现中,由网关过滤认证成功的请求并覆写响应的token信息,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Component
@Slf4j
public class AuthResponseFilter implements GlobalFilter, Ordered {
@Autowired
private JwtTokenManager jwtTokenManager;


@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
// probably should reuse buffers
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
HttpResponseWrapper httpResponseWrapper = JSON.parseObject(content, HttpResponseWrapper.class);
if(httpResponseWrapper != null && httpResponseWrapper.isSuccess()){
try {
LoginRepDto loginRepDto = JSON.parseObject(JSON.toJSONString(httpResponseWrapper.getResult()),LoginRepDto.class);
if(Strings.isNotBlank(loginRepDto.getId())){
String token = jwtTokenManager.generateToken(loginRepDto.getId());
Boolean registered = loginRepDto.getRegistered();
JSONObject jsonObject = new JSONObject();
jsonObject.put("token",token);
jsonObject.put("registered",registered);
HttpResponseWrapper success = HttpResponseWrapper.success(jsonObject);
//释放掉内存
DataBufferUtils.release(dataBuffer);
byte[] uppedContent = JSON.toJSONBytes(success);
return bufferFactory.wrap(uppedContent);
}
} catch (Exception e) {
log.error("处理认证请求的响应出错",e);
}
}
return bufferFactory.wrap(content);
}));
}
// if body is not a flux. never got there.
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}

@Override
public int getOrder() {
// -1 is response write filter, must be called before that
return -2;
}

}

自定义未授权401的返回信息

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class CustomHttpBasicServerAuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint{

@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8.toString());
byte[] dataBytes = JSON.toJSONBytes(HttpResponseWrapper.error(HttpCodeEnum.GL00000401));
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes);
return response.writeWith(Mono.just(bodyDataBuffer));
}
}

多模块下对Token解密失败的BUG

详见 Token(JJWT-0.8.0)不同应用(服务)下的解密BUG

您的支持是对我最大的动力 (●'◡'●)