Spring Security:安全上下文持有者SecurityContextHolder介绍与Debug分析

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

基于ThreadLocalSecurityContextHolderStrategy实现。

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

基于InheritableThreadLocalSecurityContextHolderStrategy实现。

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);
    }
}

SecurityConfigSpring 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/messageSpring 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分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ITKaven

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

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

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

打赏作者

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

抵扣说明:

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

余额充值