服务提供者如何获取当前用户
问题
项目中使用了 Dubbo 实现服务间调用,使用 Spring Security Oauth2 实现授权和鉴权。在服务消费者调用服务提供者的时候,服务提供者无法通过SecurityContextHolder.getContext().getAuthentication()
获取当前用户信息。
问题分析
为什么在使用 Dubbo 调用时,服务提供者无法使用SecurityContextHolder.getContext().getAuthentication()
获取当前用户信息,要搞清楚这个问题,先分析获取当前用户信息的过程。
获取用户信息的过程
方法SecurityContextHolder.getContext().getAuthentication()
是通过调用SecurityContextHolder
里面的strategy
属性的getContext
方法获取SecurityContext
实例,然后再通过SecurityContext
的getAuthentication
获取用户信息的。
SecurityContextHolderStrategy
是一个接口类,它的实现类如下:
它一共有四种实现类,那么在应用中,它使用的是哪一种实现类,先看下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
设置的,设置的用户信息又是从哪来的呢?
SecurityContext
的authentication
是从哪来的
在上面的分析中,可知 Spring Security 是如何设置的authentication
,在此打个断点:
再跟踪下堆栈信息:
在此可以发现用户信息是在方法: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
中获取了用户信息:
到此可知获取用户信息的流程,从authentication.getPrincipal()