跳到主要内容

04、Spring Security 实战 - 登录用户数据的获取,超详细的源码分析

【Spring Security 实战(一)】Spring Security的整体架构 中叙述过一个SecurityContextHolder 这个类。说在处理请求时,Spring Security 会先从 Session 中取出用户登录数据,保存到 SecurityContextHolder 中,然后在请求处理完毕后,又会拿 SecurityContextHolder 中的数据保存到 Session 中,然后再清空 SecurityContextHolder 中的数据。且说了 SecurityContextHolder 内部数据保存默认是通过 ThreadLocal 来实现的。

下面分析 SecurityContextHolder 的源码,并述说如何在代码中获取登录用户的数据。
(如果不想看源码分析的可以直接跳过看怎么获取用户数据)

一、SecurityContextHolder 源码分析

在分析源码之前,可以看一下下面这个图,它展示了 SecurityContextHolder 和 用户数据信息 的结构关系。SecurityContextHolder 依赖 SecurityContext,SecurityContext 封装了 Authentication,而Authentication 即是我们所指的认证后的用户数据信息。(从这关系以及上面的分析,大概应该可以猜测到 SecurityContextHolder 中用了策略设计模式,命名也都很规范化,~ Context,~ ContextHolder🤣)
 

策略模式(Strategy):它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。

既然说它用了策略模式,那SecurityContextHolder中定义的算法家族呢?下面来看一下SecurityContextHolder类中的属性。

public class SecurityContextHolder {
   
     
	// 指的是算法策略中的ThreadLocalSecurityContextHolderStrategy
	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
	//InheritableThreadLocalSecurityContextHolderStrategy
	public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
	// GlobalSecurityContextHolderStrategy
	public static final String MODE_GLOBAL = "MODE_GLOBAL";
	// 这个表示不适用任何策略,用原先的HttpSession
	private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
	// 配置名称
	public static final String SYSTEM_PROPERTY = "spring.security.strategy";
	// 首先是从系统配置中获取
	// idea中可以在vmoptions中进行配置,
	// 例如:-Dspring.security.strategy=MODE_THREADLOCAL
	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);

	private static SecurityContextHolderStrategy strategy;
}

可以看见有一个 SecurityContextHolderStrategy 对象 strategy,它就是“算法的封装体”。SecurityContextHolderStrategy 是一个接口,下面是其源代码,比较简单。

public interface SecurityContextHolderStrategy {
   
     
	// 清除SecurityContext
	void clearContext();
	// 获取SecurityContext
	SecurityContext getContext();
	// 存取SecurityContext
	void setContext(SecurityContext context);
	// 得到一个空的SecurityContext
	SecurityContext createEmptyContext();

}

看下图可以知道算法家族的成员。

 

  • ThreadLocalSecurityContextHolderStrategy:存储数据的载体是一个 ThreadLocal,所以针对 SecurityContext 的清空、获取以及存储,都是在 ThreadLocal 中进行操作。源码过于简单,不分析了,自己看吧。
     

  • InheritableThreadLocalSecurityContextHolderStrategy:和前者实现策略没有区别,只不过用的是ThreadLocal的子类InheritableThreadLocal,这样子线程和父线程都可以获取到用户数据了。源码也没啥,自己看看就OK了。
     

  • GlobalSecurityContextHolderStrategy:它实现起来就更更更简单了,直接用个静态变量保存 SecurityContext,所以多线程环境下它是可以使用了,但一般在web开发中,这肯定是使用的少的。
     

  • ListeningSecurityContextHolderStrategy:SecurityContext 的事件监听策略,它是5.6版本后推出来放到SecurityContextHolderStrategy 策略中的。《深入浅出 Spring Security》书中并没有提到它,但我还是有必要了解的。使用它可以在不去配置系统配置的情况下更换策略,也可以监听 SecurityContext 的创建和销毁事件。注意这里没有获取事件。

  • 它构造方法进行了重载,可以看一下(有些构造源码上说5.7更新的,不管了,现在都 6点 多了),一些判断是否为空的代码我就去调了,留核心代码。

