[SpringSecurity5.2.2源码分析八]:SecurityContextPersistenceFilter

前言

  • 当我们不在其他线程而就在容器创建的线程中使用SecurityContextHolder.getContext()获取SecurityContext的时候,正常都能获取到
  • SecurityContext默认是放在线程中的,所以说在某个地方一定将SecurityContext放到线程中,而这个类就是SecurityContextPersistenceFilter

1、SecurityContextConfigurer

  • SecurityContextConfigurer是SecurityContextPersistenceFilter的配置类,是在获取HttpSecurity的时候默认开启的
  • 这个配置类重点方法就是securityContextRepository(...)方法
    • 是往SharedObject中注册一个SecurityContextRepository
java 复制代码
public SecurityContextConfigurer<H> securityContextRepository(
      SecurityContextRepository securityContextRepository) {
   getBuilder().setSharedObject(SecurityContextRepository.class,
         securityContextRepository);
   return this;
}
  • 先看SharedObject
    • 就是一个HashMap,是在各大过滤器中共享数据的地方
    • 可以通过HttpSecurity.getSharedObjects()操作

1.1 SecurityContextRepository

  • 看源码就是三个方法
java 复制代码
public interface SecurityContextRepository {

   SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);
   
   void saveContext(SecurityContext context, HttpServletRequest request,
         HttpServletResponse response);

   boolean containsContext(HttpServletRequest request);
}
  • loadContext(...):获得当前安全上下文
  • saveContext(...):保存安全上下文
  • containsContext(...):查看指定的Request是否包含当前用户的安全上下文

1.2 HttpSessionSecurityContextRepository

  • SecurityContextRepository只有一个有用的实现,我们看看三大方法的实现:
  • loadContext(...):从HttpSession加载安全上下文
java 复制代码
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
   HttpServletRequest request = requestResponseHolder.getRequest();
   HttpServletResponse response = requestResponseHolder.getResponse();
   //false表示有就获取,没有就返回空HttpSession
   HttpSession httpSession = request.getSession(false);
   //从HttpSession获取安全存储上下文
   SecurityContext context = readSecurityContextFromSession(httpSession);
   if (context == null) {
      //如果没有找到安全上下文,那就创建一个空安全上下文
      context = generateNewContext();
      if (this.logger.isTraceEnabled()) {
         this.logger.trace(LogMessage.format("Created %s", context));
      }
   }
   //创建response包装类,目的是为了更新安全上下文
   SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request,
         httpSession != null, context);
   requestResponseHolder.setResponse(wrappedResponse);
   requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse));
   return context;
}
  • saveContext(...):通过responseWrapper保存上下文
java 复制代码
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
   SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
         SaveContextOnUpdateOrErrorResponseWrapper.class);
   Assert.state(responseWrapper != null, () -> "Cannot invoke saveContext on response " + response
         + ". You must use the HttpRequestResponseHolder.response after invoking loadContext");
   responseWrapper.saveContext(context);
}
  • 我们再看SaveToSessionResponseWrapper.saveContext(...)方法:更新存储在HttpSession中的安全上下文 如果AuthenticationTrustResolver将当前用户识别为匿名用户,则不会存储上下文
java 复制代码
@Override
protected void saveContext(SecurityContext context) {
   //首先获得认证对象
   final Authentication authentication = context.getAuthentication();
   HttpSession httpSession = this.request.getSession(false);
   String springSecurityContextKey = HttpSessionSecurityContextRepository.this.springSecurityContextKey;
   //如果没有认证对象或者是匿名用户
   if (authentication == null
         || HttpSessionSecurityContextRepository.this.trustResolver.isAnonymous(authentication)) {

      //如果是匿名用户和空认证对象那么安全上下文其实已经没有任何意义,如果存在就删除它
      if (httpSession != null && this.authBeforeExecution != null) {
         //删除存储在HttpSession中的安全上下文
         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");
         }
      }
      return;
   }
   //如果为空就创建新的HttpSession
   httpSession = (httpSession != null) ? httpSession : createNewSessionIfAllowed(context, authentication);
   //如果HttpSession存在,存储当前的安全上下文
   //但仅当它在此线程中发生了变化
   if (httpSession != null) {
      //可能是一个新的会话,所以还要检查上下文属性
      if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {
         httpSession.setAttribute(springSecurityContextKey, context);
         this.isSaveContextInvoked = true;
         if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession));
         }
      }
   }
}
  • 我们再回到containsContext(...)方法:此方法主要是SessionManagementFilter中用,后面再讲
java 复制代码
@Override
public boolean containsContext(HttpServletRequest request) {
   HttpSession session = request.getSession(false);
   if (session == null) {
      return false;
   }
   return session.getAttribute(this.springSecurityContextKey) != null;
}

1.3 SecurityContextConfigurer.configure(...)

  • 现在我们回到配置类中,根据SpirngSecurity的建筑者模式,配置类只重写了configure(...)方法
