【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 流程就结束了,总计以下:

  1. 进入 SecurityContextPersistenceFilter 过滤器,创建一个空的 SecurityContext
  2. 后续过滤器认证通过后,在当前线程获取 SecurityContext
  3. 将 SecurityContext 保存到 Session 中

3. 再次访问,获取 SecurityContext 分析

登录成功后,访问接口,依然是经过了 SecurityContextPersistenceFilter 过滤器。只是因为登录时,将 SecurityContext 保存到了 Session 中,所以过滤器中的以下方法会直接获取到认证过的 SecurityContext :

SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);

然后 SecurityContextHolder 会将 SecurityContext 设置到 ThreadLocal 中,那么当前线程,无论在什么地方,都可以获取到当前用户的 SecurityContext 中的认证信息了。

SecurityContextHolder.setContext(contextBeforeChainExecution);