public final class ListeningSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
   
     

	// 监听器集合
	private final Collection<SecurityContextChangedListener> listeners;
	// 委托策略对象,默认的话也是ThreadLocalSecurityContextHolderStrategy
	private final SecurityContextHolderStrategy delegate;

	public ListeningSecurityContextHolderStrategy(Collection<SecurityContextChangedListener> listeners) {
   
     
		this(new ThreadLocalSecurityContextHolderStrategy(), listeners);
	}
	public ListeningSecurityContextHolderStrategy(SecurityContextChangedListener... listeners) {
   
     
		this(new ThreadLocalSecurityContextHolderStrategy(), listeners);
	}
	public ListeningSecurityContextHolderStrategy(SecurityContextHolderStrategy delegate,
			Collection<SecurityContextChangedListener> listeners) {
   
     
		this.delegate = delegate;
		this.listeners = listeners;
	}

// 可变参数重载,可进行配置自己想要的策略对象(delegate)
	public ListeningSecurityContextHolderStrategy(SecurityContextHolderStrategy delegate,
			SecurityContextChangedListener... listeners) {
   
     
		this.delegate = delegate;
		this.listeners = Arrays.asList(listeners);
	}

在看看它的其他源码,在创建和销毁 SecurityContext 的时候会调用监听器去监听。


@Override
public void clearContext() {

 
    SecurityContext from = getContext();
    this.delegate.clearContext();
    publish(from, null);
}

@Override
public SecurityContext getContext() {

 
    return this.delegate.getContext();
}

@Override
public void setContext(SecurityContext context) {

 
    SecurityContext from = getContext();
    this.delegate.setContext(context);
    publish(from, context);
}

@Override
public SecurityContext createEmptyContext() {

 
    return this.delegate.createEmptyContext();
}
// 执行监听措施
private void publish(SecurityContext previous, SecurityContext current) {

 
    if (previous == current) {

 
        return;
    }
    SecurityContextChangedEvent event = new SecurityContextChangedEvent(previous, current);
    for (SecurityContextChangedListener listener : this.listeners) {

 
        listener.securityContextChanged(event);
    }
}

监听器SecurityContextChangedEvent 是一个函数式接口,咱配置的时候直接使用 lambda 就好了。

讲了半天的策略,回归策略的封装者 SecurityContextHolder。来看看它的初始化操作,它是提供了一个静态代码块,执行初始化。

static {

 
    initialize();
}

private static void initialize() {

 
    initializeStrategy();
    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;
    }
    // 然后判断是否为空,为空就默认设置为ThreadLocalSecurity...
    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);
    }
}

了解了其如何进行初始化的后,那就好办了,直接看它内部方法吧。其内部方法都是静态的。

// 清除SecurityContext
public static void clearContext() {
   
     
		strategy.clearContext();
	}
// 获取SecurityContext
	public static SecurityContext getContext() {
   
     
		return strategy.getContext();
	}

// 初始化次数,emmm,发送请求的次数?
	public static int getInitializeCount() {
   
     
		return initializeCount;
	}

// 配置SecurityContext
	public static void setContext(SecurityContext context) {
   
     
		strategy.setContext(context);
	}

// 配置StrategyName
	public static void setStrategyName(String strategyName) {
   
     
		SecurityContextHolder.strategyName = strategyName;
		initialize();
	}
// 出于5.6版本,估计是让你更好的配置监听策略用的,事实上也就这个方法可以做到了
	public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
   
     
		Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
		SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
		SecurityContextHolder.strategy = strategy;
		initialize();
	}
// 获取策略对象
	public static SecurityContextHolderStrategy getContextHolderStrategy() {
   
     
		return strategy;
	}

// 创建空的SecurityContext
// 也就是创建SecurityContextImpl
	public static SecurityContext createEmptyContext() {
   
     
		return strategy.createEmptyContext();
	}

源码分析到这,差不多就很清晰了,再看看SecurityContextImpl的源码吧,其实不用看也知道,就是 Authentication 对象的封装,这看一下属性和构造就差不多可以猜到大概了。 
最后还需要注意:ThreadLocalSecurityContextHolderStrategy、InheritableThreadLocalSecurityContextHolderStrategy、GlobalSecurityContextHolderStrategy 访问权限都是default默认的,不是本包下的是不让new的,也就是对外不让实例化,你只能通过它给的进行对内策略更改。

ListeningSecurityContextHolderStrategy 使用案例

