[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中
相关推荐
代码之光_198024 分钟前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
戴眼镜的猴2 小时前
Spring Boot的过滤器与拦截器的区别
spring boot
尘浮生2 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
尚学教辅学习资料2 小时前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
morris1313 小时前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
阿伟*rui6 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
paopaokaka_luck8 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
Yaml410 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~10 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong16168810 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端