当前位置:   article > 正文

Spring Security Oauth资源服务器并发情况下获取用户信息错乱

Spring Security Oauth资源服务器并发情况下获取用户信息错乱

Oauth2 资源服务器并发情况下获取用户信息错乱

问题描述

当用户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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

源码分析

资源服务器获取用户信息的主要核心源码类是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);

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

我们当前使用的便是其子类实现之一的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);
	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在这里出现并发问题的主要是在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");
		}
	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

在这里通过我的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);
	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在这里出现并发问题的主要是在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);
	}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

在该子类中,可以看出setAccessToken并没有做并发控制,简而言之是当A用户设置了token准备访问url获取用户信息时候,B用户进来修改了该值变为Btoken,然而A用户线程又获取到CPU,开始访问了url链接,拿着已被修改为B的token 值获取了 B的用户信息。

解决方案

1、修改源码

下载spring security源码,修改DefaultOAuth2ClientContext,源码将线程问题解决,然后打包,上传maven私服,修改自己项目工程的maven依赖

2,添加新的子类实现,并作为新bean注入

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


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

这种方式的实现,或许好多人并不理解,只是把bean放到容器里,就可以替换之前默认实现吗?难道不需要一个配置类引入之类吗?答案是不需要的

我通过源码debug发现改类主要是被OAuth2RestTemplate使用,而OAuth2RestTemplate却又是被UserInfoRestTemplateFactory工厂创建,而UserInfoRestTemplateFactory的构造创建又是在ResourceServerTokenServicesConfiguration中,在这个类里面,已经将容器里已存在的类型bean做了注入,然后默认实现的自动替换,这也是源码的巧妙之处。

原文地址:https://blog.csdn.net/qq_38226693/article/details/107708554

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/188243
推荐阅读
相关标签
  

闽ICP备14008679号