一文搞懂SecurityContext1 概述
首先需要阐明什么是SecurityContext
,这是著名框架SpringSecurity
中的组件,通过一段时间的研究,我可以很负责的说,在笔者微乎其微的智商水平下,这个框架真的很难懂。
阅读前置知识:
- 了解基本SpringSecurity的身份验证过程
首先我们来看一下这个契约接口所包含的具体功能有哪些?
public interface SecurityContext extends Serializable {/** * Obtains the currently authenticated principal, or an authentication request token. * @return the Authentication
or null
if no authentication * information is available */Authentication getAuthentication();/** * Changes the currently authenticated principal, or removes the authentication * information. * @param authentication the new Authentication
token, or * null
if no further authentication information should be stored */void setAuthentication(Authentication authentication);}
很简单的一个接口,可以看到它主要的功能就是维护Authentication
(官方说法:认证事件)这其中含有用户的相关信息。所以这里我们可以简单下一个定义:存储Authentication
的实例就是安全上下文,也就是本文的重点——SecurityContext
。
接下来简单看一下它究竟是怎么起作用的:
在身份验证完成后,AuthenticationManager
便会将Authentication
实例存入SecurityContext
,而对于我们的业务开发,我们便可以在控制层乃至于业务层去获取这部分用户信息。
2 SecurityContext的管理者
我们可以从接口的定义中观察到,SecurityContext
的主要职责是存储身份验证的对象,但是SecurityContext
又是被怎么管理的呢?我们的SpringSecurity
提供了3种管理策略,其中有这样一个充当管理者的对象——SecurityContextHolder
。
三种工作模式:
- MODE_THREADLOCAL(默认)
- MODE_INHERITABLETHREADLOCAL
- MODE_GLOBAL
在我们开是研究这个管理策略前,先谈一下它究竟该怎么设置?
//最简单的方法:注册这样一个Bean即可@Beanpublic InitializingBean initializingBean(){ return () -> SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);}
MODE_THREADLOCAL模式下允许每个线程在安全上下文中存储自己的信息,前提是每个请求是独立的线程处理,那么这样的话异步处理就成了问题。
做如下测试:
@Componentpublic class AsyncUtil { @Async public void test(){ SecurityContext context = SecurityContextHolder.getContext(); String name = context.getAuthentication().getName(); System.out.println("name = " + name); }}@RestController@RequestMapping("/test")public class IndexController { @Autowired private AsyncUtil asyncUtil; @GetMapping("/hello") public String index(){ SecurityContext context = SecurityContextHolder.getContext(); String name = context.getAuthentication().getName(); System.out.println("name = " + name); asyncUtil.test(); return "你好,服务器" + name; } }
我们在AsyncUtil
中尝试去取Authentication
,可以惊奇的发现:
java.lang.NullPointerException: nullat com.harlon.chapter.utils.AsyncUtil.test(AsyncUtil.java:14) ~[classes/:na]
直接报错,也就直接验证了ThreadLocal
的功效,
此时我们如果改成MODE_INHERITABLETHREADLOCAL便不会报错了,这里介绍一下这种模式的工作流程。
当异步开启线程后,Spring Security
会为新开起的线程复制一份SecurityContext
,但是这里也是有讲究的,我们所创建的线程必须是SpringSecurity
所知道的线程,在本文的最后将会介绍这种情况该怎么处理。
MODE_GLOBAL其实就是所有线程共享的思路,没什么看头了。
需要提一句的是SecurityContext
是非线程安全的,所以如果设置了Global,那我们就需要去关注访问并发问题。
3 自定义转发SecurityContext
⚠️先说结果:
SpringSecurity
提供了以下多种委托对象:
类 | 描述 |
---|---|
DelegatingSecurityContextExecutor | 实现了Executor接口,并被设计用来装饰了Executor对象,使其具有安全上下文转发并创建线程池的能力。 |
DelegatingSecurityContextExecutorService | 实现了ExecutorService接口,并被设计用来装饰ExecutorService对象,和上面作用类似。 |
DelegatingSecurityContextScheduledExecutorService | 实现了ScheduledExecutorService,并被设计用来装饰ScheduledExecutorService对象,和上面作用类似。 |
DelegatingSecurityContextRunnable | 实现了Runnable接口,表示新建线程执行任务并不要求响应的任务,也可以用作传播上下文。 |
DelegatingSecurityContextCallable | 实现了Callable接口,表示新线程执行任务且返回响应的任务,也可以传播。 |
接下来抽几个做测试案例:
3.1 DelegatingSecurityContextCallable
@GetMapping("/callable") public String callable() throws ExecutionException, InterruptedException { Callable task = () -> { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return authentication.getName(); }; ExecutorService executorService = Executors.newCachedThreadPool(); try { var contextCallable = new DelegatingSecurityContextCallable(task); return "callable 测试 : " + executorService.submit(contextCallable).get(); }finally { executorService.shutdown(); } }
正规测试不存在问题,这里简单测一下不适用委托的情况:
@GetMapping("/callable") public String callable() throws ExecutionException, InterruptedException { Callable task = () -> { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return authentication.getName(); }; ExecutorService executorService = Executors.newCachedThreadPool(); try { //var contextCallable = new DelegatingSecurityContextCallable(task); return "callable 测试 : " + executorService.submit(task).get(); }finally { executorService.shutdown(); } }
注释掉装饰器后的结果不难得知:
{ "timestamp":"2022-12-29T12:49:43.617+00:00", "status":500,"error":"Internal Server Error", "path":"/test/callable" }
当然内部也就是空指针了。其实其他几个都很类,这里就不一一尝试了。