SecurityContextHolder
将给定的SecurityContext
与当前执行线程相关联,此类提供了一系列委托给SecurityContextHolderStrategy
实例的静态方法,此类的目的是提供一种方便的方法来指定应该用于给定JVM
的策略(持有SecurityContext
的策略),这是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();
}
/**
* 主要出于故障排除目的
* 此方法显示该类重新初始化SecurityContextHolderStrategy的次数
*/
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++;
}
/**
* 将新的SecurityContext与当前执行线程相关联
*/
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
/**
* 更改策略
* 不要为给定的JVM多次调用此方法,因为它会重新初始化策略并对使用旧策略的任何现有线程产生不利影响
*/
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);
/**
* 首次创建新上下文时,创建一个新的空上下文实现
* 供SecurityContextRepository实现使用
*/
SecurityContext createEmptyContext();
}
SecurityContextHolderStrategy
接口有三个实现类,如下图所示,分别对应SecurityContextHolder
类中的三种模式设置。
ThreadLocalSecurityContextHolderStrategy
基于ThreadLocal
的SecurityContextHolderStrategy
实现。
final class ThreadLocalSecurityContextHolderStrategy implements
SecurityContextHolderStrategy {
// 使用ThreadLocal持有SecurityContext实例
private static final ThreadLocal<SecurityContext> 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<SecurityContext> 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
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kaven</groupId>
<artifactId>security</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
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
分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。