Spring Security:安全上下文持有者SecurityContextHold
SecurityContextHolder
将给定的SecurityContext与当前执行线程相关联,此类提供了一系列委托给SecurityContextHolderStrategy实例的静态方法,此类的目的是提供一种方便的方法来指定应该用于给定JVM的策略,这是JVM范围的设置,因为此类中的所有内容都是static修饰,方便调用。
要指定使用哪种策略,必须提供模式设置,模式设置是定义为static final字段的三个有效设置之一,或者是提供公共无参构造方法的SecurityContextHolderStrategy具体实现的完全限定类名(会通过反射进行创建)。有两种方法可以指定所需的策略模式,第一种是通过键入SYSTEM_PROPERTY的系统属性来指定它,第二种是在使用类之前调用setStrategyName(String)进行设置。如果这两种方法都没有使用,则该类将默认使用MODE_THREADLOCAL,它向后兼容,具有较少的JVM不兼容性并且适用于服务器(而MODE_GLOBAL不适合服务器使用)。
SecurityContextHolder类源码
public class SecurityContextHolder { // 三种模式设置,代表三种策略 public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL"; public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL"; public static final String MODE_GLOBAL = "MODE_GLOBAL"; // 系统属性 public static final String SYSTEM_PROPERTY = "spring.security.strategy"; // 获取系统属性spring.security.strategy的值 private static String strategyName = System.getProperty(SYSTEM_PROPERTY); // 安全上下文持有策略 private static SecurityContextHolderStrategy strategy; // 初始化SecurityContextHolderStrategy的次数 private static int initializeCount = 0; static { // 初始化 initialize(); } public static void clearContext() { strategy.clearContext(); } public static SecurityContext getContext() { return strategy.getContext(); } public static int getInitializeCount() { return initializeCount; } // 初始化方法,私有方法 private static void initialize() { // 如果strategyName属性没有值 if (!StringUtils.hasText(strategyName)) { // 设置默认值MODE_THREADLOCAL strategyName = MODE_THREADLOCAL; } // 根据strategyName的值,设置strategy属性 // 即三种模式设置,代表三种策略,默认为ThreadLocalSecurityContextHolderStrategy if (strategyName.equals(MODE_THREADLOCAL)) { strategy = ne ThreadLocalSecurityContextHolderStrategy(); } else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) { strategy = ne InheritableThreadLocalSecurityContextHolderStrategy(); } else if (strategyName.equals(MODE_GLOBAL)) { strategy = ne GlobalSecurityContextHolderStrategy(); } else { // 尝试加载自定义策略 try { Class> clazz = Class.forName(strategyName); Constructor> customStrategy = clazz.getConstructor(); strategy = (SecurityContextHolderStrategy) customStrategy.neInstance(); } catch (Exception ex) { ReflectionUtils.handleReflectionException(ex); } } // 更新initializeCount initializeCount++; } public static void setContext(SecurityContext context) { strategy.setContext(context); } public static void setStrategyName(String strategyName) { SecurityContextHolder.strategyName = strategyName; // 调用初始化方法 initialize(); } public static SecurityContextHolderStrategy getContextHolderStrategy() { return strategy; } public static SecurityContext createEmptyContext() { return strategy.createEmptyContext(); } @Override public String toString() { return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount=" + initializeCount + "]"; } }SecurityContextHolderStrategy
针对线程存储安全上下文信息的策略,首选策略由SecurityContextHolder加载。
public interface SecurityContextHolderStrategy { void clearContext(); SecurityContext getContext(); void setContext(SecurityContext context); SecurityContext createEmptyContext(); }
SecurityContextHolderStrategy接口有三个实现类,如下图所示,分别对应SecurityContextHolder类中的三种模式设置。
基于ThreadLocal的SecurityContextHolderStrategy实现。
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { // 使用ThreadLocal持有SecurityContext实例 private static final ThreadLocalInheritableThreadLocalSecurityContextHolderStrategycontextHolder = ne ThreadLocal<>(); // 删除ThreadLocal持有的SecurityContext实例 public void clearContext() { contextHolder.remove(); } // 获取ThreadLocal持有的SecurityContext实例 public SecurityContext getContext() { SecurityContext ctx = contextHolder.get(); // 如果为null // 会创建一个空上下文,并且设置到ThreadLocal中 if (ctx == null) { ctx = createEmptyContext(); contextHolder.set(ctx); } return ctx; } // 将SecurityContext实例设置到ThreadLocal中 public void setContext(SecurityContext context) { Assert.notNull(context, "only non-null SecurityContext instances are permitted"); contextHolder.set(context); } // 创建一个空上下文 public SecurityContext createEmptyContext() { // 调用SecurityContextImpl的无参构造方法,实例的authentication属性为null return ne SecurityContextImpl(); } }
基于InheritableThreadLocal的SecurityContextHolderStrategy实现。
InheritableThreadLocal类扩展了ThreadLocal类,以提供从父线程到子线程的值继承当创建子线程时,子线程接收父线程具有值的所有可继承线程局部变量的初始值。
final class InheritableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { // 使用ThreadLocal持有SecurityContext实例,但该ThreadLocal是一个InheritableThreadLocal实例 private static final ThreadLocalGlobalSecurityContextHolderStrategycontextHolder = ne InheritableThreadLocal<>(); // 删除ThreadLocal持有的SecurityContext实例 public void clearContext() { contextHolder.remove(); } // 获取ThreadLocal持有的SecurityContext实例 public SecurityContext getContext() { SecurityContext ctx = contextHolder.get(); // 如果为null // 会创建一个空上下文,并且设置到ThreadLocal中 if (ctx == null) { ctx = createEmptyContext(); contextHolder.set(ctx); } return ctx; } // 将SecurityContext实例设置到ThreadLocal中 public void setContext(SecurityContext context) { Assert.notNull(context, "only non-null SecurityContext instances are permitted"); contextHolder.set(context); } // 创建一个空上下文 public SecurityContext createEmptyContext() { // 调用SecurityContextImpl的无参构造方法,实例的authentication属性为null return ne SecurityContextImpl(); } }
基于static字段的SecurityContextHolderStrategy实现,这意味着JVM中的所有实例共享相同的SecurityContext。
final class GlobalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { // 将SecurityContext实例定义成一个静态属性 private static SecurityContext contextHolder; // 清除上下文,即设置contextHolder属性为null public void clearContext() { contextHolder = null; } // 获取上下文 public SecurityContext getContext() { // 如果contextHolder属性为null // 将创建一个空上下文赋值给它 if (contextHolder == null) { contextHolder = ne SecurityContextImpl(); } return contextHolder; } // 设置上下文 public void setContext(SecurityContext context) { Assert.notNull(context, "only non-null SecurityContext instances are permitted"); contextHolder = context; } // 创建一个空上下文 public SecurityContext createEmptyContext() { return ne SecurityContextImpl(); } }Debug分析
项目结构图
pom.xml
4.0.0 .kaven security1.0-SNAPSHOT .springframeork.boot spring-boot-starter-parent2.3.6.RELEASE 8 8 .springframeork.boot spring-boot-starter-eb.springframeork.boot spring-boot-starter-security.projectlombok lombok
application.yml
spring: security: user: name: kaven passord: itkaven roles: USER logging: level: : springframeork: security: DEBUG
MessageController(定义接口)
package .kaven.security.controller; import .springframeork.eb.bind.annotation.GetMapping; import .springframeork.eb.bind.annotation.RestController; @RestController public class MessageController { @GetMapping("/message") public String getMessage() { return "hello spring security"; } }
启动类
package .kaven.security; import .springframeork.boot.SpringApplication; import .springframeork.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); } }
SecurityConfig(Spring Security的配置类,不是必须的,因为有默认的配置)
package .kaven.security.config; import .springframeork.security.config.Customizer; import .springframeork.security.config.annotation.eb.builders.HttpSecurity; import .springframeork.security.config.annotation.eb.configuration.EnableWebSecurity; import .springframeork.security.config.annotation.eb.configuration.WebSecurityConfigurerAdapter; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) thros Exception { // 任何请求都需要进行验证 http.authorizeRequests() .anyRequest() .authenticated() .and() // 记住身份验证 .rememberMe(Customizer.ithDefaults()) // 基于表单登陆的身份验证方式 .formLogin(Customizer.ithDefaults()); } }
Debug方式启动应用,访问http://localhost:8080/message,Spring Security会通过SecurityContextHolder创建空SecurityContextImpl实例(实例的authentication属性为空)。
SecurityContextHolder使用ThreadLocalSecurityContextHolderStrategy实例(默认策略)创建空SecurityContextImpl实例(实例的authentication属性为空)。
然后AnonymousAuthenticationFilter会处理请求(当前是匿名访问资源),该过滤器会设置该空SecurityContextImpl实例的authentication属性为AnonymousAuthenticationToken实例。
而通过身份验证(authenticated属性为true)的AnonymousAuthenticationToken实例(身份验证请求令牌),也是没有权限访问/message接口的,所以请求被拒绝访问了。
之后请求会被重定向到表单登陆页面,需要填写用户名和密码进行身份验证。
点击登陆后,又会创建一个空SecurityContextImpl实例(实例的authentication属性为空)。
之后会设置该空SecurityContextImpl实例的authentication属性为UsernamePassordAuthenticationToken实例,并且该UsernamePassordAuthenticationToken实例的authenticated属性为true(通过身份验证)。
控制台的日志也能说明身份验证成功了。
请求也成功访问到了/message接口。
安全上下文持有者SecurityContextHolder介绍与Debug分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。