Dubbo + Spring Security Oauth2 如何优雅解决服务提供者无法获取当前用户(上)

2,874 阅读4分钟

Quiet 项目简介:juejin.cn/post/717122…

问题起源

Quiet 项目中使用了 Dubbo 实现服务间调用,使用 Spring Security Oauth2 实现授权和鉴权。

那么就会有个问题:在服务消费者调用服务提供者的时候,服务提供者无法通过 SecurityContextHolder.getContext().getAuthentication() 获取当前用户信息。曾经有个面试官问我:为什么会有这种问题?虽然 Dubbo 支持 http 协议,但是 Dubbo 发起的 http 请求跟 Spring Security Oauth2 处理的 http 请求完全不相干,我不明白为啥会问这个问题。然后他又问:这种问题官方没有提供解决方案吗?啊?这两个八竿子打不着的开源项目为什么官方会提供这种业务问题的解决方案,当时我是这样的

image.png

因为在我的理解里,每个开源项目都有它要解决的具体问题,在解决具体问题的同时会提供一些扩展点,我们可以通过这些扩展点实现自己的业务,像上面的那个问题,属于具体业务问题,官方没理由提供这类问题的解决方案吧?🤔

问题分析

言归正传,为啥使用 Dubbo 调用时,服务提供者无法通过 SecurityContextHolder.getContext().getAuthentication() 获取当前用户信息,要搞清楚这个问题,先搞明白获取的过程。

获取用户信息的过程

方法 SecurityContextHolder.getContext().getAuthentication() 是通过调用 SecurityContextHolder 里面的 strategy 属性的 getContext 方法获取 SecurityContext 实例,然后再通过 SecurityContextgetAuthentication 获取用户信息的。

SecurityContextHolderStrategy 是一个接口类,它的实现类如下:

image.png

它一共有四种实现类,那么在应用中,它使用的是哪一种实现类,先看下 SecurityContextHolder

SecurityContextHolder 是将给定的 SecurityContext 与当前执行线程关联起来,获取和设置 SecurityContext 的方式是委托给 strategy 属性,读一下:

/**
 * 1. 首先会获取 JVM 的 spring.security.strategy 属性
 * 2. 在类加载的时候会调用一次 initialize 方法,进行初始化
 * 3. 根据 spring.security.strategy 属性,采用不同的实现类初始化 strategy 属性
 * 4. 如果没有设置 spring.security.strategy 属性,则默认使用 MODE_THREADLOCAL 策略
 * 5. MODE_THREADLOCAL 策略,strategy 则是使用 ThreadLocalSecurityContextHolderStrategy 进行初始化
 **/
public static final String SYSTEM_PROPERTY = "spring.security.strategy";

private static String strategyName = System.getProperty(SYSTEM_PROPERTY);

static {
 initialize();
}

private static void initialize() {
 initializeStrategy();
 // 初始化次数 +1
 initializeCount++;
}

private static void initializeStrategy() {
 if (MODE_PRE_INITIALIZED.equals(strategyName)) {
  Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
    + ", setContextHolderStrategy must be called with the fully constructed strategy");
  return;
 }
 if (!StringUtils.hasText(strategyName)) {
  // Set default
  strategyName = MODE_THREADLOCAL;
 }
 if (strategyName.equals(MODE_THREADLOCAL)) {
  strategy = new ThreadLocalSecurityContextHolderStrategy();
  return;
 }
 if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
  strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
  return;
 }
 if (strategyName.equals(MODE_GLOBAL)) {
  strategy = new GlobalSecurityContextHolderStrategy();
  return;
 }
 // Try to load a custom strategy
 try {
  // 我们也可以自定义实现策略,扩展性+++
  Class<?> clazz = Class.forName(strategyName);
  Constructor<?> customStrategy = clazz.getConstructor();
  strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
 }
 catch (Exception ex) {
  ReflectionUtils.handleReflectionException(ex);
 }
}

看下 ThreadLocalSecurityContextHolderStrategy 类,可以发现这个类是通过一个 ThreadLocal 属性存储和获取 SecurityContext:

private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

再看看接口 SecurityContextHolderStrategy,可以发现它提供了 setContext 方法来设置 SecurityContext,所以在使用 SecurityContextHolder.getContext().getAuthentication() 获取的 SecurityContext 就是通过 setContext 方法设置的值,如果没有手动指定其他策略的话,也就是 org.springframework.security.core.context.ThreadLocalSecurityContextHolderStrategy#contextHolder 存储的值,看下 SecurityContext 的实现类,可以发现用户信息是通过 org.springframework.security.core.context.SecurityContextImpl#getAuthentication 方法获取的,而获取的用户信息是 SecurityContext 使用方法 setAuthentication 设置的,设置的用户信息又是从哪来的呢?

SecurityContextauthentication 是从哪来的

在上面的分析中,知道了 Spring Security 是如何设置的 authentication,那么就在设置的地方打个断点 DEBUG 下:

image.png

再跟踪下它的堆栈信息:

image.png

在此可以发现用户信息是在方法:org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter#doFilter 中获取和设置的,在这个方法中,关键在于这段代码:

Authentication authResult = authenticationManager.authenticate(authentication);

这段代码验证并获取了用户信息,在分析这段代码之前,可以先留意下这个方法中的这段代码,后面有用:request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());

authenticationManager.authenticate 打个断点,跟踪下去,可以发现在方法 org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager#authenticate 中获取了用户信息:

image.png

好了,到这里就可以搞清楚是怎么获取用户信息的了,从 authentication.getPrincipal() 获取 token,再通过 token 到 tokenServices 获取用户信息,至于 tokenServices 是怎么获取用户信息的,感兴趣的可以自己深究,这里简单说下,tokenServices 是通过一个 tokenStore 和 token 获取用户信息,这个 tokenStore 是在我们整合 Spring Security Oauth2 时注入的一个 Bean,这个 Bean 可以用来存储和获取用户信息,具体的实现方式可以自己实现接口 org.springframework.security.oauth2.provider.token.TokenStore 进行扩展,当然 tokenServices 也可以自己实现。

还记得上面留意的那段代码吗?Spring Security Oauth2 其实已经把这个 token 设置到 request 的属性中了。

结论

通过上面的分析,我们知道了在应用中获取用户信息的整个过程,现在能明白为啥 Dubbo 调用的时候服务提供者无法获取用户信息了吧。应用跟 Dubbo 之间的关系是:应用中嵌入了一个 Dubbo 服务,它们之间是各自处理各自的逻辑,这两个是互不干扰的,Dubbo 提供了服务鉴权方案,但是不会也不必去适配各种用户鉴权框架。

下篇:Dubbo + Spring Security Oauth2 如何优雅解决服务提供者无法获取当前用户(下)