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)); } 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; } 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;
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) .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 -> { 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); })); } return super.writeWith(body); } }; return chain.filter(exchange.mutate().response(decoratedResponse).build()); }
@Override public int getOrder() { 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