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

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