SpringSecurity OAuth2中关于TokenStore实现类JwtTokenStore的详解

导读:本篇文章讲解 SpringSecurity OAuth2中关于TokenStore实现类JwtTokenStore的详解,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

1、前言

  在《SpringSecurity OAuth2中真正创建Token的实现类DefaultTokenServices、TokenStore(Token存储管理)的详解》中,我们分析了在OAuth2中,Token是如何创建的,同时也了解了TokenStore是如何管理Token的,并详细分析了InMemoryTokenStore 实现类的逻辑,而JdbcTokenStore 和 RedisTokenStore 实现思路是类似的,但是JwtTokenStore 的实现类就和InMemoryTokenStore不一样了,这篇内容就详细分析JwtTokenStore是如何实现Token的管理的。首先,了解一下JWT中的一些概念,如下所示:

2、概念

2.1、JWK (JSON Web Key)、JWKS

  JSON Web Key (JWK)是RFC规范定义的一种数据结构,用来表示密码密钥的结构。详情可以参考:《JSON Web Key (JWK) RFC官方规范》。JWKS 是 JWK的数组。
  其中,JWK的参数定义如下:

  1. “kty”(key type)
    表示密钥使用的加密算法,比如“RSA”或者“EC”等,是大小写敏感的字符串。JWK中必须携带这个字段。

  2. “use”(Public Key Use)
    表示公钥的使用目的。指示公钥是用于加密数据还是用于验证数据上的签名。可以是如下值:
    1>、“sig”(signature)
    2>、“enc”(encryption)
    大小写敏感。可以选择性携带。

  3. “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不用于密钥)

  4. “alg”(algorithm)
    标识用于密钥的算法。

  5. “kid”(Key ID)
    用于匹配密钥。主要在JWK集合中选择jwk。

  6. “n”(公钥的模值)

  7. “e”(公钥的指数)

  8. “x5u”(X.509 URL)

  9. “x5c”(X.509 Certificate Chain)

  10. “x5t”(X.509 Certificate SHA-1 Thumbprint)

  11. “x5t#S256”(X.509 Certificate SHA-256 Thumbprint)

上述参数介绍内容来自《JWK和JWKs的格式》

2.2、JWS(JSON Web Signature)、JWT(Json Web Token)、JWE(JSON Web Encryption)
JWT、JWS、JWE三者间的关系

  JWS、JWE是JWT的一种实现,而网上大多数介绍JWT的文章实际介绍的都是JWS(JSON Web Signature),也往往导致了人们对于JWT的误解,但是JWT并不等于JWS。
在这里插入图片描述

JWS(JSON Web Signature)

  JWS是一个有着简单的统一表达形式的字符串,通过使用数字技术保护传输的内容不被修改,可以保证数据的完整性,但是由于仅采用Base64对消息内容编码,因此不保证数据的不可泄露性,所以不适合用于传输敏感数据。

  JWS字符串包括了头部(Header)、载荷(PayLoad)、签名(signature)三部分。

  其中,头部(Header)用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等,JSON内容要经Base64 编码生成字符串成为Header。

  载荷(PayLoad)的五个字段都是由JWT的标准所定义的:

  1. iss: 该JWT的签发者
  2. sub: 该JWT所面向的用户
  3. aud: 接收该JWT的一方
  4. exp(expires): 什么时候过期,这里是一个Unix时间戳
  5. iat(issued at): 在什么时候签发的

  载荷(PayLoad)后面的信息可以按需补充。JSON内容要经Base64 编码生成字符串成为PayLoad。

  签名(signature)这个部分header与payload通过header中声明的加密方式,使用密钥secret进行加密,生成签名,保证数据在传输过程中不被修改。

在这里插入图片描述

详情可以参考:《JSON Web Signature (JWS) RFC官方规范》《一篇文章带你分清楚JWT,JWS与JWE》

JWE(JSON Web Encryption)

  相对于JWS,JWE则同时保证了安全性与数据完整性。JWE由五部分组成:
