一文搞懂SecurityContext

一文搞懂SecurityContext

1 概述

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

阅读前置知识:

  • 了解基本SpringSecurity的身份验证过程

​ 首先我们来看一下这个契约接口所包含的具体功能有哪些?

public interface SecurityContext extends Serializable {

	/**
	 * Obtains the currently authenticated principal, or an authentication request token.
	 * @return the <code>Authentication</code> or <code>null</code> if no authentication
	 * information is available
	 */
	Authentication getAuthentication();

	/**
	 * Changes the currently authenticated principal, or removes the authentication
	 * information.
	 * @param authentication the new <code>Authentication</code> token, or
	 * <code>null</code> if no further authentication information should be stored
	 */
	void setAuthentication(Authentication authentication);

}

​ 很简单的一个接口,可以看到它主要的功能就是维护Authentication(官方说法:认证事件)这其中含有用户的相关信息。所以这里我们可以简单下一个定义:存储Authentication的实例就是安全上下文,也就是本文的重点——SecurityContext

​ 接下来简单看一下它究竟是怎么起作用的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wslKdKe3-1672318385652)(https://upic-image-bed.oss-cn-beijing.aliyuncs.com/uPic/20221229200453_%E6%9C%AA%E5%91%BD%E5%90%8D%E7%BB%98%E5%9B%BE.drawio.png)]

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

2 SecurityContext的管理者

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

三种工作模式:

  • MODE_THREADLOCAL(默认)
  • MODE_INHERITABLETHREADLOCAL
  • MODE_GLOBAL

在我们开是研究这个管理策略前,先谈一下它究竟该怎么设置?

      //最简单的方法:注册这样一个Bean即可
@Bean
public InitializingBean initializingBean(){
    return () -> SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}

​ MODE_THREADLOCAL模式下允许每个线程在安全上下文中存储自己的信息,前提是每个请求是独立的线程处理,那么这样的话异步处理就成了问题。

​ 做如下测试:

@Component
public 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: null
	at com.harlon.chapter.utils.AsyncUtil.test(AsyncUtil.java:14) ~[classes/:na]

直接报错,也就直接验证了ThreadLocal的功效,

​ 此时我们如果改成MODE_INHERITABLETHREADLOCAL便不会报错了,这里介绍一下这种模式的工作流程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GdrHwnS1-1672318385653)(https://upic-image-bed.oss-cn-beijing.aliyuncs.com/uPic/20221229202754_%E6%9C%AA%E5%91%BD%E5%90%8D%E7%BB%98%E5%9B%BE-2.drawio.png)]

当异步开启线程后,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<String> 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<String> 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"
 }     

当然内部也就是空指针了。其实其他几个都很类,这里就不一一尝试了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沈自在-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值