SecurityContextPersistenceFilter
SecurityContextPersistenceFilter是Springsecurity链中第二道防线,位于WebAsyncManagerIntegrationFilter之后,作用是为了存储SecurityContext而设计的。
SecurityContextPersistenceFilter主要做两件事:
- 当请求到来时,从HttpSession中获取SecurityContext并存入SecurityContextHolder中,这样在同一个请求的后续处理过程中,通过SecurityContextHolder获取数据
- 当一个请求处理完毕时,从SecurityContextHolder中获取SecurityContext并存入HttpSession中,方便下一个请求到来时,再从HTTPSession中拿来使用,同时擦除SecurityContextHolder中的登录信息。
将SpringContext存入HttpSession,或者从HttpSession中加载数据转化为SpringContext对象,这些事情都是由SecurityContextRepository的实现类完成。
SecurityContextRepository接口有三个实现类:
- TestSecurityContextRepository:为单元测试提供支持
- NullSecurityContextRepository:未做SpringContext的存储工作
- HttpSessionSecurityContextRepository:Springsecurity的默认使用类,实现了将SpringContext存储到HttpSession中以及从HttpSession中加载SecurityContext出来。
HttpSessionSecurityContextRepository
HttpSessionSecurityContextRepository定义的关于请求和封装的两个内部类
SaveToSessionResponseWrapper
它是对HttpServletResponse的扩展,
- HttpServletResponseWrapper实现了HttpServletResponse接口,它是HttpServletResponse的装饰类,利用HttpServletResponseWrapper可以方便操作参数和输出流等。
- OnCommittedResponseWrapper 继承HttpServletResponseWrapper,对其功能进行了增强,当HttpServletResponse的sendError sendRedirect flushBuffer等方法被调用的时候,doOnResponseCommitted()方法会被调用,这个方法里可以做一些数据的保存操作。OnCommittedResponseWrapper中的onResponseCommitted是抽象方法,实现类在SaveContextOnUpdateOrErrorResponseWrapper中
- SaveContextOnUpdateOrErrorResponseWrapper 继承OnCommittedResponseWrapper,并对onResponseCommitted方法做了实现。在SaveContextOnUpdateOrErrorResponseWrapper中声明一个contextSaved变量,表示SecurityContext是否已经存储成功。当HttpServletResponse提交时,会调用onResponseCommitted方法,在onResponseCommitted中调用saveContext方法,并将contextSaved设置为true,表示已经存储。saveContext是抽闲方法,在SaveToSessionResponseWrapper中实现。
- 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
代码语言:javascript复制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源码:
代码语言:javascript复制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源码
代码语言:javascript复制//
// 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方法:
- 首先从request中获取FILTER_APPLIED属性,不为null放行,为null设置上属性值为true。确保该请求只执行一次这个过滤器
- forceEagerSessionCreation表示是否在过滤器链执行前确保会话有效,比较耗费资源,默认是false
- 构造HttpRequestResponseHolder对象,将HttpServletRequest HttpServletResponse存入。
- 调用repo.loadContext加载SecurityContext
- SecurityContext放入SecurityContextHolder中,这样就可以通过SecurityContextHolder来获取当前用户信息了
- 调用chain.doFilter方法执行下一个过滤链,此时传递的是SaveToSessionRequestWrapper SaveToSessionResponseWrapper的实例。
- 请求处理完毕后,在finally模块中,获取最新的SecurityContext,然后清空SecurityContextHolder中的数据。再调用repo.saveContext保存SecurityContext
- 移除FILTER_APPLIED属性
总结:
SecurityContextPersistenceFilter 先从HttpSession中读取SecurityContext出来,存入SecurityContextHolder以备后续使用,当请求离开SecurityContextPersistenceFilter的时候,获取最新的SecurityContext并存入HttpSession中,同时清空SecurityContextHolder中的登录信息