写点什么

SecurityContextPersistenceFilter 过滤器链

作者:周杰伦本人
  • 2022 年 5 月 04 日
  • 本文字数:9521 字

    阅读完需:约 31 分钟

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 中的登录信息

发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2020.02.29 加入

还未添加个人简介

评论

发布
暂无评论
SecurityContextPersistenceFilter 过滤器链_5月月更_周杰伦本人_InfoQ写作社区