了解SecurityContext

2023-03-06 13:39:53 浏览数 (1)

通常我们在认证后访问需要认证的资源时需要获取当前认证用户的信息。比如 “查询我的个人信息”。如果你直接在接口访问时显式的传入你的 UserID 肯定是不合适的。因为你认证通过后访问资源,系统是知道你是谁的。而且显式的暴露用户的检索接口也不安全。所以我们需要一个业务中可以检索当前认证用户的工具。 接下来我们来看看 Spring Security 是如何解决这个痛点的。

安全上下文 SecurityContext

不知道你有没有留意Spring Security 中 :使用 JWT 认证访问接口中是如何实现 JWT 认证拦截器 JwtAuthenticationFilter 。当服务端对 JWT Token 认证通过后,会将认证用户的信息封装到 UsernamePasswordAuthenticationToken 中 并使用工具类放入安全上下文 SecurityContext 中,当服务端响应用户后又使用同一个工具类将 UsernamePasswordAuthenticationTokenSecurityContextclear 掉。 我们来简单了解 SecurityContext 具体是个什么东西。

阅读前置知识:

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

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

代码语言:javascript复制
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

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

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

SecurityContext的管理者

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

​ 三种工作模式:

    • MODE_THREADLOCAL(默认)
    • MODE_INHERITABLETHREADLOCAL
    • MODE_GLOBAL

SecurityContextHolder

存储策略

这里也扩展面: SecurityContextHolder 是如何存储 SecurityContext 的。SecurityContextHolder 默认有三种存储 SecurityContext 的策略:

  • MODE_THREADLOCAL 利用ThreadLocal 机制来保存每个使用者的 SecurityContext缺省策略,平常我们使用这个就行了。
  • MODE_INHERITABLETHREADLOCAL 利用InheritableThreadLocal 机制来保存每个使用者的 SecurityContext,多用于多线程环境环境下。
  • MODE_GLOBAL 静态机制,作用于全局--不经常用~

这个工具类就是 SecurityContextHolder 。 它提供了两个有用的方法:

  • clearContext 清除当前的 SecurityContext
  • getContext 获取当前的 SecurityContext
  • setContext 设置当前的 SecurityContext

平常我们通过这三个方法来操作安全上下文 SecurityContext 。你可以直接在代码中使用工具类 SecurityContextHolder 获取用户信息,像下面一样:

代码语言:javascript复制
 public String getCurrentUser() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
          
     if (authentication instanceof AnonymousAuthenticationToken){
         return "anonymousUser";
     }
    UserDetails principal = (UserDetails) authentication.getPrincipal();
    return principal.getUsername();
 }

从源码来看很简单就是一个 存储 Authentication 的容器。而 Authentication 是一个用户凭证接口用来作为用户认证的凭证使用,通常常用的实现有 认证用户 UsernamePasswordAuthenticationToken匿名用户AnonymousAuthenticationToken。其中 UsernamePasswordAuthenticationToken 包含了 UserDetails , AnonymousAuthenticationToken 只包含了一个字符串 anonymousUser 作为匿名用户的标识。我们通过 SecurityContext 获取上下文时需要来进行类型判断。接下来我们来聊聊操作 SecurityContext 的工具类。

代码语言:javascript复制
 package org.springframework.security.core.context;
 
 import java.io.Serializable;
 import org.springframework.security.core.Authentication;
 
 public interface SecurityContext extends Serializable {
     Authentication getAuthentication();
 
     void setAuthentication(Authentication var1);
 }

在开始研究这个管理策略前,先谈它究竟该怎么设置?

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

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

代码语言:javascript复制
@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 可以发现:直接报错,也就直接验证了ThreadLocal的功效

代码语言:javascript复制
java.lang.NullPointerException: null
    at com.harlon.chapter.utils.AsyncUtil.test(AsyncUtil.java:14) ~[classes/:na]

此时我们如果改成MODE_INHERITABLETHREADLOCAL便不会报错了,这里介绍一下这种模式的工作流程。 ![EXWJ`83GGOT0UQTZDYG.png](http://blog-dm-01.oss-cn-hangzhou.aliyuncs.com/articles/1c341c4c64f4d09e2b9ff9ee4e579dcc.png)

当异步开启线程后,Spring Security会为新开起的线程复制一份SecurityContext,但是这里也是有讲究的,我们所创建的线程必须是SpringSecurity所知道的线程,在本文的最后将会介绍这种情况该怎么处理。

MODE_GLOBAL其实就是所有线程共享的思路

需要提一句的是SecurityContext是非线程安全的,所以如果设置了Global,那我们就需要去关注访问并发问题。

自定义转发SecurityContext

⚠️结论:

描述

DelegatingSecurityContextExecutor

实现了Executor接口,并被设计用来装饰了Executor对象,使其具有安全上下文转发并创建线程池的能力。

DelegatingSecurityContextExecutorService

实现了ExecutorService接口,并被设计用来装饰ExecutorService对象,和上面作用类似。

DelegatingSecurityContextScheduledExecutorService

实现了ScheduledExecutorService,并被设计用来装饰ScheduledExecutorService对象,和上面作用类似。

DelegatingSecurityContextRunnable

实现了Runnable接口,表示新建线程执行任务并不要求响应的任务,也可以用作传播上下文。

DelegatingSecurityContextCallable

实现了Callable接口,表示新线程执行任务且返回响应的任务,也可以传播。

SecurityContext是非线程安全的哟~

该文转载自大佬 :沈自在????

0 人点赞