赞
踩
当用户A与用户B分别持有一个合法的令牌token 访问同一个资源服务器时,会间接性的出现,用户A拿着A的合法token 却获取到了用户B的用户信息,B用户相反而之。
security:
oauth2:
resource:
user-info-uri: http://127.0.0.1:8081/user-me
prefer-token-info: false
资源服务器获取用户信息的主要核心源码类是ResourceServerTokenServices
public interface ResourceServerTokenServices {
/**
* Load the credentials for the specified access token.
*
* @param accessToken The access token value.
* @return The authentication for the access token.
* @throws AuthenticationException If the access token is expired
* @throws InvalidTokenException if the token isn't valid
*/
OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
/**
* Retrieve the full access token details from just the value.
*
* @param accessToken the token value
* @return the full access token with client id etc.
*/
OAuth2AccessToken readAccessToken(String accessToken);
}
我们当前使用的便是其子类实现之一的UserInfoTokenServices
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException {
Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
if (map.containsKey("error")) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("userinfo returned error: " + map.get("error"));
}
throw new InvalidTokenException(accessToken);
}
return extractAuthentication(map);
}
在这里出现并发问题的主要是在getmap这个函数
private Map<String, Object> getMap(String path, String accessToken) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Getting user info from: " + path);
}
try {
OAuth2RestOperations restTemplate = this.restTemplate;
if (restTemplate == null) {
BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
resource.setClientId(this.clientId);
restTemplate = new OAuth2RestTemplate(resource);
}
OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext()
.getAccessToken();
if (existingToken == null || !accessToken.equals(existingToken.getValue())) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(
accessToken);
token.setTokenType(this.tokenType);
restTemplate.getOAuth2ClientContext().setAccessToken(token);
}
return restTemplate.getForEntity(path, Map.class).getBody();
}
catch (Exception ex) {
this.logger.warn("Could not fetch user details: " + ex.getClass() + ", "
+ ex.getMessage());
return Collections.<String, Object>singletonMap("error",
"Could not fetch user details");
}
}
在这里通过我的debug 发现问题出现在restTemplate.getOAuth2ClientContext().setAccessToken(token);
设置token时候,出现了并发问题。当我打开了该函数的子类实现,一切问题都烟消云散。
该核心类是OAuth2ClientContext,子类默认实现是DefaultOAuth2ClientContext。
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException {
Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
if (map.containsKey("error")) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("userinfo returned error: " + map.get("error"));
}
throw new InvalidTokenException(accessToken);
}
return extractAuthentication(map);
}
在这里出现并发问题的主要是在getmap这个函数
public class DefaultOAuth2ClientContext implements OAuth2ClientContext, Serializable {
private static final long serialVersionUID = 914967629530462926L;
private OAuth2AccessToken accessToken;
private AccessTokenRequest accessTokenRequest;
private Map<String, Object> state = new HashMap<String, Object>();
public DefaultOAuth2ClientContext() {
this(new DefaultAccessTokenRequest());
}
public DefaultOAuth2ClientContext(AccessTokenRequest accessTokenRequest) {
this.accessTokenRequest = accessTokenRequest;
}
public DefaultOAuth2ClientContext(OAuth2AccessToken accessToken) {
this.accessToken = accessToken;
this.accessTokenRequest = new DefaultAccessTokenRequest();
}
public OAuth2AccessToken getAccessToken() {
return accessToken;
}
public void setAccessToken(OAuth2AccessToken accessToken) {
this.accessToken = accessToken;
this.accessTokenRequest.setExistingToken(accessToken);
}
public AccessTokenRequest getAccessTokenRequest() {
return accessTokenRequest;
}
public void setPreservedState(String stateKey, Object preservedState) {
state.put(stateKey, preservedState);
}
public Object removePreservedState(String stateKey) {
return state.remove(stateKey);
}
}
在该子类中,可以看出setAccessToken并没有做并发控制,简而言之是当A用户设置了token准备访问url获取用户信息时候,B用户进来修改了该值变为Btoken,然而A用户线程又获取到CPU,开始访问了url链接,拿着已被修改为B的token 值获取了 B的用户信息。
下载spring security源码,修改DefaultOAuth2ClientContext,源码将线程问题解决,然后打包,上传maven私服,修改自己项目工程的maven依赖
@Component
public class WAYZDefaultOAuth2ClientContext implements OAuth2ClientContext, Serializable {
private static final long serialVersionUID = 3078781745905248724L;
// make accessToken thread local to avoid thread safe issue
private ThreadLocal<OAuth2AccessToken> accessToken = new ThreadLocal<>();
private AccessTokenRequest accessTokenRequest;
private Map<String, Object> state = new HashMap<String, Object>();
public WAYZDefaultOAuth2ClientContext() {
this(new DefaultAccessTokenRequest());
}
public WAYZDefaultOAuth2ClientContext(AccessTokenRequest accessTokenRequest) {
this.accessTokenRequest = accessTokenRequest;
}
public WAYZDefaultOAuth2ClientContext(OAuth2AccessToken accessToken) {
this.accessToken.set(accessToken);
this.accessTokenRequest = new DefaultAccessTokenRequest();
}
public OAuth2AccessToken getAccessToken() {
return accessToken.get();
}
public void setAccessToken(OAuth2AccessToken accessToken) {
this.accessToken.set(accessToken);
this.accessTokenRequest.setExistingToken(accessToken);
}
public AccessTokenRequest getAccessTokenRequest() {
return accessTokenRequest;
}
public void setPreservedState(String stateKey, Object preservedState) {
state.put(stateKey, preservedState);
}
public Object removePreservedState(String stateKey) {
return state.remove(stateKey);
}
}
这种方式的实现,或许好多人并不理解,只是把bean放到容器里,就可以替换之前默认实现吗?难道不需要一个配置类引入之类吗?答案是不需要的
我通过源码debug发现改类主要是被OAuth2RestTemplate使用,而OAuth2RestTemplate却又是被UserInfoRestTemplateFactory工厂创建,而UserInfoRestTemplateFactory的构造创建又是在ResourceServerTokenServicesConfiguration中,在这个类里面,已经将容器里已存在的类型bean做了注入,然后默认实现的自动替换,这也是源码的巧妙之处。
原文地址:https://blog.csdn.net/qq_38226693/article/details/107708554
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。