一文搞懂SecurityContext

打印 上一主题 下一主题

主题 672|帖子 672|积分 2016

一文搞懂SecurityContext

1 概述

​        首先需要阐明什么是SecurityContext,这是著名框架SpringSecurity 中的组件,通过一段时间的研究,我可以很负责的说,在笔者微乎其微的智商水平下,这个框架真的很难懂。
阅读前置知识:

  • 了解基本SpringSecurity的身份验证过程
​        首先我们来看一下这个契约接口所包含的具体功能有哪些?
  1. public interface SecurityContext extends Serializable {
  2.         /**
  3.          * Obtains the currently authenticated principal, or an authentication request token.
  4.          * @return the Authentication or null if no authentication
  5.          * information is available
  6.          */
  7.         Authentication getAuthentication();
  8.         /**
  9.          * Changes the currently authenticated principal, or removes the authentication
  10.          * information.
  11.          * @param authentication the new Authentication token, or
  12.          * null if no further authentication information should be stored
  13.          */
  14.         void setAuthentication(Authentication authentication);
  15. }
复制代码
​        很简单的一个接口,可以看到它主要的功能就是维护Authentication(官方说法:认证事件)这其中含有用户的相关信息。所以这里我们可以简单下一个定义:存储Authentication的实例就是安全上下文,也就是本文的重点——SecurityContext。
​        接下来简单看一下它究竟是怎么起作用的:

​        在身份验证完成后,AuthenticationManager便会将Authentication实例存入SecurityContext,而对于我们的业务开发,我们便可以在控制层乃至于业务层去获取这部分用户信息。
2 SecurityContext的管理者

​        我们可以从接口的定义中观察到,SecurityContext的主要职责是存储身份验证的对象,但是SecurityContext又是被怎么管理的呢?我们的SpringSecurity提供了3种管理策略,其中有这样一个充当管理者的对象——SecurityContextHolder。
​        三种工作模式:

  • MODE_THREADLOCAL(默认)
  • MODE_INHERITABLETHREADLOCAL
  • MODE_GLOBAL
在我们开是研究这个管理策略前,先谈一下它究竟该怎么设置?
  1.       //最简单的方法:注册这样一个Bean即可
  2. @Bean
  3. public InitializingBean initializingBean(){
  4.     return () -> SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
  5. }
复制代码
​        MODE_THREADLOCAL模式下允许每个线程在安全上下文中存储自己的信息,前提是每个请求是独立的线程处理,那么这样的话异步处理就成了问题。
​        做如下测试:
  1. @Component
  2. public class AsyncUtil {
  3.     @Async
  4.     public void test(){
  5.         SecurityContext context = SecurityContextHolder.getContext();
  6.         String name = context.getAuthentication().getName();
  7.         System.out.println("name = " + name);
  8.     }
  9. }
  10. @RestController
  11. @RequestMapping("/test")
  12. public class IndexController {
  13.     @Autowired
  14.     private AsyncUtil asyncUtil;
  15.     @GetMapping("/hello")
  16.     public String index(){
  17.         SecurityContext context = SecurityContextHolder.getContext();
  18.         String name = context.getAuthentication().getName();
  19.         System.out.println("name = " + name);
  20.         asyncUtil.test();
  21.         return "你好,服务器" + name;
  22.     }   
  23. }
复制代码
​        我们在AsyncUtil 中尝试去取Authentication,可以惊奇的发现:
  1. java.lang.NullPointerException: null
  2.         at 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
  1.     @GetMapping("/callable")
  2.     public String callable() throws ExecutionException, InterruptedException {
  3.         Callable<String> task = () -> {
  4.             Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  5.             return authentication.getName();
  6.         };
  7.         ExecutorService executorService = Executors.newCachedThreadPool();
  8.         try {
  9.             var contextCallable = new DelegatingSecurityContextCallable<>(task);
  10.             return "callable 测试 : " + executorService.submit(contextCallable).get();
  11.         }finally {
  12.             executorService.shutdown();
  13.         }
  14.     }
复制代码
正规测试不存在问题,这里简单测一下不适用委托的情况:
  1. @GetMapping("/callable")
  2.     public String callable() throws ExecutionException, InterruptedException {
  3.         Callable<String> task = () -> {
  4.             Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  5.             return authentication.getName();
  6.         };
  7.         ExecutorService executorService = Executors.newCachedThreadPool();
  8.         try {
  9.             //var contextCallable = new DelegatingSecurityContextCallable<>(task);
  10.             return "callable 测试 : " + executorService.submit(task).get();
  11.         }finally {
  12.             executorService.shutdown();
  13.         }
  14.     }
复制代码
注释掉装饰器后的结果不难得知:
  1. {
  2.    "timestamp":"2022-12-29T12:49:43.617+00:00",
  3.    "status":500,"error":"Internal Server Error",
  4.    "path":"/test/callable"
  5. }     
复制代码
当然内部也就是空指针了。其实其他几个都很类,这里就不一一尝试了。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

曂沅仴駦

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表