在《SpringSecurity之OAuth2 令牌accessToken的生成过程_clonetx的博客-CSDN博客》中我们分析了access_token生成整体流程的源码,最终提到了accessTokenEnhancer.enhance(token, authentication)方法。
而说到转换token,最普遍的就是转成 jwt 了。
1 jwt
jwt共分为三部分组成,header、payload、signature,即头、体、签名;其结构如下,使用英文句号连接:
Base64(header).Base64(payload).HMACSHA256(Base64(header)+"."+Base64(payload),secret)
下面分别看下每部分的内容。
header:{
"alg":"HS256",//签名算法
"typ":"JWT" //类型
}
payload: {
"iss": "jwt签发者",
"sub": "jwt的主体,如用户标识",
"aud": "jwt的接收者,即resource_ids",
"exp": "jwt的过期时间",//不能为空
"nbf": "定义在指定时间之前,该jwt不可用",
"iat": "jwt签发时间",
"jti": "jwt的唯一身份标识,用来作为一次性token时可以防止重放攻击",
"自定义key1":"自定义value1",
"自定义key2":"自定义value2"
}
这里注意一下,jwt中,仅仅对 payload 进行了一次 base64,并非什么加密手段,因此,千万不要自定义敏感信息进去,比如用户的身份证号,密码,手机号等信息的明文数据。
signature:HMACSHA256(Base64(header)+"."+Base64(payload),secret)
签名的作用就是为了防止窜改 header 和 payload ,验证 jwt 的时候会对signature进行解密,根据上面的加密公式看,显然解密得到的就是 header 和 payload 的 base64 串,只要与签发时的header 和 payload 一对比就可以知道 jwt是否被修改过。
加密方式可以使用对称加密也可以使用非对称加密,这是因为,加解密都是在服务端进行的,密钥不泄露就不用担心被伪造。个人推荐非对称方式,即私钥签名,公钥验证签名,这样更安全,也可以允许第三方验证我们的 jwt。
//客户端授权模式产生的 jwt 示例
{
"access_token": "eyJhbGciOiJIUzI1NiIsIn5cCI6IkpXVCJ9.eyJzY29wZSI6Wy0ZXN0Il0sImV4cCI6MTY1MzUzNDgyNCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9DTElFTlQiXSwianRpIjoiYjFjNjRmN2YtMmMxYS00YjljLTlmMTgtNGYxN2Q0YmY2OTE2IiwiY2xpZW50X2lkIjoidGVzdEFwcElkIn0.F2ZZq4rg9BDw2oCU_qDs9fulSKbhD53xnmTmYH57L6U",
"token_type": "bearer",
"expires_in": 1473,
"scope": "test",
"jti": "b1c64f7f-2c1a-4b9c-9f18-4f17d4bf6916"
}
2 TokenEnhancer
在上一篇源码分析中,我们提到TokenEnhancer的对象是在 AuthorizationServerEndpointsConfigurer 类中设置的。
private DefaultTokenServices createDefaultTokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(reuseRefreshToken);
tokenServices.setClientDetailsService(clientDetailsService());
tokenServices.setTokenEnhancer(tokenEnhancer());//token增强器
addUserDetailsService(tokenServices, this.userDetailsService);
return tokenServices;
}
private TokenEnhancer tokenEnhancer() {
//如果授权服务器的配置中endpoints没有指定 tokenEnhancer,
//并且设置了JwtAccessTokenConverter类型的token转换器,就返回转换器
if (this.tokenEnhancer == null && accessTokenConverter() instanceof JwtAccessTokenConverter) {
tokenEnhancer = (TokenEnhancer) accessTokenConverter;
}
//N 如果endpoints 指定了tokenEnhancer,就返回指定的,或者token转换器类型不是jwt的就只能返回null了
return this.tokenEnhancer;
}
private AccessTokenConverter accessTokenConverter() {
if (this.accessTokenConverter == null) {
accessTokenConverter = new DefaultAccessTokenConverter();//非JwtAccessTokenConverter类型
}
return this.accessTokenConverter;
}
结合授权服务器的配置,如果我们既没有指定 tokenEnhancer(),也没有指定accessTokenConverter()配置,那么tokenEnhancer() 的返回值就一定是null了。下例我们设置一个 JwtAccessTokenConverter 的对象。
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
......
//.tokenEnhancer(null)
.accessTokenConverter(new JwtAccessTokenConverter())
;
}
其实,我们能动手脚的也就是endpoints的这两个配置了。
3 JwtAccessTokenConverter
在 DefaultTokenServices 类里面创建accessToken的最终实现中,我们看到有一个 accessTokenEnhancer.enhance(token, authentication)方法,在配置了accessTokenConverter()后,其 enhance() 方法的具体实现代码如下:
这段代码逻辑分别处理了access_token和refresh_token,逻辑比较简单,主要工作就是在原本的token中加了一个jti,也就是原本的uuid不再作为access_token的值,而变成了 jti 的值,然后就是从 authentication 对象中取了一些值构造出一个content,即payload,并使用JwtHelper工具类基于content生成了一个jwt作为最终的access_token。
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
//info 中只放了一个jti
Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
String tokenId = result.getValue();
if (!info.containsKey(TOKEN_ID)) {
//TOKEN_ID 就是 jti 也就是原始token的值,即uuid
info.put(TOKEN_ID, tokenId);
} else {
tokenId = (String) info.get(TOKEN_ID);
}
result.setAdditionalInformation(info);
//N 这里的value就是我们最终看到的access_token
result.setValue(encode(result, authentication));
OAuth2RefreshToken refreshToken = result.getRefreshToken();
if (refreshToken != null) {
//N 开始处理refresh_token
DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
encodedRefreshToken.setValue(refreshToken.getValue());
// Refresh tokens do not expire unless explicitly of the right type
encodedRefreshToken.setExpiration(null);//默认refreshToken 不过期
try {
//claims 就是refreshToken中的 payload
Map<String, Object> claims = objectMapper
.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
if (claims.containsKey(TOKEN_ID)) {
//这里只保留了 refreshToken 的 uuid
encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
}
} catch (IllegalArgumentException e) {
}
Map<String, Object> refreshTokenInfo = new LinkedHashMap<String, Object>(
accessToken.getAdditionalInformation());
refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
//refreshToken 的info 同时存储了jti 和 ati
refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
encode(encodedRefreshToken, authentication));
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
// refreshToken 的类型是在创建时,根据数据库表中是否设置了过期时间决定的
Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
encodedRefreshToken.setExpiration(expiration);
token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
}
result.setRefreshToken(token);
}
return result;
}
encode()方法的作用就是基于payload生成 jwt,而 payload 的生成则是由 tokenConverter 完成的,在创建JwtAccessTokenConverter 对象时,tokenConverter 默认是 DefaultAccessTokenConverter类型,当然我们也可以通过set方法改变 tokenConverter 为自定义的。
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String content;
try {
//N tokenConverter 默认是 DefaultAccessTokenConverter
content = objectMapper.formatMap(tokenConverter.convertAccessToken(accessToken, authentication));
} catch (Exception e) {
throw new IllegalStateException("Cannot convert access token to JSON", e);
}
//N content 就是 payload
String token = JwtHelper.encode(content, signer).getEncoded();
return token;
}
4 DefaultAccessTokenConverter
下面代码我们通过Map<String, Object> response中put的值就可以知道最终生成的 jwt 中 payload 会有哪些内容了,想要拓展 payload,只要 set 一个自定义的 tokenConverter 取代默认的 DefaultAccessTokenConverter就可以。
/**
* 这里就是在构造 payload 了,如果我们配置JwtAccessTokenConverter 时,set 一个自定义的tokenConverter,
* 就可以添加一下自定义的内容了,比如 authentication 中的用户标识 account 等
*/
public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
Map<String, Object> response = new HashMap<String, Object>();
OAuth2Request clientToken = authentication.getOAuth2Request();
if (!authentication.isClientOnly()) {
response.putAll(userTokenConverter.convertUserAuthentication(authentication.getUserAuthentication()));
} else {
if (clientToken.getAuthorities()!=null && !clientToken.getAuthorities().isEmpty()) {
response.put(UserAuthenticationConverter.AUTHORITIES,
AuthorityUtils.authorityListToSet(clientToken.getAuthorities()));
}
}
if (token.getScope()!=null) {
response.put(SCOPE, token.getScope());
}
if (token.getAdditionalInformation().containsKey(JTI)) {
response.put(JTI, token.getAdditionalInformation().get(JTI));
}
if (token.getExpiration() != null) {
response.put(EXP, token.getExpiration().getTime() / 1000);
}
if (includeGrantType && authentication.getOAuth2Request().getGrantType()!=null) {
response.put(GRANT_TYPE, authentication.getOAuth2Request().getGrantType());
}
response.putAll(token.getAdditionalInformation());
response.put(CLIENT_ID, clientToken.getClientId());
if (clientToken.getResourceIds() != null && !clientToken.getResourceIds().isEmpty()) {
response.put(AUD, clientToken.getResourceIds());
}
return response;
}
至此,如何将一个 uuid 的 access_token 转成 jwt 的主要源码就分析完了。
5 授权服务器配置
对 token 的额外处理,可以说完全是围绕下面的授权服务器的配置了。
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
......
//.tokenEnhancer()
//.accessTokenConverter(new JwtAccessTokenConverter())
;
}
-
如果我们想拓展payload的内容,在 new JwtAccessTokenConverter() 之后,set一个自定义的AccessTokenConverter对象即可
public void setAccessTokenConverter(AccessTokenConverter tokenConverter) {
this.tokenConverter = tokenConverter;
}
-
如果我们不想用 JwtHelper 的加密方式,RSA、HMACSHA256等。比如想换成SM2啥的,可以继承JwtAccessTokenConverter,子类重写JWT的生成和验证方法,然后将new JwtAccessTokenConverter() 替换成子类对象就可以了。
-
如果我们不想用jwt,想自定义一种token,可以给 endpoints 设置一个自定义的 tokenEnhancer 对象,按需求实现enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) 方法。