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