SecurityContextPersistenceFilter 过滤器链

简介: SecurityContextPersistenceFilter 过滤器链

SecurityContextPersistenceFilter

SecurityContextPersistenceFilter是Springsecurity链中第二道防线,位于WebAsyncManagerIntegrationFilter之后,作用是为了存储SecurityContext而设计的。

SecurityContextPersistenceFilter主要做两件事:

  1. 当请求到来时,从HttpSession中获取SecurityContext并存入SecurityContextHolder中,这样在同一个请求的后续处理过程中,通过SecurityContextHolder获取数据
  2. 当一个请求处理完毕时,从SecurityContextHolder中获取SecurityContext并存入HttpSession中,方便下一个请求到来时,再从HTTPSession中拿来使用,同时擦除SecurityContextHolder中的登录信息。

将SpringContext存入HttpSession,或者从HttpSession中加载数据转化为SpringContext对象,这些事情都是由SecurityContextRepository的实现类完成。

SecurityContextRepository接口有三个实现类:

  • TestSecurityContextRepository:为单元测试提供支持
  • NullSecurityContextRepository:未做SpringContext的存储工作
  • HttpSessionSecurityContextRepository:Springsecurity的默认使用类,实现了将SpringContext存储到HttpSession中以及从HttpSession中加载SecurityContext出来。

HttpSessionSecurityContextRepository

HttpSessionSecurityContextRepository定义的关于请求和封装的两个内部类

SaveToSessionResponseWrapper

