SecurityContextPersistenceFilter
介绍
我们从字面上可以看出这是一个SecurityContext持久化的过滤器,这个也是SecurityContext相关的过滤器。再一次HTTP请求经过该过滤器时,Spring Security会先从SecurityContextRepository的取认证信息,这个接口有2种实现,分别是:1)NullSecurityContextRepository和2)HttpSessionSecurityContextRepository,前者所有的方法实现都是空操作,没有实际的意义。后者是以"SPRING_SECURITY_CONTEXT"为key,SecurityContext为value存储到HttpSession中。当有请求经过过滤器时,先从HttpSession中取上下文信息,如果根据sessionId取得的上下文中身份认证信息有效,会写入到SecurityContextHolder中起到一层缓存作用。这样,SecurityContextPersistenceFilter后续要执行的过滤器就能从SecurityContextHolder取出缓存的SecurityContext,从而减少一次认证流程。当然,如果HttpSession中无上下文信息,因为一次请求成功调用接口,必然通过了身份认证,在其他过滤器执行过程中会把身份认证信息写入SecurityContextHolder。SecurityContextPersistenceFilter的后置处理会把上下文写入到HttpSession,以便下次sessionId一样的请求可以从HttpSession缓存中取到身份认证信息。需要注意的是,SessionCreationPolicy需要设置为IF_REQUIRED或ALWAYS才能进行session缓存。
代码分析
步骤1
SecurityContextPersistenceFilter的关键代码如下:
public class SecurityContextPersistenceFilter extends GenericFilterBean {
static final String FILTER_APPLIED = "__spring_security_scpf_applied";
//HttpSession仓储,有个2个实现NullSecurityContextRepository和HttpSessionSecurityContextRepository
private SecurityContextRepository repo;
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
...省略部分代码
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
//过滤链执行前从仓储中取认证信息
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
//把认证信息放到ThreadLocal中
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
//过滤链执行后从仓储中取认证信息
//有的Filter执行过程中会把上下文放到Holder中
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
//清空Holder中的上下文
SecurityContextHolder.clearContext();
//如果repo是SecurityContextPersistenceFilter,则把SecurityContext写入HttpSession
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
}
}
}
步骤2
调试发现SecurityContextRepository的默认实现是NullSecurityContextRepository,这个是一个空实现,代码如下:
public final class NullSecurityContextRepository implements SecurityContextRepository {
public boolean containsContext(HttpServletRequest request) {
return false;
}
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
return SecurityContextHolder.createEmptyContext();
}
public void saveContext(SecurityContext context, HttpServletRequest request,
HttpServletResponse response) {
}
}
那么我们如何才能注入HttpSessionSecurityContextRepository呢?我们看到SecurityContextPersistenceFilter是通过构造器注入的,我们在构造器关键代码处打上断点,重启项目。断点位置和调用栈截图如下:
public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
this.repo = repo;
}
从SecurityContextConfigurer有http.getSharedObject(SecurityContextRepository.class)这行代码,进入getSharedObject()方法的实现类,sharedObjects是一个HashMap。getSharedObject()/setSharedObject()这2个方法在Spring Security中被反复用到。其实sharedObjects就是一个全局变量,用于存储全局共享的Class类型的实例,但是也导致了代码阅读比较让人费解,往往不知道实例时什么时候被到共享Map中的。如果使用的是Idea,则可以通过条件断点的方式,在setSharedObject()方法中打上断点,代码和调用栈截图如下:
public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBuilder<O>>
extends AbstractSecurityBuilder<O> {
private final Log logger = LogFactory.getLog(getClass());
public <C> void setSharedObject(Class<C> sharedType, C object) {
this.sharedObjects.put(sharedType, object);
}
@SuppressWarnings("unchecked")
public <C> C getSharedObject(Class<C> sharedType) {
return (C) this.sharedObjects.get(sharedType);
}
}
SessionManagementConfigurer中发现SecurityContextRepository的究竟用哪个实现决于isStateless()方法,isStateless()代码可以看出,只要sessionPolicy不是STATELESS就会注入HttpSessionSecurityContextRepository实例,我们可以在项目中配置sessionPolicy为IF_REQUIRED。代码和配置如下:
private boolean isStateless() {
SessionCreationPolicy sessionPolicy = getSessionCreationPolicy();
return SessionCreationPolicy.STATELESS == sessionPolicy;
}
public SessionManagementConfigurer<H> sessionCreationPolicy(
SessionCreationPolicy sessionCreationPolicy) {
Assert.notNull(sessionCreationPolicy, "sessionCreationPolicy cannot be null");
this.sessionPolicy = sessionCreationPolicy;
return this;
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
}
步骤3
通过上述步骤将SecurityContextRepository配置为HttpSessionSecurityContextRepository,当执行repo.loadContext(holder)时,调用readSecurityContextFromSession()读取SecurityContext,同一sessionId在HttpSession中有缓存,则取出,否则接下来new出来一个Empty SecurityContext。如果HttpSession中缓存了SecurityContext,在chain.doFilter()执行到下个过滤器时,可以直接取出上下文,减少一次读取身份认证信息的操作。chain.doFilter()执行完毕后,要么上下中写入更新的身份认证信息,要么写入新的身份认证信息,无论如何,SecurityContextPersistenceFilter都会调用HttpSessionSecurityContextRepository的saveContext()试图把最新的上下文写入到HttpSession中。这样,对于同一个Session(浏览器只要不关闭窗口,或者刷新一般都是在同一个会话中,sessionId不变)发起的请求,SecurityContext就会被SecurityContextPersistenceFilter做缓存。代码如下:
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
HttpSession httpSession = request.getSession(false);
//httpSession为null的话,返回的context也为null,从httpSession中取SecurityContext
SecurityContext context = readSecurityContextFromSession(httpSession);
if (context == null) {
//new出来一个Empty SecurityContext
context = generateNewContext();
}
SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(
response, request, httpSession != null, context);
requestResponseHolder.setResponse(wrappedResponse);
requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(
request, wrappedResponse));
return context;
}
private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
//若httpSession都为null,直接返回
if (httpSession == null) {
return null;
}
//冲httpSession中取出指定key的对象,这里不出意外就是SecurityContext
Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
//没取到信息直接返回
if (contextFromSession == null) {
return null;
}
// We now have the security context object from the session.
//对象不是SecurityContext直接返回
if (!(contextFromSession instanceof SecurityContext)) {
return null;
}
//类型强转为SecurityContext
return (SecurityContext) contextFromSession;
}
@Override
protected void saveContext(SecurityContext context) {
final Authentication authentication = context.getAuthentication();
HttpSession httpSession = request.getSession(false);
//如果是authentication为null或者authentication为匿名类型,直接返回
if (authentication == null || trustResolver.isAnonymous(authentication)) {
if (httpSession != null && authBeforeExecution != null) {
httpSession.removeAttribute(springSecurityContextKey);
}
return;
}
if (httpSession == null) {
//在配置允许以及Authentication类型支持序列化的情况下,会创建新的HttpSession
httpSession = createNewSessionIfAllowed(context);
}
// If HttpSession exists, store current SecurityContext but only if it has
// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
if (httpSession != null) {
// We may have a new session, so check also whether the context attribute
// is set SEC-1561
if (contextChanged(context)
|| httpSession.getAttribute(springSecurityContextKey) == null) {
httpSession.setAttribute(springSecurityContextKey, context);
}
}
}
private HttpSession createNewSessionIfAllowed(SecurityContext context) {
//@Transient标识Authentication不能被序列化,对此种Authentication无需写入到Session中
if (isTransientAuthentication(context.getAuthentication())) {
return null;
}
try {
return request.getSession(true);
}
}
private boolean isTransientAuthentication(Authentication authentication) {
return AnnotationUtils.getAnnotation(authentication.getClass(), Transient.class) != null;
}