什么是JWT
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。(更多信息建议去官网了解)
使用JWT替换传统Token有很多好处,比如:
- 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
- 不需要在服务端保存会话信息,特别适用于分布式微服务。
先了解一下使用到的两个组件的作用:
JwtAccessTokenConverter:TokenEnhancer的子类,帮助程序在JWT编码的令牌值和OAuth身份验证信息之间进行转换(在两个方向上),同时充当TokenEnhancer授予令牌的时间。
自定义的JwtAccessTokenConverter(把自己设置的jwt签名加入accessTokenConverter中)
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey());
return accessTokenConverter;
}
TokenEnhancer:在AuthorizationServerTokenServices 实现存储访问令牌之前增强访问令牌的策略。
下面是自定义TokenEnhancer的代码(把附加信息加入oAuth2AccessToken中):
public class TuckerJwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String ,Object> info = new HashMap<>();
info.put("admin","tucker");
info.put("company","bobo");
((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(info);
//System.out.println(oAuth2AccessToken.getAdditionalInformation());
return oAuth2AccessToken;
}
}
下面开始增强JWT令牌
Springsecurity默认生成Token的方法是DfaultTokenServices的createAccessToken方法:
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = this.getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (long)validitySeconds * 1000L));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return (OAuth2AccessToken)(this.accessTokenEnhancer != null ? this.accessTokenEnhancer.enhance(token, authentication) : token);
}
该方法做了五件事:
- 使用UUID生成Token
- 判断Token是否过期,如果没过期,就把过期时间设为当前时间加1000s
- 设置刷新令牌
- 设置权限
- 判断是否有增强器,如果有就调用它的enhance方法
对令牌的增强操作就在enhance方法中
下面在配置类中,将TokenEnhancer和JwtAccessConverter加到一个enhancerChain中
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
if (jwtAccessTokenConverter != null && jwtTokenEnhancer !=null){
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancers = new ArrayList<>();
enhancers.add(jwtTokenEnhancer);
enhancers.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancers);//将自定义Enhancer加入EnhancerChain的delegates数组中
endpoints.tokenEnhancer(enhancerChain)//为什么不直接把jwtTokenEnhancer加在这个位置呢?
.accessTokenConverter(jwtAccessTokenConverter);
}
}
下面开始分析上面的问题,下面是TokenEnhancerChain 的源码:
public class TokenEnhancerChain implements TokenEnhancer {
private List<TokenEnhancer> delegates = Collections.emptyList();
public TokenEnhancerChain() {
}
public void setTokenEnhancers(List<TokenEnhancer> delegates) {
this.delegates = delegates;
}
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
OAuth2AccessToken result = accessToken;
TokenEnhancer enhancer;
for(Iterator var4 = this.delegates.iterator(); var4.hasNext(); result = enhancer.enhance(result, authentication)) {
enhancer = (TokenEnhancer)var4.next();
}
return result;
}
}
它也是一个TokenEhancer的子类,Springsecurity把执行enhancer方法的任务委派给这个TokenEhancerChain,它的任务就是执行我们配置的enhancer。
下面是TokenEhancerChain的delegates数组中的元素,也就是我们自定义的TukcerTokenEnhancer和Springceurity中的JwtAccessTokenConverter,signingKey在我们自定JwtAccessTokenConverter这个Bena初始化时已经设置进去了
下面是JwtAccessTokenConverter中的enhancer方法中:
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
// info数组用来存储附加信息,也就是我们自定义的TokenEnhancer中的附附加信息,
Map<String, Object> info = new LinkedHashMap(accessToken.getAdditionalInformation());
String tokenId = result.getValue();
if (!info.containsKey("jti")) {
info.put("jti", tokenId);
} else {
tokenId = (String)info.get("jti");
}
result.setAdditionalInformation(info);
result.setValue(this.encode(result, authentication));
OAuth2RefreshToken refreshToken = result.getRefreshToken();
if (refreshToken != null) {
DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
encodedRefreshToken.setValue(refreshToken.getValue());
encodedRefreshToken.setExpiration((Date)null);
try {
Map<String, Object> claims = this.objectMapper.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
if (claims.containsKey("jti")) {
encodedRefreshToken.setValue(claims.get("jti").toString());
}
} catch (IllegalArgumentException var11) {
}
Map<String, Object> refreshTokenInfo = new LinkedHashMap(accessToken.getAdditionalInformation());
refreshTokenInfo.put("jti", encodedRefreshToken.getValue());
refreshTokenInfo.put("ati", tokenId);
encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(this.encode(encodedRefreshToken, authentication));
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
Date expiration = ((ExpiringOAuth2RefreshToken)refreshToken).getExpiration();
encodedRefreshToken.setExpiration(expiration);
token = new DefaultExpiringOAuth2RefreshToken(this.encode(encodedRefreshToken, authentication), expiration);
}
result.setRefreshToken((OAuth2RefreshToken)token);
}
return result;
}
上面说这个Enhancer的作用是:帮助程序在JWT编码的令牌值和OAuth身份验证信息之间进行转换(在两个方向上),同时充当TokenEnhancer授予令牌的时间。通俗点讲它做了两件事:
- 给JWT令牌中设置附加信息和jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
- 判断请求中是否有refreshToken,如果有,就重新设置refreshToken并加入附加信息
我们自定义的TokenEnhancer接收调用DefaultOAuth2AccessToken的setAdditionalInformation(info)方法时,建立一个新的LinkedHashMap覆盖当前存有附加信息的Map
public void setAdditionalInformation(Map<String, Object> additionalInformation) {
this.additionalInformation = new LinkedHashMap(additionalInformation);
}