将给定的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 = new ThreadLocalSecurityContextHolderStrategy();
}
else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
}
else if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
}
else {
// 尝试加载自定义策略
try {
Class> clazz = Class.forName(strategyName);
Constructor> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
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 ThreadLocal contextHolder = new 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 new SecurityContextImpl();
}
}
InheritableThreadLocalSecurityContextHolderStrategy
基于InheritableThreadLocal的SecurityContextHolderStrategy实现。
InheritableThreadLocal类扩展了ThreadLocal类,以提供从父线程到子线程的值继承:当创建子线程时,子线程接收父线程具有值的所有可继承线程局部变量的初始值。
final class InheritableThreadLocalSecurityContextHolderStrategy implements
SecurityContextHolderStrategy {
// 使用ThreadLocal持有SecurityContext实例,但该ThreadLocal是一个InheritableThreadLocal实例
private static final ThreadLocal contextHolder = new 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 new SecurityContextImpl();
}
}
GlobalSecurityContextHolderStrategy
基于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 = new SecurityContextImpl();
}
return contextHolder;
}
// 设置上下文
public void setContext(SecurityContext context) {
Assert.notNull(context, "only non-null SecurityContext instances are permitted");
contextHolder = context;
}
// 创建一个空上下文
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
Debug分析
项目结构图:
pom.xml:
4.0.0 com.kaven security 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.3.6.RELEASE 8 8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.projectlombok lombok
application.yml:
spring:
security:
user:
name: kaven
password: itkaven
roles: USER
logging:
level:
org:
springframework:
security: DEBUG
MessageController(定义接口):
package com.kaven.security.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MessageController {
@GetMapping("/message")
public String getMessage() {
return "hello spring security";
}
}
启动类:
package com.kaven.security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
SecurityConfig(Spring Security的配置类,不是必须的,因为有默认的配置):
package com.kaven.security.config;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 任何请求都需要进行验证
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
// 记住身份验证
.rememberMe(Customizer.withDefaults())
// 基于表单登陆的身份验证方式
.formLogin(Customizer.withDefaults());
}
}
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属性为UsernamePasswordAuthenticationToken实例,并且该UsernamePasswordAuthenticationToken实例的authenticated属性为true(通过身份验证)。
控制台的日志也能说明身份验证成功了。
请求也成功访问到了/message接口。
安全上下文持有者SecurityContextHolder介绍与Debug分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。



