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

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

问题描述

当用户A与用户B分别持有一个合法的令牌token 访问同一个资源服务器时,会间接性的出现,用户A拿着A的合法token 却获取到了用户B的用户信息,B用户相反而之。

项目工程

代码使用了redis 作为token 存储,资源服务器通过配置

security:
  oauth2:
    resource:
      user-info-uri: http://127.0.0.1:8081/user-me
      prefer-token-info: false

来指向认证服务器,资源服务器也是根据此接口进行token合法性校验以及校验成功后的用户信息返回。

源码分析

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

解决方案

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

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

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

总结

源码问题出现bug,是很难解决的头疼问题,此文章旨在给大家一个框架解决的思路和基本方案。当我们有一天可以熟练的使用和扩展框架,相信那一刻我们是很伟大的。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值