java 复制代码
@Override
@SuppressWarnings("unchecked")
public void configure(H http) {
   //获得HttpSession级别的安全上下文存储策略
   SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
   //如果没有就创建默认的
   if (securityContextRepository == null) {
      securityContextRepository = new HttpSessionSecurityContextRepository();
   }
   //创建过滤器
   SecurityContextPersistenceFilter securityContextFilter = new SecurityContextPersistenceFilter(
         securityContextRepository);
   //从HttpSecurity中获得会话管理配配置类
   SessionManagementConfigurer<?> sessionManagement = http.getConfigurer(SessionManagementConfigurer.class);
   SessionCreationPolicy sessionCreationPolicy = (sessionManagement != null)
         ? sessionManagement.getSessionCreationPolicy() : null;
   //看会话管理配配置类是否允许一直创建Session
   //这样的话SecurityContextPersistenceFilter就直接使用request.getSession()创建session
   if (SessionCreationPolicy.ALWAYS == sessionCreationPolicy) {
      securityContextFilter.setForceEagerSessionCreation(true);
   }
   securityContextFilter = postProcess(securityContextFilter);
   http.addFilter(securityContextFilter);
}
  • 这里唯一没讲的就是通过SessionManagementConfigurer获取SessionCreationPolicy
    • 因为安全上下文存储策略默认只有一个HttpSession的实现
    • 如果是不允许一直创建,那么在SecurityContextPersistenceFilter中获取HttpSession就不会执行下面的代码了
java 复制代码
if (this.forceEagerSessionCreation) {
   HttpSession session = request.getSession();
   if (this.logger.isDebugEnabled() && session.isNew()) {
      this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
   }
}
  • 再看下SessionCreationPolicy的源码
java 复制代码
/**
 * Spring Security的过滤器在执行过程中是否允许创建会话的策略
 * <li>比如说:{@link org.springframework.security.web.context.SecurityContextPersistenceFilter#doFilter(ServletRequest, ServletResponse, FilterChain)}</li>
 */
public enum SessionCreationPolicy {

   /**
    * 总是 {@link HttpSession}
    */
   ALWAYS,

   /**
    * 永远不会创建 {@link HttpSession}, 除非他已经存在
    * 应该不会由Spring Security创建
    */
   NEVER,

   /**
    * 在需要的时候创建 {@link HttpSession}
    */
   IF_REQUIRED,

   /**
    * Spring Security永远不会创建 {@link HttpSession},也永远不会使用它获取 {@link HttpSession}
    */
   STATELESS

}

2、SecurityContextPersistenceFilter

  • 此过滤器是为了从HttpSession级别的安全上下文存储策略中读取安全上下文,然后放到线程级别的安全上下文策略中,方便后面程序操作安全上下文
    • 这里的HttpSession级别的安全上下文存储策略指的是:HttpSessionSecurityContextRepository
    • 这里的线程级别的安全上下文策略指的是:SecurityContextHolderStrategy的实现类
  • 看一下关键方法:doFilter(...):
java 复制代码
   private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
         throws IOException, ServletException {
      //确保过滤器器在每个请求中只执行一次
      if (request.getAttribute(FILTER_APPLIED) != null) {
         chain.doFilter(request, response);
         return;
      }
      //标志本次请求已经执行过当前过滤器
      request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
      //是否允许创建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()));
         }
      }
      HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
      //从HttpSession级别的安全上下文存储策略中尝试获取安全上下文
      SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
      try {
         //设置到线程级别的安全上下文存储策略中
         //方便后续程序的操作
         SecurityContextHolder.setContext(contextBeforeChainExecution);
         if (contextBeforeChainExecution.getAuthentication() == null) {
            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());
      }
      finally {
         //这里是已经执行完Controller的代码

         //先拿到当前用户的线程级别的安全上下文
         SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
         //清空
         SecurityContextHolder.clearContext();
         //由于用户可能修改过线程级别的安全上下文
         //所有重新设置到HttpSession的线程级别的安全上下文策略中
         this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
         request.removeAttribute(FILTER_APPLIED);
         this.logger.debug("Cleared SecurityContextHolder to complete request");
      }
   }
  • 第23行代码就是将上下文放入到SecurityContextHolderStrategy中
  • 然后我们可以看出方法的后半段就是一个try finally的格式
    • 第33行代码就是执行后面的过滤器,而SpringMVC的DispatcherServlet也会在其中执行完毕
    • 所以说一旦开始执行finally方法就代表SecurityContext已经是最新的了,所以说将其重新设置到HttpSesison中
相关推荐
JH30734 小时前
SpringBoot 优雅处理金额格式化:拦截器+自定义注解方案
java·spring boot·spring
qq_12498707537 小时前
基于SSM的动物保护系统的设计与实现(源码+论文+部署+安装)
java·数据库·spring boot·毕业设计·ssm·计算机毕业设计
Coder_Boy_7 小时前
基于SpringAI的在线考试系统-考试系统开发流程案例
java·数据库·人工智能·spring boot·后端
2301_818732067 小时前
前端调用控制层接口,进不去,报错415,类型不匹配
java·spring boot·spring·tomcat·intellij-idea
汤姆yu11 小时前
基于springboot的尿毒症健康管理系统
java·spring boot·后端
暮色妖娆丶11 小时前
Spring 源码分析 单例 Bean 的创建过程
spring boot·后端·spring
biyezuopinvip12 小时前
基于Spring Boot的企业网盘的设计与实现(任务书)
java·spring boot·后端·vue·ssm·任务书·企业网盘的设计与实现
JavaGuide12 小时前
一款悄然崛起的国产规则引擎,让业务编排效率提升 10 倍!
java·spring boot
figo10tf13 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端
zhangyi_viva13 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端