它是对HttpServletResponse的扩展,

  1. HttpServletResponseWrapper实现了HttpServletResponse接口,它是HttpServletResponse的装饰类,利用HttpServletResponseWrapper可以方便操作参数和输出流等。
  2. OnCommittedResponseWrapper 继承HttpServletResponseWrapper,对其功能进行了增强,当HttpServletResponse的sendError sendRedirect flushBuffer等方法被调用的时候,doOnResponseCommitted()方法会被调用,这个方法里可以做一些数据的保存操作。OnCommittedResponseWrapper中的onResponseCommitted是抽象方法,实现类在SaveContextOnUpdateOrErrorResponseWrapper中
  3. SaveContextOnUpdateOrErrorResponseWrapper 继承OnCommittedResponseWrapper,并对onResponseCommitted方法做了实现。在SaveContextOnUpdateOrErrorResponseWrapper中声明一个contextSaved变量,表示SecurityContext是否已经存储成功。当HttpServletResponse提交时,会调用onResponseCommitted方法,在onResponseCommitted中调用saveContext方法,并将contextSaved设置为true,表示已经存储。saveContext是抽闲方法,在SaveToSessionResponseWrapper中实现。
  4. SaveToSessionResponseWrapper

      final class SaveToSessionResponseWrapper extends SaveContextOnUpdateOrErrorResponseWrapper {
          private final HttpServletRequest request;
          private final boolean httpSessionExistedAtStartOfRequest;
          private final SecurityContext contextBeforeExecution;
          private final Authentication authBeforeExecution;
      
          SaveToSessionResponseWrapper(HttpServletResponse response, HttpServletRequest request, boolean httpSessionExistedAtStartOfRequest, SecurityContext context) {
              super(response, HttpSessionSecurityContextRepository.this.disableUrlRewriting);
              this.request = request;
              this.httpSessionExistedAtStartOfRequest = httpSessionExistedAtStartOfRequest;
              this.contextBeforeExecution = context;
              this.authBeforeExecution = context.getAuthentication();
          }
      
          protected void saveContext(SecurityContext context) {
              Authentication authentication = context.getAuthentication();
              HttpSession httpSession = this.request.getSession(false);
              if (authentication != null && !HttpSessionSecurityContextRepository.this.trustResolver.isAnonymous(authentication)) {
                  if (httpSession == null) {
                      httpSession = this.createNewSessionIfAllowed(context);
                  }
      
                  if (httpSession != null && (this.contextChanged(context) || httpSession.getAttribute(HttpSessionSecurityContextRepository.this.springSecurityContextKey) == null)) {
                      httpSession.setAttribute(HttpSessionSecurityContextRepository.this.springSecurityContextKey, context);
                      if (HttpSessionSecurityContextRepository.this.logger.isDebugEnabled()) {
                          HttpSessionSecurityContextRepository.this.logger.debug("SecurityContext '" + context + "' stored to HttpSession: '" + httpSession);
                      }
                  }
      
              } else {
                  if (HttpSessionSecurityContextRepository.this.logger.isDebugEnabled()) {
                      HttpSessionSecurityContextRepository.this.logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.");
                  }
      
                  if (httpSession != null && this.authBeforeExecution != null) {
                      httpSession.removeAttribute(HttpSessionSecurityContextRepository.this.springSecurityContextKey);
                  }
      
              }
          }
      
          private boolean contextChanged(SecurityContext context) {
              return context != this.contextBeforeExecution || context.getAuthentication() != this.authBeforeExecution;
          }
      
          private HttpSession createNewSessionIfAllowed(SecurityContext context) {
              if (HttpSessionSecurityContextRepository.this.isTransientAuthentication(context.getAuthentication())) {
                  return null;
              } else if (this.httpSessionExistedAtStartOfRequest) {
                  if (HttpSessionSecurityContextRepository.this.logger.isDebugEnabled()) {
                      HttpSessionSecurityContextRepository.this.logger.debug("HttpSession is now null, but was not null at start of request; session was invalidated, so do not create a new session");
                  }
      
                  return null;
              } else if (!HttpSessionSecurityContextRepository.this.allowSessionCreation) {
                  if (HttpSessionSecurityContextRepository.this.logger.isDebugEnabled()) {
                      HttpSessionSecurityContextRepository.this.logger.debug("The HttpSession is currently null, and the " + HttpSessionSecurityContextRepository.class.getSimpleName() + " is prohibited from creating an HttpSession (because the allowSessionCreation property is false) - SecurityContext thus not stored for next request");
                  }
      
                  return null;
              } else if (HttpSessionSecurityContextRepository.this.contextObject.equals(context)) {
                  if (HttpSessionSecurityContextRepository.this.logger.isDebugEnabled()) {
                      HttpSessionSecurityContextRepository.this.logger.debug("HttpSession is null, but SecurityContext has not changed from default empty context: ' " + context + "'; not creating HttpSession or storing SecurityContext");
                  }
      
                  return null;
              } else {
                  if (HttpSessionSecurityContextRepository.this.logger.isDebugEnabled()) {
                      HttpSessionSecurityContextRepository.this.logger.debug("HttpSession being created as SecurityContext is non-default");
                  }
      
                  try {
                      return this.request.getSession(true);
                  } catch (IllegalStateException var3) {
                      HttpSessionSecurityContextRepository.this.logger.warn("Failed to create a session, as response has been committed. Unable to store SecurityContext.");
                      return null;
                  }
              }
          }
      }

saveContext方法:主要用来保存SecurityContext,如果authentication对象为null或者它是一个匿名对象,则不需要保存SecurityContext;同时如果httpSession不为null并且authBeforeExecution不为null,就从httpSession中将保存的登录用户数据移除,主要是为了防止开发者在注销成功的回调中继续调用chain.doFilter方法,进而导致原始的登录信息无法清除;如果httpSession为null,则去创建一个HttpSession对象;最后,如果SpringContext发生了变化,或者httpSession中没有保存SpringContext,则调用httpSession中的setAttribute方法将SpringContext保存起来。

这就是HttpSessionSecurityContextRepository封装的SaveToSessionResponseWrapper对象,一个核心功能就是在HttpServletResponse提交的时候,将SecurityContext保存到HttpSession中。

SaveToSessionRequestWrapper

