在Spring Security源碼閱讀2-Spring Security過濾器鏈的初始化1文章中,遺留了如下兩個問題:
-
在步驟(15)中,我們說HttpSecurity類中的performBuild函數返回了DefaultSecurityFilterChain類,而該類中封裝了該鏈的所有過濾器,那麼這些過濾器是如何添加進來的呢?
-
在步驟(17)中生成的FilterChainProxy對象也是Filter實例,那麼在收到客戶端請求後是如何完成過濾器鏈式調用的呢?
接下來我們就逐步回答上面兩個問題。本文先回答問題2,因爲回答問題2後,讀者就可以從全視角瞭解Spring Security過濾器的創建和調用流程。本文中會將該問題拆的更細,按由簡到繁的順序來解答。
1. FilterChainProxy如何實現過濾器鏈調用
根據Filter接口的定義,可以確定每次客戶端請求在經過過濾器時調用的都是doFilter函數,FilterChainProxy對doFilter函數的實現如下:
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
(1)doFilterInternal(request, response, chain);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
else {
doFilterInternal(request, response, chain);
}
}
(1) 派發到過濾器鏈上執行,可以看到doFilter調用的是FilterChainProxy實例的內部函數doFilterInternal,其代碼如下:
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
(2)List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
(3)chain.doFilter(fwRequest, fwResponse);
return;
}
(4)VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
(5)vfc.doFilter(fwRequest, fwResponse);
}
(2) 根據每個過濾器鏈配置的RequestMathcher,決定每一個請求要經過哪些過濾器。(參考Spring Security源碼閱讀2-Spring Security過濾器鏈的初始化1第(15)步,HttpSecurity在返回performBuild中返回的是DefaultSecurityFilterChain類的實例,而在創造DefaultSecurityFilterChain實例時傳遞的RequestMatcher實例是AnyRequestMatcher.INSTANCE,其matches函數默認返回true,請參考附錄代碼一)。
(3) 如果當前過濾器鏈沒有匹配的過濾器,則執行下一條過濾器鏈。
(4) 將所有的過濾器合併成一個虛擬過濾器鏈。
(5) 執行虛擬過濾器鏈。
可以看到最終執行的是虛擬過濾器鏈VirtualFilterChain類的實例,接下來看看VirtualFilterChain類doFilter的實現:
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (currentPosition == size) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
(6)originalChain.doFilter(request, response);
}
else {
currentPosition++;
(7)Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
(8)nextFilter.doFilter(request, response, this);
}
}
}
(6) 如果當前虛擬過濾器鏈上的所有過濾器都已經執行完畢,則執行原生過濾器鏈上的剩餘邏輯。
(7) 獲得當前虛擬過濾器鏈上的下一個過濾器。
(8) 執行過濾器。doFilter的函數原型定義如下:
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
我們可以看到第三個參數傳遞的是this,即當前虛擬過濾器鏈實例。因此,當在nextFilter的doFilter函數中再次通過chain參數調用doFilter函數時,則會再次調用到當前虛擬過濾器鏈實例(僞代碼如下),從而完成虛擬過濾器鏈上的所有過濾器的調用 。
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
......
chain.doFilter(request, response, chain); //這裏的chain是傳進來的VirtualFilterChain實例
......
}
以上就是FilterChainProxy實現過濾器鏈調用的全過程。
接下來討論一下客戶端請求是如何傳遞到過濾器的,但是要了解請求傳遞到過濾器的過程,就必須得清楚過濾器在Servlet容器中註冊的是什麼。
2. Spring Security過濾器如何註冊到Servlet容器
這裏先說明一下,爲什麼本節的標題用的是過濾器而非過濾器鏈,因爲本文第一節已經分析了FilterChainProxy實現過濾器鏈調用的原理,而FilterChainProxy本質上也是一個Filter實例。
在IDE中啓動Spring應用默認使用的是Spring Boot內嵌的Tomcat容器,因此這裏討論的Servlet容器指的是Tomcat的Servlet容器。在啓動Spring應用時會向IoC註冊securityFilterChainRegistration Bean:
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class SecurityFilterAutoConfiguration {
private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;
@Bean
@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
SecurityProperties securityProperties) {
(9)DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
DEFAULT_FILTER_NAME);
registration.setOrder(securityProperties.getFilter().getOrder());
registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
return registration;
}
......
}
(9) DelegatingFilterProxyRegistrationBean實現了ServletContextInitializer接口,該接口用於配置Servlet上下文。DelegatingFilterProxyRegistrationBean目的是向Servlet容器中註冊一個過濾器:實現類爲 DelegatingFilterProxy 的一個 Servlet Filter。DelegatingFilterProxy 其實是一個代理過濾器,它被 Servlet 容器用於匹配特定URL模式的請求,而它會將任務委託給Spring管理的Bean,即名字爲 springSecurityFilterChain 的Bean,而springSecurityFilterChain正是Spring Security源碼閱讀2-Spring Security過濾器鏈的初始化1中介紹的,從而實現了Servlet容器管理的DelegatingFilterProxy與Spring容器創建的springSecurityFilterChain Bean的關聯。關於這一段結論的代碼分析,請參考附錄代碼二。
爲了加深對DelegatingFilterProxy的理解,我們可以看下其註釋:
/*Proxy for a standard Servlet Filter, delegating to a Spring-managed bean that
* implements the Filter interface. Supports a "targetBeanName" filter init-param
* in {@code web.xml}, specifying the name of the target bean in the Spring
* application context.
*/
清楚了Servlet容器管理的DelegatingFilterProxy與Spring容器創建的springSecurityFilterChain Bean的關聯,接下來就可以分析客戶端請求如何傳遞到過濾器。
3. 客戶端請求如何傳遞到過濾器
當用戶請求到來並且與過濾器的URL模式匹配後,會調用DelegatingFilterProxy的doFilter函數:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
(10)delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// Let the delegate perform the actual doFilter operation.
(11)invokeDelegate(delegateToUse, request, response, filterChain);
}
(10) 獲得Spring容器管理的springSecurityFilterChain Bean.
(11) 調用FilterChainProxy的doFilter函數,代碼如下:
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
以上就是Spring Security過濾器的調用和執行流程。在整個分析中,爲了分析的簡單和文章不至於太長,我並沒有詳細分析Servlet容器和Spring IoC容器的交互過程,有興趣的讀者可以自行分析。
附錄
1. 代碼一
HttpSecurity.java
private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE;
@Override
protected DefaultSecurityFilterChain performBuild() {
filters.sort(comparator);
return new DefaultSecurityFilterChain(requestMatcher, filters);
}
2. 代碼二
我們直接從ServletWebServerApplicationContext類開始分析,因爲在Servlet類型應用中,實際實例化的應用上下文爲ServletWebServerApplicationContext。爲什麼會如此,讀者可以分析一下Spring Boot的啓動流程。
/**
* Returns the {@link ServletContextInitializer} that will be used to complete the
* setup of this {@link WebApplicationContext}.
* @return the self initializer
* @see #prepareWebApplicationContext(ServletContext)
*/
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
return this::selfInitialize;
}
private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
(1)for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}
(1) 回調所有ServletContextInitializer的onStart函數。通過調試,ServletWebServerApplicationContext中有如下這些ServletContextInitializer:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-N2m2sgf5-1582177602093)(/Users/liulijun/projects/blog/source/_posts/Spring-Security-Source-Code2-filter-Chain2/ServletContextInitializer.png)]
可以看到DelegatingFilterProxyRegistrationBean類的實例,再看看其onStart回調函數,其最終回調的是addRegistration函數(DelegatingFilterProxyRegistrationBean繼承自AbstractFilterRegistrationBean,該函數位於其中):
@Override
protected Dynamic addRegistration(String description, ServletContext servletContext) {
Filter filter = getFilter();
(2)return servletContext.addFilter(getOrDeduceName(filter), filter);
}
(2) 通過調用DelegatingFilterProxyRegistrationBean中的getFilter函數獲得DelegatingFilterProxy類的實例,並將基添加到Servlet上下文中,最終將過濾器添加到StandardContext(這個就是Tomcat的上下文了,從而建立了Tomcat容器與Spring的關係)的filterDefs屬性中。