@Component
@Slf4j
public class InitCommandRun implements CommandLineRunner {
   
     
    @Override
    public void run(String... args) throws Exception {
   
     
// 配置 InheritableThreadLocalSecurityContextHolderStrategy        
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
// 去获取这个策略对象
        SecurityContextHolderStrategy initStrategy = SecurityContextHolder.getContextHolderStrategy();
        // 将获取到的策略对象用到监听策略中,当委托策略
        SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(
                initStrategy,
                event -> {
   
     
            if(event.getNewContext() != null)
                log.warn("new context->{}",event.getNewContext());
        });
        SecurityContextHolder.setContextHolderStrategy(strategy);
    }
}

测试结果

 

SecurityContextPersistenceFilter 说明

Persistence(持久性)。

【Spring Security 实战(二)】Spring Security的实现原理 中概述了Spring Security 中默认加载的过滤器,SecurityContextPersistenceFilter 即是其中的一员。它的作用是为了存储 SecurityContext 而设计的。

它整体来说做了两件事:

  • 当一个请求到来时,从 HttpSession 中获取 SecurityContext 并存入 SecurityContextHolder 中,这样在同一个请求的后续处理过程中,开发者始终可以通过 SecurityContextHolder 获取到当前登录用户信息。
  • 当一个请求处理完毕时,从 SecurityContextHolder 中获取 SecurityContext 并存入 HttpSession 中(主要针对异步 Servlet,不是异步的相应提交自动就会保存到HttpSession中),方便下一个请求到来时,再从 HttpSession 中拿出来使用,同时擦除 SecurityContextHolder 中的登录用户信息。

下面是SecurityContextPersistenceFilter 过滤器的核心代码(下面出现的 repo 是 SecurityContextRepository 对象,默认是HttpSessionSecurityContextRepository对象):

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {

 
    // 获取 SecurityContext 对象
    HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
    SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
    try {

 
    // 存入到 SecurityContextHolder 中
        SecurityContextHolder.setContext(contextBeforeChainExecution);
        // 让下一个过滤器处理请求
        chain.doFilter(holder.getRequest(), holder.getResponse());
    }
    finally {

 
    // 请求结束后清楚SecurityContextHolder 中的用户信息
    // 并把信息保存在HttpSession中
        SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
        // Crucial removal of SecurityContextHolder contents before anything else.
        SecurityContextHolder.clearContext();
        this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
        request.removeAttribute(FILTER_APPLIED);
    }
}

注意:SecurityContextPersistenceFilter被标记为已过时(Deprecated),但它仍然被包含在Spring Security默认的过滤器链中。这是因为虽然存在一些问题,但它仍然是一个广泛使用的过滤器,并且在某些情况下仍然是有用的。新版本是去拿 PersistentTokenBasedRememberMeServices 去取代它。

二、登录用户数据的获取

通过上面的源码分析呢?咱可以知道如何获取用户信息了(Authentication)。
调用SecurityContextHolder 中的 getContext() 静态方法获取其对应策略中保存的 SecurityContext 对象,再调用 getAuthentication() 方法获取 Authentication 对象。

@RestController
public class TestController {
   
     

    @GetMapping("/test")
    public Object test(){
   
     
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = (User) authentication.getPrincipal();
        /*return "Spring Security Test Success!";*/
        return user;
    }

}

getPrincipal() 是去获取主要的用户信息,它是一个User对象,所以可以进行强转。

测试效果

 
由于我做了如下配置,所以即使在多线程情况下,也是可以使用的(子线程可以用父线程中的 SecurityContext)。

 

三、总结

  • SecurityContextPersistenceFilter 完成了 SecurityContext 的存储和擦除;
  • 在 5.6 版本(准确来说是5.7)后引入了 ListeningSecurityContextHolderStrategy 监听SecurityContext策略;
  • 可以使用 SecurityContextHolder.getContext.getAuthentication() 的方式获取登录用户数据;
  • SecurityContext 的存储和擦除内部用了策略设计模式,SecurityContextHolder 中定义了 SecurityContextHolderStrategy 策略,去获取、擦除、存储SecurityContext。

当使用ListeningSecurityContextHolderStrategy 时,可以向如下这样使用。当然它默认的执行策略是 ThreadLocalSecurity… ,所以当不需要换策略的话直接用监听器对象当构造参数构造即可,如果想切换成多线程,就像如下那样配置吧。

 当然也可以去配置idea的vmoptions参数,但小编并不觉得它是个好主意。你觉得呢?