在《SpringSecurity OAuth2中真正创建Token的实现类DefaultTokenServices、TokenStore(Token存储管理)的详解》中,我们分析了在OAuth2中,Token是如何创建的,同时也了解了TokenStore是如何管理Token的,并详细分析了InMemoryTokenStore 实现类的逻辑,而JdbcTokenStore 和 RedisTokenStore 实现思路是类似的,但是JwtTokenStore 的实现类就和InMemoryTokenStore不一样了,这篇内容就详细分析JwtTokenStore是如何实现Token的管理的。首先,了解一下JWT中的一些概念,如下所示:
JSON Web Key (JWK)是RFC规范定义的一种数据结构,用来表示密码密钥的结构。详情可以参考:《JSON Web Key (JWK) RFC官方规范》。JWKS 是 JWK的数组。
其中,JWK的参数定义如下:
“kty”(key type)
表示密钥使用的加密算法,比如“RSA”或者“EC”等,是大小写敏感的字符串。JWK中必须携带这个字段。
“use”(Public Key Use)
表示公钥的使用目的。指示公钥是用于加密数据还是用于验证数据上的签名。可以是如下值:
1>、“sig”(signature)
2>、“enc”(encryption)
大小写敏感。可以选择性携带。
“key_ops”(Key Operations)
标识要使用密钥的操作。用于可能存在公共、私有或对称密钥的用例。key_ops字段的值是数组,数组可以包含以下值:
1>、“sign”(计算数字签名或MAC)
2>、“verify”(验证数字签名或MAC)
3>、“encrypt”(加密内容)
4>、“decrypt”(解密内容以及验证解密)
5>、“warpKey”(加密密钥)
6>、“unwrapKey”(解密密钥并验证解密)
7>、“deriveKey”(产生密钥)
8>、“deriveBits”(产生bits,但是该bits不用于密钥)
“alg”(algorithm)
标识用于密钥的算法。
“kid”(Key ID)
用于匹配密钥。主要在JWK集合中选择jwk。
“n”(公钥的模值)
“e”(公钥的指数)
“x5u”(X.509 URL)
“x5c”(X.509 Certificate Chain)
“x5t”(X.509 Certificate SHA-1 Thumbprint)
“x5t#S256”(X.509 Certificate SHA-256 Thumbprint)
上述参数介绍内容来自《JWK和JWKs的格式》。
JWS、JWE是JWT的一种实现,而网上大多数介绍JWT的文章实际介绍的都是JWS(JSON Web Signature),也往往导致了人们对于JWT的误解,但是JWT并不等于JWS。
JWS是一个有着简单的统一表达形式的字符串,通过使用数字技术保护传输的内容不被修改,可以保证数据的完整性,但是由于仅采用Base64对消息内容编码,因此不保证数据的不可泄露性,所以不适合用于传输敏感数据。
JWS字符串包括了头部(Header)、载荷(PayLoad)、签名(signature)三部分。
其中,头部(Header)用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等,JSON内容要经Base64 编码生成字符串成为Header。
载荷(PayLoad)的五个字段都是由JWT的标准所定义的:
载荷(PayLoad)后面的信息可以按需补充。JSON内容要经Base64 编码生成字符串成为PayLoad。
签名(signature)这个部分header与payload通过header中声明的加密方式,使用密钥secret进行加密,生成签名,保证数据在传输过程中不被修改。
详情可以参考:《JSON Web Signature (JWS) RFC官方规范》、《一篇文章带你分清楚JWT,JWS与JWE》。
相对于JWS,JWE则同时保证了安全性与数据完整性。JWE由五部分组成:
具体生成步骤为:
可见,JWE的计算过程相对繁琐,不够轻量级,因此适合与数据传输而非token认证,但该协议也足够安全可靠,用简短字符串描述了传输内容,兼顾数据的安全性与完整性。
详情可以参考:《JSON Web Encryption (JWE) RFC官方规范》、《一篇文章带你分清楚JWT,JWS与JWE》、《一文读懂JWT,JWS,JWE》。
JWT(json web token)是设计一种简洁,安全,无状态的token的实现规范rfc7519,通常用于网络请求方和网络接收方之间的网络请求认证。JWS和JWE都是JWT的一种实现。
JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。其中,第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature)。当拥有签名的JWT被称为JWS,也就是签了名的JWS;没有签名部分的JWT被称为nonsecure JWT也就是不安全的JWT,此时header中声明的签名算法为none。
详情可以参考: 《JSON Web Token(JWT) RFC官方规范》、《JWT、JWE、JWS 、JWK 到底是什么?该用 JWT 还是 JWS?》。
总之,一句话概况上述的概念就是:JWT是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519);而JWK 是用来加密JWT的密钥或者密钥对;JWS,也就是JWT Signature,即在JWT基础上,在头部声明签名算法,并在最后添加上签名,保证可以校验token的完整性;JWE,也是JWT的一种实现,不仅能够保证数据的完整性,还可以保证数据的安全性,即不会被第三方解码获取原始数据。
和InMemoryTokenStore相比,JwtTokenStore实现类没有持久化Token信息,但是JwtTokenStore实现了access tokens 和 authentications的相互转换,该功能通过JwtAccessTokenConverter对象实现,因此当需要authentications信息时,直接通过access tokens就可以获取到。因为不需要存储Token信息,所以TokenStore接口中定义的一些方法,在JwtTokenStore的实现类中就不需要实现,如下所示:
以下空方法的实现,均因为不需要存储Token这种特性造成的。
根据AccessToken对象查询对应的OAuth2Authentication(认证的用户信息),在JwtTokenStore实现类中,是通过jwtTokenEnhancer的extractAuthentication()方法,实现了token字符串和OAuth2Authentication对象的转换。jwtTokenEnhancer后续专门分析。具体实现如下:
@Override
public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
return readAuthentication(token.getValue());
}
@Override
public OAuth2Authentication readAuthentication(String token) {
return jwtTokenEnhancer.extractAuthentication(jwtTokenEnhancer.decode(token));
}
@Override
public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
return readAuthentication(token.getValue());
}
根据AccessToken的value值查询对应的token对象,该方法是通过jwtTokenEnhancer的extractAccessToken()方法实现。具体实现如下:
@Override
public OAuth2AccessToken readAccessToken(String tokenValue) {
OAuth2AccessToken accessToken = convertAccessToken(tokenValue);
//判断是否是刷新token,当additionalInformation中包括ACCESS_TOKEN_ID(AccessTokenConverter.ATI,“ati”)时,就是刷新token
if (jwtTokenEnhancer.isRefreshToken(accessToken)) {
throw new InvalidTokenException("Encoded token is a refresh token");
}
return accessToken;
}
private OAuth2AccessToken convertAccessToken(String tokenValue) {
return jwtTokenEnhancer.extractAccessToken(tokenValue, jwtTokenEnhancer.decode(tokenValue));
}
根据token字符串查询对应的token对象,实际上还是通过jwtTokenEnhancer的extractAccessToken()先获取OAuth2AccessToken对象,然后判断该对象是否是刷新对,如果是的话,就创建默认的DefaultOAuth2RefreshToken对象,具体实现如下所示:
@Override
public OAuth2RefreshToken readRefreshToken(String tokenValue) {
OAuth2AccessToken encodedRefreshToken = convertAccessToken(tokenValue);
OAuth2RefreshToken refreshToken = createRefreshToken(encodedRefreshToken);
if (approvalStore != null) {
OAuth2Authentication authentication = readAuthentication(tokenValue);
if (authentication.getUserAuthentication() != null) {
String userId = authentication.getUserAuthentication().getName();
String clientId = authentication.getOAuth2Request().getClientId();
Collection<Approval> approvals = approvalStore.getApprovals(userId, clientId);
Collection<String> approvedScopes = new HashSet<String>();
for (Approval approval : approvals) {
if (approval.isApproved()) {
approvedScopes.add(approval.getScope());
}
}
if (!approvedScopes.containsAll(authentication.getOAuth2Request().getScope())) {
return null;
}
}
}
return refreshToken;
}
在上述readRefreshToken()方法中,首先通过convertAccessToken()方法把token字符串转换成OAuth2AccessToken对象,实现如下:
private OAuth2AccessToken convertAccessToken(String tokenValue) {
return jwtTokenEnhancer.extractAccessToken(tokenValue, jwtTokenEnhancer.decode(tokenValue));
}
然后,在通过createRefreshToken()方法,实现OAuth2AccessToken对象转OAuth2RefreshToken对象,实现如下:
private OAuth2RefreshToken createRefreshToken(OAuth2AccessToken encodedRefreshToken) {
//首先判断,encodedRefreshToken是不是刷新token,即是否带“ati”标识
if (!jwtTokenEnhancer.isRefreshToken(encodedRefreshToken)) {
throw new InvalidTokenException("Encoded token is not a refresh token");
}
//判断是否带有expiration参数,然后创建对应的DefaultExpiringOAuth2RefreshToken或DefaultOAuth2RefreshToken对象。
if (encodedRefreshToken.getExpiration()!=null) {
return new DefaultExpiringOAuth2RefreshToken(encodedRefreshToken.getValue(),
encodedRefreshToken.getExpiration());
}
return new DefaultOAuth2RefreshToken(encodedRefreshToken.getValue());
}
再然后,当approvalStore对象不为空时,校验当前token是否有权限访问,没有权限的话,直接返回null,具体实现如下:
if (approvalStore != null) {
//首先,token字符串转换成OAuth2Authentication对象
OAuth2Authentication authentication = readAuthentication(tokenValue);
//判断OAuth2Authentication对象中是否包含用户认证信息
if (authentication.getUserAuthentication() != null) {
//获取userId,clientId,最后通过getApprovals()方法获取当前用户的被授权的scope集合。
String userId = authentication.getUserAuthentication().getName();
String clientId = authentication.getOAuth2Request().getClientId();
Collection<Approval> approvals = approvalStore.getApprovals(userId, clientId);
Collection<String> approvedScopes = new HashSet<String>();
//当前授权可用的,添加到approvedScopes集合中
for (Approval approval : approvals) {
if (approval.isApproved()) {
approvedScopes.add(approval.getScope());
}
}
//判断当前请求中的scope是否在允许的scope范围内,没有的话直接返回null,既获取不到刷新token。
if (!approvedScopes.containsAll(authentication.getOAuth2Request().getScope())) {
return null;
}
}
}
最后,如果验证通过,就把创建的OAuth2RefreshToken的对象直接返回。
移除OAuth2RefreshToken 对象。
@Override
public void removeRefreshToken(OAuth2RefreshToken token) {
remove(token.getValue());
}
private void remove(String token) {
//当approvalStore不为空时,执行下面逻辑,否则不执行任何逻辑,相当于removeRefreshToken()方法为空方法
if (approvalStore != null) {
//token字符串转 OAuth2Authentication对象
OAuth2Authentication auth = readAuthentication(token);
//获取clientId、用户认证信息
String clientId = auth.getOAuth2Request().getClientId();
Authentication user = auth.getUserAuthentication();
//当用户认证信息不为空时
if (user != null) {
Collection<Approval> approvals = new ArrayList<Approval>();
//获取当前token中的信息,构建approvals 集合信息,即当前token对应允许的权限集合
for (String scope : auth.getOAuth2Request().getScope()) {
approvals.add(new Approval(user.getName(), clientId, scope, new Date(), ApprovalStatus.APPROVED));
}
//调用approvalStore的revokeApprovals()方法实现授权信息的移除
approvalStore.revokeApprovals(approvals);
}
}
}
该接口定义了一些变量,同时提供了在JWT编码的令牌值和OAuth身份验证信息之间进行转换的功能(双向)。具体定义如下:
public interface AccessTokenConverter {
final String AUD = "aud";
final String CLIENT_ID = "client_id";
final String EXP = "exp";
final String JTI = "jti";
final String GRANT_TYPE = "grant_type";
final String ATI = "ati";
final String SCOPE = OAuth2AccessToken.SCOPE;
final String AUTHORITIES = "authorities";
//把OAuth2AccessToken 和 OAuth2Authentication 对象信息转换成Map对象
Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);
//转换OAuth2AccessToken 对象
OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map);
//转换OAuth2Authentication 对象
OAuth2Authentication extractAuthentication(Map<String, ?> map);
}
AccessTokenConverter 接口有如下实现类:
其中,DefaultAccessTokenConverter 是默认实现类,提供了AccessTokenConverter接口的默认实现,在该实现类中,又通过UserAuthenticationConverter(DefaultUserAuthenticationConverter默认实现类)实现类Map对象与Authentication对象之间的相互转换;JwtAccessTokenConverter实现类不仅实现了AccessTokenConverter接口,同时还实现了TokenEnhancer接口,所以JwtAccessTokenConverter同时具备两个接口的功能,即不仅提供了JWT编码的令牌值和OAuth身份验证信息之间进行转换的功能,还提供了增强access token的功能;JwkVerifyingJwtAccessTokenConverter是JwtAccessTokenConverter的扩展,负责使用JWK验证JWS。
在AuthorizationServerTokenServices实现存储访问令牌之前对其进行增强的策略。该接口只定义了一个增强access token的接口,如下所示:
public interface TokenEnhancer {
OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication);
}
TokenEnhancer实现类如下:
其中,TokenEnhancerChain 实现类是为了组合使用TokenEnhancer真正的实现类,提供了代理功能;而JwtAccessTokenConverter实现类是真正的实现类。
JwtAccessTokenConverter实现类不仅实现了AccessTokenConverter接口,同时还实现了TokenEnhancer接口,所以JwtAccessTokenConverter同时具备两个接口的功能,即不仅提供了JWT编码的令牌值和OAuth身份验证信息之间进行转换的功能,还提供了增强access token的功能。
在JwtAccessTokenConverter实现类中,引入了AccessTokenConverter属性,默认值为DefaultAccessTokenConverter对象,即通过该属性对象提供了在JWT编码的令牌值和OAuth身份验证信息之间进行转换的功能(双向)。实际上JwtAccessTokenConverter实现AccessTokenConverter接口中定义的方法的方式,就是直接调用DefaultAccessTokenConverter对象对应的方法而实现的,如下所示:
@Override
public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
return tokenConverter.convertAccessToken(token, authentication);
}
@Override
public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
return tokenConverter.extractAccessToken(value, map);
}
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
return tokenConverter.extractAuthentication(map);
}
该对象主要用于校验claim内容,在解码token字符串的时候,会调用该对象的verify()方法校验claim内容。
protected Map<String, Object> decode(String token) {
try {
//转换token字符串为JWT对象
Jwt jwt = JwtHelper.decodeAndVerify(token, verifier);
String claimsStr = jwt.getClaims();
//获取claim内容,并转换成Map对象返回
Map<String, Object> claims = objectMapper.parseMap(claimsStr);
if (claims.containsKey(EXP) && claims.get(EXP) instanceof Integer) {
Integer intValue = (Integer) claims.get(EXP);
claims.put(EXP, new Long(intValue));
}
this.getJwtClaimsSetVerifier().verify(claims);
return claims;
}
catch (Exception e) {
throw new InvalidTokenException("Cannot convert access token to JSON", e);
}
}
JwtClaimsSetVerifier接口,定义了一个校验claims的verify()方法,如下所示:
public interface JwtClaimsSetVerifier {
void verify(Map<String, Object> claims) throws InvalidTokenException;
}
JwtClaimsSetVerifier接口的实现类,如下:
其中,DelegatingJwtClaimsSetVerifier用于组合其他JwtClaimsSetVerifier实现类,实现代理功能;IssuerClaimVerifier实现类,用于校验iss信息;NoOpJwtClaimsSetVerifier实现类是JwtAccessTokenConverter类的内部实现类,定义了一个空方法,即不做任何校验。
在JwtAccessTokenConverter实现类中,定义了signer、verifier属性,其中,signer属性用于token编码时签名,verifier属性用于token解码时校验,包括了对称(默认MacSigner实现)和非对称(默认RsaSigner、RsaVerifier实现)两种方式。
//编码
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
//…… 省略
//编码时,需要签名,实际是在JwtHelper的工具类中完成。
String token = JwtHelper.encode(content, signer).getEncoded();
return token;
}
//解码
protected Map<String, Object> decode(String token) {
//省略其他代码……
//解码时,需要校验token字符串内容
Jwt jwt = JwtHelper.decodeAndVerify(token, verifier);
}
JwtAccessTokenConverter类针对TokenEnhancer接口方法的实现,提供了自定义access token内容的入口,具体实现如下:
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
//创建新的DefaultOAuth2AccessToken 对象
DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
//获取原来的additionalInformation信息,并设置对应的tokenId
Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
String tokenId = result.getValue();
if (!info.containsKey(TOKEN_ID)) {
info.put(TOKEN_ID, tokenId);
}
else {
tokenId = (String) info.get(TOKEN_ID);
}
result.setAdditionalInformation(info);
//把最新的内容,进行编码,然后设置到DefaultOAuth2AccessToken 的value属性中,即对应的token字符串
result.setValue(encode(result, authentication));
//处理刷新token信息
OAuth2RefreshToken refreshToken = result.getRefreshToken();
if (refreshToken != null) {
//创建新的刷新token对象
DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
//设置刷新token的value值,还是原来刷新token的value值
encodedRefreshToken.setValue(refreshToken.getValue());
encodedRefreshToken.setExpiration(null);
try {
//解析refreshToken中,原来是否存在TOKEN_ID值,如果存在,就作为刷新token的value值
Map<String, Object> claims = objectMapper
.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
if (claims.containsKey(TOKEN_ID)) {
encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
}
}
catch (IllegalArgumentException e) {
}
//维护刷新token的additionalInformation信息,并设置对应的TOKEN_ID、ACCESS_TOKEN_ID值,其中ACCESS_TOKEN_ID值可以作为是否是刷新token的判断条件。
Map<String, Object> refreshTokenInfo = new LinkedHashMap<String, Object>(
accessToken.getAdditionalInformation());
refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
//根据编码后的encodedRefreshToken 和 authentication创建刷新token。
DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
encode(encodedRefreshToken, authentication));
//如果带有过期时间,就创建DefaultExpiringOAuth2RefreshToken对象
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
encodedRefreshToken.setExpiration(expiration);
token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
}
//为访问token设置刷新token。
result.setRefreshToken(token);
}
return result;
}
关于JwtTokenStore类中的ApprovalStore对象,后续章节中专门内容进行介绍。还有JwtAccessTokenConverter中签名对象Signer、校验对象SignatureVerifier更具体的用法后续章节在继续讨论。