在这里插入图片描述
  具体生成步骤为:

  1. JOSE含义与JWS头部相同。
  2. 生成一个随机的Content Encryption Key (CEK)。
  3. 使用RSAES-OAEP加密算法,用公钥加密CEK,生成JWE Encrypted Key。
  4. 生成JWE初始化向量。
  5. 使用AESGCM加密算法对明文部分进行加密生成密文Ciphertext,算法会随之生成一个128位的认证标记Authentication Tag。
  6. 对五个部分分别进行base64编码。

  可见,JWE的计算过程相对繁琐,不够轻量级,因此适合与数据传输而非token认证,但该协议也足够安全可靠,用简短字符串描述了传输内容,兼顾数据的安全性与完整性。

详情可以参考:《JSON Web Encryption (JWE) RFC官方规范》《一篇文章带你分清楚JWT,JWS与JWE》《一文读懂JWT,JWS,JWE》

JWT(JSON Web Token)

  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的一种实现,不仅能够保证数据的完整性,还可以保证数据的安全性,即不会被第三方解码获取原始数据。

3、JwtTokenStore

  和InMemoryTokenStore相比,JwtTokenStore实现类没有持久化Token信息,但是JwtTokenStore实现了access tokens 和 authentications的相互转换,该功能通过JwtAccessTokenConverter对象实现,因此当需要authentications信息时,直接通过access tokens就可以获取到。因为不需要存储Token信息,所以TokenStore接口中定义的一些方法,在JwtTokenStore的实现类中就不需要实现,如下所示:

3.1、JwtTokenStore实现类中的空方法

  以下空方法的实现,均因为不需要存储Token这种特性造成的。

  1. storeAccessToken()方法 不需要存储Token信息
  2. removeAccessToken()方法,因为没有存储,所以也不需要删除,同时也造成了JWT的方式不能使得Token过期,这也是JWT方式的一个致命缺点。
  3. storeRefreshToken()方法
  4. removeAccessTokenUsingRefreshToken()方法
  5. getAccessToken()方法,调用该方法会直接返回null
  6. findTokensByClientIdAndUserName()方法,返回空集合
  7. findTokensByClientId()方法,返回空集合
3.2、readAuthentication()方法、readAuthenticationForRefreshToken()方法

  根据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());
}
3.3、readAccessToken()方法

  根据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));
}
3.4、readRefreshToken()方法

  根据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的对象直接返回。

3.5、removeRefreshToken()方法

  移除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);
		}
	}
}

4、JwtAccessTokenConverter

4.1、AccessTokenConverter 接口

  该接口定义了一些变量,同时提供了在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。

4.2、TokenEnhancer接口

  在AuthorizationServerTokenServices实现存储访问令牌之前对其进行增强的策略。该接口只定义了一个增强access token的接口,如下所示:

public interface TokenEnhancer {
	OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication);
}

  TokenEnhancer实现类如下:
在这里插入图片描述
  其中,TokenEnhancerChain 实现类是为了组合使用TokenEnhancer真正的实现类,提供了代理功能;而JwtAccessTokenConverter实现类是真正的实现类。

4.3、JwtAccessTokenConverter

  JwtAccessTokenConverter实现类不仅实现了AccessTokenConverter接口,同时还实现了TokenEnhancer接口,所以JwtAccessTokenConverter同时具备两个接口的功能,即不仅提供了JWT编码的令牌值和OAuth身份验证信息之间进行转换的功能,还提供了增强access token的功能。

AccessTokenConverter属性

  在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);
}
jwtClaimsSetVerifier属性

  该对象主要用于校验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类的内部实现类,定义了一个空方法,即不做任何校验。

Signer、SignatureVerifier对象

  在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);
}
enhance()方法

  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;
}

5、后续

  关于JwtTokenStore类中的ApprovalStore对象,后续章节中专门内容进行介绍。还有JwtAccessTokenConverter中签名对象Signer、校验对象SignatureVerifier更具体的用法后续章节在继续讨论。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68717.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!