【Spring Security】 SecurityContext
【Spring Security】 SecurityContext
Metadata
title: 【Spring Security】 SecurityContext
date: 2023-02-05 10:23
tags:
- 行动阶段/完成
- 主题场景/组件
- 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
- 细化主题/Module/SpringSecurity
categories:
- SpringSecurity
keywords:
- SpringSecurity
description: 【Spring Security】 SecurityContext
【Spring Security】 SecurityContext
在 Security 中,登录以后,我们可以通过以下代码获取当前用户的认证信息:
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
在 SecurityContext 中,包含了用户信息、账号状态、拥有的权限等信息:
在获取用户信息的代码中,通过 SecurityContextHolder 获取 SecurityContext ,然后在 SecurityContext 中获取 Authentication ,那么 SecurityContext 就是保存用户认证的地方了,那么这个时候就有几个疑问需要了解:
- SecurityContext 是什么?
- 用户登录后,认证信息保存在哪里?
- 已登录用户访问,是如何获取认证信息的?
- 如何保证返回的是当前登录用户的信息?
带着以上疑问,我们分析下相关源码,进行解析
核心类
1. SecurityContextPersistenceFilter
SecurityContextPersistenceFilter 是负责 SecurityContext 处理的过滤器,主要负责:
- 认证成功后,将 SecurityContext 保存到 Session 中
- 访问时,从 Session 中获取 SecurityContext,设置到当前线程中
2. SecurityContext
SecurityContext 安全上下文,主要是存储了认证信息,我们可以通过获取 SecurityContext,来获取已认证的用户信息,提供了获取和设置 Authentication 的两个方法:
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication var1);
}
它只有一个实现类 SecurityContextImpl,其内部维护了一个 Authentication 对象。
public class SecurityContextImpl implements SecurityContext {
private Authentication authentication;
}
3. SecurityContextRepository
SecurityContextRepository 接口定义了一些 SecurityContext 持久化和查询的方法:
public interface SecurityContextRepository {
// 加载(获取)SecurityContext
SecurityContext loadContext(HttpRequestResponseHolder var1);
// 保存SecurityContext
void saveContext(SecurityContext var1, HttpServletRequest var2, HttpServletResponse var3);
boolean containsContext(HttpServletRequest var1);
}
SecurityContextRepository 只有两个实现类 HttpSessionSecurityContextRepository、NullSecurityContextRepository。
HttpSessionSecurityContextRepository 会将 SecurityContext 保存在 Session 中,获取的时候,从 Session 中查询。
NullSecurityContextRepository 则不会存储 SecurityContext。
4. SecurityContextHolder
SecurityContextHolder 意为 SecurityContex 拥有者,我们可以通过它在代码中来获取 SecurityContex,进而获取当前登陆用户信息。
SecurityContextHolder 有一个重要的属性 SecurityContextHolderStrategy。
private static SecurityContextHolderStrategy strategy;
5. SecurityContextHolderStrategy
SecurityContextRepository 负责持久化存储 SecurityContext,SecurityContextHolder 则负责将持久化的数据,设置到相关使用场景中,方便获取。而 SecurityContextHolderStrategy 就是 SecurityContextHolder 使用策略。
SecurityContextHolderStrategy 定义个获取、删除、设置、创建等操作 SecurityContext 的方法
public interface SecurityContextHolderStrategy {
void clearContext();
SecurityContext getContext();
void setContext(SecurityContext var1);
SecurityContext createEmptyContext();
}
其有三个实现类:
- GlobalSecurityContextHolderStrategy:SecurityContext 为静态属性,所有在该类下的对象共享这一个属性,所以适用于应用从开启到关闭的整个生命周期只有一个用户在使用。
- ThreadLocalSecurityContextHolderStrategy(默认策略):利用 ThreadLocal 保存多个 SecurityContext(安全上下文),每个线程都可以利用 ThreadLocal 获取其自己的 SecurityContext 安全上下文。
- InheritableThreadLocalSecurityContextHolderStrategy:使用 ThreadLocal 的子类 InheritableThreadLocal 来实现,子线程也可以获取到(本来父线程的本地变量是无法传递给子线程的)。
1. 项目初始化阶段
项目在启动过程中,肯定会根据配置类加载各种对象,比如以下配置,我们配置了一些放行路径、处理器、session 创建策略等。
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf,开启跨域支持
http.csrf().disable().cors();
http.authorizeRequests()
.antMatchers( "/sms/send/code", "/sms/login","/permission/**").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.failureHandler(authenticationFailureHandler);
//.successHandler(authenticationSuccessHandler); // 配置登录失败处理器
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 配置DeniedHandler处理器
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
// 添加手机号短信登录
http.apply(smsSecurityConfigurerAdapter);
}
HttpSecurity 配置加载过程中,SessionManagementConfigurer(Session 管理配置类)会对 SecurityContextRepository(安全上下文存储仓库)进行初始化。
public void init(H http) {
// 1. 获取HTTPSecurity 共享对象 SecurityContextRepository
SecurityContextRepository securityContextRepository = (SecurityContextRepository)http.getSharedObject(SecurityContextRepository.class);
boolean stateless = this.isStateless();
// 2. 不存在 则会创建
if (securityContextRepository == null) {
// 2.1 如果设置了不需要,设置SecurityContextRepository为NullSecurityContextRepository
if (stateless) {
http.setSharedObject(SecurityContextRepository.class, new NullSecurityContextRepository());
} else {
// 2.2 需要则会创建HttpSessionSecurityContextRepository(基于Session存储安全上下文)
HttpSessionSecurityContextRepository httpSecurityRepository = new HttpSessionSecurityContextRepository();
// 是否启用会话 URL 重写
httpSecurityRepository.setDisableUrlRewriting(!this.enableSessionUrlRewriting);
// 是否允许创建会话
httpSecurityRepository.setAllowSessionCreation(this.isAllowSessionCreation());
AuthenticationTrustResolver trustResolver = (AuthenticationTrustResolver)http.getSharedObject(AuthenticationTrustResolver.class);
if (trustResolver != null) {
httpSecurityRepository.setTrustResolver(trustResolver);
}
// 设置身份验证信任解析器
http.setSharedObject(SecurityContextRepository.class, httpSecurityRepository);
}
}
// 3. HttpSecurity添加一些共享对象。
RequestCache requestCache = (RequestCache)http.getSharedObject(RequestCache.class);
if (requestCache == null && stateless) {
http.setSharedObject(RequestCache.class, new NullRequestCache());
}
http.setSharedObject(SessionAuthenticationStrategy.class, this.getSessionAuthenticationStrategy(http));
http.setSharedObject(InvalidSessionStrategy.class, this.getInvalidSessionStrategy());
}
上述代码中,创建了 HttpSessionSecurityContextRepository 对象用于存储 SecurityContext 在 Session 中,在使用无参构造器 new 这个对象的过程中,学过 JVM 的应该知道,在调用构造器之前成员属性会先进行初始化,而在这个类中,有下面这样一个属性:
private final Object contextObject = SecurityContextHolder.createEmptyContext();
contextObject 属性会调用 SecurityContextHolder(安全上下文拥有者)来创建一个空的上下文,而在调用 SecurityContextHolder 类的时候,因为他有一个静态代码块,所以会执行 static 代码块进行初始化:
static {
initialize();
}
代码块中的 initialize 方法主要是创建 SecurityContextHolder 相关的策略。
private static void initialize() {
// 1. SecurityContext 策略名称不存在,则设置为MODE_THREADLOCAL(THREADLOCAL模式)
if (!StringUtils.hasText(strategyName)) {
strategyName = "MODE_THREADLOCAL";
}
// 2. 创建ThreadLocalSecurityContextHolderStrategy(ThreadLocal安全上下文策略)
if (strategyName.equals("MODE_THREADLOCAL")) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
// 3. InheritableThreadLocal 策略()
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_GLOBAL")) {
// 4. 全局策略
strategy = new GlobalSecurityContextHolderStrategy();
} else {
try {
// 5. 自定义策略
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
} catch (Exception var2) {
ReflectionUtils.handleReflectionException(var2);
}
}
// 6. 计数器initializeCount +1
++initializeCount;
}
静态代码块执行完成后,HttpSessionSecurityContextRepository 开始初始化成员属性 contextObject :
private final Object contextObject = SecurityContextHolder.createEmptyContext();
createEmptyContext 方法会创建一个空的 SecurityContext,调用的是 ThreadLocalSecurityContextHolderStrategy 中的方法。
public static SecurityContext createEmptyContext() {
return strategy.createEmptyContext();
}
ThreadLocalSecurityContextHolderStrategy 直接 new 了一个 SecurityContextImpl 对象。
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
SecurityContextImpl 类实现了 SecurityContext,它包含了一个 Authentication 对象。
public class SecurityContextImpl implements SecurityContext {
private static final long serialVersionUID = 550L;
private Authentication authentication;
}
至此关于 HttpSessionSecurityContextRepository 的初始化就已经完成了。
2. 登录处理 SecurityContext
我们知道 Security 登录和访问,都是由过滤器链来完成的,SecurityContext 处理是由 SecurityContextPersistenceFilter(SecurityContext 持久化过滤器)来完成的。
用户输入账号密码后,进入 SecurityContextPersistenceFilter,执行以下过滤方法:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 1. 判断是否已执行当前逻辑,已执行则直接放行
if (request.getAttribute("__spring_security_scpf_applied") != null) {
chain.doFilter(request, response);
} else {
// 2. 设置已执行标记
request.setAttribute("__spring_security_scpf_applied", Boolean.TRUE);
// 3. 如果启用了并发会话控制,获取Session
if (this.forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (this.logger.isDebugEnabled() && session.isNew()) {
this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
}
}
// 4. 创建HttpRequestResponseHolder (理解为包含了request、response的一个对象)
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
// 5. 存储库中,获取SecurityContext
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
boolean var10 = false;
try {
var10 = true;
// 6. SecurityContextHolder设置Context(这里SecurityContext是未认证的)
SecurityContextHolder.setContext(contextBeforeChainExecution);
if (contextBeforeChainExecution.getAuthentication() == null) {
this.logger.debug("Set SecurityContextHolder to empty SecurityContext");
} else if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
}
chain.doFilter(holder.getRequest(), holder.getResponse());
var10 = false;
// 7. 经过其他过滤器后(认证完成),后续最终处理
} finally {
if (var10) {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute("__spring_security_scpf_applied");
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
// 8.获取SecurityContext
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// 9. 清理SecurityContext
SecurityContextHolder.clearContext();
// 10. 新的SecurityContext 保存到存储库中
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute("__spring_security_scpf_applied");
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
在过滤器中,会在存储库中,获取 SecurityContext,这个方法主要是 Session 中获取 SecurityContext ,没有则会创建一个空的,并设置到 Session 中。
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
// 1. 获取request 、response、httpSession
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
HttpSession httpSession = request.getSession(false);
// 2. 从Session 中获取键为SPRING_SECURITY_CONTEXT的属性
SecurityContext context = this.readSecurityContextFromSession(httpSession);
// 3. 登录时,是没有的,所以会创建一个空的SecurityContext (SecurityContextImpl)
if (context == null) {
context = this.generateNewContext();
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Created %s", context));
}
}
// 4. 设置新的request 、response、httpSession(SPRING_SECURITY_CONTEXT)
HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper wrappedResponse = new HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper(response, request, httpSession != null, context);
requestResponseHolder.setResponse(wrappedResponse);
requestResponseHolder.setRequest(new HttpSessionSecurityContextRepository.SaveToSessionRequestWrapper(request, wrappedResponse));
return context;
}
接下来看下过滤器中使用 SecurityContextHolder 的 getContext 方法是如何执行的:
// getContext或从SecurityContextHolder的存储库中获取Context
public static SecurityContext getContext() {
return strategy.getContext();
}
因为模式使用的是 ThreadLocalSecurityContextHolderStrategy,所以会 ThreadLocal 中获取 SecurityContext,清理方法也是直接移除当前 ThreadLocal 中的信息:
public SecurityContext getContext() {
SecurityContext ctx = (SecurityContext)contextHolder.get();
if (ctx == null) {
ctx = this.createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
最后看下 saveContext 是如何存储 SecurityContext 的,saveContext 最终调用的是 HttpSessionSecurityContextRepository 内部类 SaveToSessionResponseWrapper 中的方法,
protected void saveContext(SecurityContext context) {
// 1. 获取认证对象,Session、Key(SPRING_SECURITY_CONTEXT)
Authentication authentication = context.getAuthentication();
HttpSession httpSession = this.request.getSession(false);
String springSecurityContextKey = HttpSessionSecurityContextRepository.this.springSecurityContextKey;
if (authentication != null && !HttpSessionSecurityContextRepository.this.trustResolver.isAnonymous(authentication)) {
httpSession = httpSession != null ? httpSession : this.createNewSessionIfAllowed(context, authentication);
if (httpSession != null && (this.contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null)) {
// 2. 设置到Session中
httpSession.setAttribute(springSecurityContextKey, context);
this.isSaveContextInvoked = true;
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession));
}
}
} else {
if (httpSession != null && this.authBeforeExecution != null) {
httpSession.removeAttribute(springSecurityContextKey);
this.isSaveContextInvoked = true;
}
if (this.logger.isDebugEnabled()) {
if (authentication == null) {
this.logger.debug("Did not store empty SecurityContext");
} else {
this.logger.debug("Did not store anonymous SecurityContext");
}
}
}
}
至此,整个登录处理 SecurityContext 流程就结束了,总计以下:
- 进入 SecurityContextPersistenceFilter 过滤器,创建一个空的 SecurityContext
- 后续过滤器认证通过后,在当前线程获取 SecurityContext
- 将 SecurityContext 保存到 Session 中
3. 再次访问,获取 SecurityContext 分析
登录成功后,访问接口,依然是经过了 SecurityContextPersistenceFilter 过滤器。只是因为登录时,将 SecurityContext 保存到了 Session 中,所以过滤器中的以下方法会直接获取到认证过的 SecurityContext :
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
然后 SecurityContextHolder 会将 SecurityContext 设置到 ThreadLocal 中,那么当前线程,无论在什么地方,都可以获取到当前用户的 SecurityContext 中的认证信息了。
SecurityContextHolder.setContext(contextBeforeChainExecution);