SecurityContextPersistenceFilter 过滤器链

·  阅读 291

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战

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

分类:
后端
标签: