[3] SecurityContextPersistenceFilter

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;
	}

image.png    从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);
	}
}

image.png    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;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章