private static class SaveToSessionRequestWrapper extends HttpServletRequestWrapper {
    private final SaveContextOnUpdateOrErrorResponseWrapper response;

    SaveToSessionRequestWrapper(HttpServletRequest request, SaveContextOnUpdateOrErrorResponseWrapper response) {
        super(request);
        this.response = response;
    }

    public AsyncContext startAsync() {
        this.response.disableSaveOnResponseCommitted();
        return super.startAsync();
    }

    public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException {
        this.response.disableSaveOnResponseCommitted();
        return super.startAsync(servletRequest, servletResponse);
    }
}

作用是禁止在异步Servlet提交时,自动保存SecurityContext。为什么要禁止?

在异步Servlet中,当任务执行完毕后,HttpServletResponse会自动提交,在提交过程中会自动保存SecurityContext到HttpSession中,由于子线程无法获取用户信息,导致保存失败。如果使用异步Servlet,默认情况会禁用HttpServletResponse提交时自动保存SecurityContext,而是由SecurityContextPersistenceFilter中保存SecurityContext。

HttpSessionSecurityContextRepository源码:

public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
    public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
    protected final Log logger = LogFactory.getLog(this.getClass());
    private final Object contextObject = SecurityContextHolder.createEmptyContext();
    private boolean allowSessionCreation = true;
    private boolean disableUrlRewriting = false;
    private String springSecurityContextKey = "SPRING_SECURITY_CONTEXT";
    private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

    public HttpSessionSecurityContextRepository() {
    }

    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        HttpServletRequest request = requestResponseHolder.getRequest();
        HttpServletResponse response = requestResponseHolder.getResponse();
        HttpSession httpSession = request.getSession(false);
        SecurityContext context = this.readSecurityContextFromSession(httpSession);
        if (context == null) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("No SecurityContext was available from the HttpSession: " + httpSession + ". A new one will be created.");
            }

            context = this.generateNewContext();
        }

        HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper wrappedResponse = new HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper(response, request, httpSession != null, context);
        requestResponseHolder.setResponse(wrappedResponse);
        requestResponseHolder.setRequest(new HttpSessionSecurityContextRepository.SaveToSessionRequestWrapper(request, wrappedResponse));
        return context;
    }

    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
        SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = (SaveContextOnUpdateOrErrorResponseWrapper)WebUtils.getNativeResponse(response, SaveContextOnUpdateOrErrorResponseWrapper.class);
        if (responseWrapper == null) {
            throw new IllegalStateException("Cannot invoke saveContext on response " + response + ". You must use the HttpRequestResponseHolder.response after invoking loadContext");
        } else {
            if (!responseWrapper.isContextSaved()) {
                responseWrapper.saveContext(context);
            }

        }
    }

    public boolean containsContext(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return false;
        } else {
            return session.getAttribute(this.springSecurityContextKey) != null;
        }
    }

    private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
        boolean debug = this.logger.isDebugEnabled();
        if (httpSession == null) {
            if (debug) {
                this.logger.debug("No HttpSession currently exists");
            }

            return null;
        } else {
            Object contextFromSession = httpSession.getAttribute(this.springSecurityContextKey);
            if (contextFromSession == null) {
                if (debug) {
                    this.logger.debug("HttpSession returned null object for SPRING_SECURITY_CONTEXT");
                }

                return null;
            } else if (!(contextFromSession instanceof SecurityContext)) {
                if (this.logger.isWarnEnabled()) {
                    this.logger.warn(this.springSecurityContextKey + " did not contain a SecurityContext but contained: '" + contextFromSession + "'; are you improperly modifying the HttpSession directly (you should always use SecurityContextHolder) or using the HttpSession attribute reserved for this class?");
                }

                return null;
            } else {
                if (debug) {
                    this.logger.debug("Obtained a valid SecurityContext from " + this.springSecurityContextKey + ": '" + contextFromSession + "'");
                }

                return (SecurityContext)contextFromSession;
            }
        }
    }

    protected SecurityContext generateNewContext() {
        return SecurityContextHolder.createEmptyContext();
    }

    public void setAllowSessionCreation(boolean allowSessionCreation) {
        this.allowSessionCreation = allowSessionCreation;
    }

    public void setDisableUrlRewriting(boolean disableUrlRewriting) {
        this.disableUrlRewriting = disableUrlRewriting;
    }

    public void setSpringSecurityContextKey(String springSecurityContextKey) {
        Assert.hasText(springSecurityContextKey, "springSecurityContextKey cannot be empty");
        this.springSecurityContextKey = springSecurityContextKey;
    }

    private boolean isTransientAuthentication(Authentication authentication) {
        return AnnotationUtils.getAnnotation(authentication.getClass(), Transient.class) != null;
    }

    public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
        Assert.notNull(trustResolver, "trustResolver cannot be null");
        this.trustResolver = trustResolver;
    }
  • loadContext:调用readSecurityContextFromSession获取SecurityContext对象。如果为null,调用generateNewContext生成空的SecurityContext对象,并构造请求和响应的装饰类存入requestResponseHolder中。
  • saveContext:保存SecurityContext

SecurityContextPersistenceFilter源码

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.web.context;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

public class SecurityContextPersistenceFilter extends GenericFilterBean {
    static final String FILTER_APPLIED = "__spring_security_scpf_applied";
    private SecurityContextRepository repo;
    private boolean forceEagerSessionCreation;

    public SecurityContextPersistenceFilter() {
        this(new HttpSessionSecurityContextRepository());
    }

    public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
        this.forceEagerSessionCreation = false;
        this.repo = repo;
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (request.getAttribute("__spring_security_scpf_applied") != null) {
            chain.doFilter(request, response);
        } else {
            boolean debug = this.logger.isDebugEnabled();
            request.setAttribute("__spring_security_scpf_applied", Boolean.TRUE);
            if (this.forceEagerSessionCreation) {
                HttpSession session = request.getSession();
                if (debug && session.isNew()) {
                    this.logger.debug("Eagerly created session: " + session.getId());
                }
            }

            HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
            SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
            boolean var13 = false;

            try {
                var13 = true;
                SecurityContextHolder.setContext(contextBeforeChainExecution);
                chain.doFilter(holder.getRequest(), holder.getResponse());
                var13 = false;
            } finally {
                if (var13) {
                    SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
                    SecurityContextHolder.clearContext();
                    this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
                    request.removeAttribute("__spring_security_scpf_applied");
                    if (debug) {
                        this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
                    }

                }
            }

            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
            SecurityContextHolder.clearContext();
            this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
            request.removeAttribute("__spring_security_scpf_applied");
            if (debug) {
                this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
            }

        }
    }

    public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
        this.forceEagerSessionCreation = forceEagerSessionCreation;
    }
}

核心方法是doFilter方法:

  1. 首先从request中获取FILTER_APPLIED属性,不为null放行,为null设置上属性值为true。确保该请求只执行一次这个过滤器
  2. forceEagerSessionCreation表示是否在过滤器链执行前确保会话有效,比较耗费资源,默认是false
  3. 构造HttpRequestResponseHolder对象,将HttpServletRequest HttpServletResponse存入。
  4. 调用repo.loadContext加载SecurityContext
  5. SecurityContext放入SecurityContextHolder中,这样就可以通过SecurityContextHolder来获取当前用户信息了
  6. 调用chain.doFilter方法执行下一个过滤链,此时传递的是SaveToSessionRequestWrapper SaveToSessionResponseWrapper的实例。
  7. 请求处理完毕后,在finally模块中,获取最新的SecurityContext,然后清空SecurityContextHolder中的数据。再调用repo.saveContext保存SecurityContext
  8. 移除FILTER_APPLIED属性

总结:

SecurityContextPersistenceFilter 先从HttpSession中读取SecurityContext出来,存入SecurityContextHolder以备后续使用,当请求离开SecurityContextPersistenceFilter的时候,获取最新的SecurityContext并存入HttpSession中,同时清空SecurityContextHolder中的登录信息

相关文章
|
JavaScript 前端开发 索引
如何判断一个值是否在数组内?
如何判断一个值是否在数组内?
|
设计模式 开发框架 前端开发
项目开发中,真的有必要定义VO,BO,PO,DO,DTO这些吗?
存在即是合理的,业务复杂,人员协同性要求高的场景下,这些规范性的东西不按着来虽然不会出错,程序照样跑,但是遵守规范会让程序更具扩展性和可读性,都是前辈血淋淋的宝贵经验,为什么不用?
|
Java 数据库连接 Spring
SpringBoot2 | BeanDefinition 注册核心类 ImportBeanDefinitionRegistrar 源码分析 (十)
SpringBoot2 | BeanDefinition 注册核心类 ImportBeanDefinitionRegistrar 源码分析 (十)
347 0
|
机器学习/深度学习 数据采集 存储
时间序列预测新突破:深入解析循环神经网络(RNN)在金融数据分析中的应用
【10月更文挑战第7天】时间序列预测是数据科学领域的一个重要课题,特别是在金融行业中。准确的时间序列预测能够帮助投资者做出更明智的决策,比如股票价格预测、汇率变动预测等。近年来,随着深度学习技术的发展,尤其是循环神经网络(Recurrent Neural Networks, RNNs)及其变体如长短期记忆网络(LSTM)和门控循环单元(GRU),在处理时间序列数据方面展现出了巨大的潜力。本文将探讨RNN的基本概念,并通过具体的代码示例展示如何使用这些模型来进行金融数据分析。
1217 2
|
8月前
|
关系型数据库 MySQL Linux
MySQL8官方YUM仓库使用指南
MySQL 8 是广受欢迎的开源关系数据库管理系统,引入了诸多新特性和性能提升。本文介绍如何在 Linux 上通过 MySQL 官方 YUM 仓库安装和管理 MySQL 8。首先配置 YUM 仓库并安装 MySQL,启动服务后获取临时密码并登录。接着创建数据库与用户,使用 SQL 命令创建表格、插入及查询数据。此方法简便高效,适合快速上手 MySQL 8 的基本操作。
639 13
|
SQL 分布式计算 DataWorks
DataWorks产品使用合集之如何开发ODPS Spark任务
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
292 2
|
11月前
|
存储 SQL Apache
Apache Doris 创始人:何为“现代化”的数据仓库?
3.0 版本是 Apache Doris 研发路程中的重要里程碑,他将这一进展总结为“实时之路”、“统一之路”和“弹性之路”,详细介绍了所对应的核心特性的设计思考与应用价值,揭晓了 2025 年社区发展蓝图
649 6
Apache Doris 创始人:何为“现代化”的数据仓库?
|
小程序 搜索推荐 前端开发
小剧场短剧影视小程序开发
小剧场短剧影视小程序旨在为用户提供一个便捷、互动的平台,让用户能够随时随地观看、分享和评论各类小剧场短剧。通过小程序,用户可以浏览热门短剧、搜索感兴趣的内容、参与社区互动,以及享受个性化的推荐服务。
|
JavaScript
Vue2级联选择(Cascader)
这是一个基于 Vue 3 的级联选择组件,支持多种自定义属性,如数据源、选中项、文本字段等。提供了丰富的配置项,如层级间隙、宽度、高度、禁用状态和占位符等,便于灵活使用。组件通过监听选择变化并触发回调事件,实现了动态更新与交互。
694 1
Vue2级联选择(Cascader)
|
存储 SQL Cloud Native
揭秘数据库技术的核心与未来:从架构到应用
一、引言 数据库技术是当代信息系统中不可或缺的一部分,它为企业和个人提供了可靠、高效的数据管理解决方案