SpringBoot+SpringSecurity誤攔截靜態資源問題調研

摘要html

在將p模塊遷移到Spring Boot框架下的過程當中,發現了這樣一個問題:在訪問靜態資源時,咱們爲SpringSecurity配置的AfterAuthenticatedProcessingFilter會錯誤地攔截請求,並致使拋出異常。經調研發現,這是Spring Boot自動裝配javax.sevlet.Filter致使的問題。java


 

問題git

在將p遷移到Spring Boot架構下以後,正常啓動系統,並訪問靜態資源(如http://localhost:8080/thread/js/fingerprint.json)時,發生以下異常:github

17:20:07,806 INFO [cn.xxx.thread.common.security.AfterAuthenticatedProcessingFilter.doFilter] (http-nio-8080-exec-2) url:http://localhost:8080/thread/js/fingerprint.json,uri:{}/thread/js/fingerprint.json^|TraceId.-http-nio-8080-exec-2web

17:20:07,813 ERROR [org.springframework.boot.web.support.ErrorPageFilter.forwardToErrorPage] (http-nio-8080-exec-2) Forwarding to error page from request [/js/fingerprint.json] due to exception [null]^|TraceId.-http-nio-8080-exec-2spring

java.lang.NullPointerException: null數據庫

at cn.xxx.thread.common.security.AfterAuthenticatedProcessingFilter.isFirstTimeLogin(AfterAuthenticatedProcessingFilter.java:108) ~[thread_common-2015.jar:?]express

at cn.xxx.thread.common.security.AfterAuthenticatedProcessingFilter.doFilter(AfterAuthenticatedProcfessingFilter.java:84) ~[thread_common-2015.jar:?]apache

at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[catalina.jar:8.0.47]json

at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.47]

at org.springframework.orm.hibernate4.support.OpenSessionInViewFilter.doFilterInternal(OpenSessionInViewFilter.java:151) ~[spring-orm-4.3.10.RELEASE.jar:4.3.10.RELEASE]

at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]

at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[catalina.jar:8.0.47]

at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.47]

at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]

at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]

at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[catalina.jar:8.0.47]

at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[catalina.jar:8.0.47]

at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:207) ~[spring-security-web-4.0.3.RELEASE.jar:4.0.3.RELEASE]

at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176) ~[spring-security-web-4.0.3.RELEASE.jar:4.0.3.RELEASE]

 

其中的AfterAuthenticatedProcessingFilter是在spring-security-common.xml中配置的,用於在BasicAuth認證經過以後,再作一些額外處理。其配置以下:

spring-security-common.xml

<http create-session="stateless" use-expressions="true" auto-config="false" realm="UCredit Inc. Thread"

    entry-point-ref="authenticationEntryPoint">

    <intercept-url pattern="/**" access="isAuthenticated()" />

    <http-basic authentication-details-source-ref="ipAwareWebAuthenticationDetailsSource" />

    <logout delete-cookies="JSESSIONID" invalidate-session="true" success-handler-ref="logoutSuccessHandler" />

    <custom-filter ref="preAuthenticatedProcessingFilter" before="BASIC_AUTH_FILTER" />

    <custom-filter ref="afterAuthenticatedProcessingFilter" after="BASIC_AUTH_FILTER" />

    <headers>

        <frame-options policy="SAMEORIGIN" />

        <cache-control />

        <content-type-options />

        <hsts include-subdomains="false" />

        <xss-protection />

    </headers>

    <csrf disabled="true" />

</http>

代碼以下:

AfterAuthenticatedProcessingFilter

@Override

public void doFilter(ServletRequest request, ServletResponse response,

        FilterChain chain) throws IOException, ServletException {

    HttpServletRequest req = (HttpServletRequest) request;

    HttpServletResponse rep = (HttpServletResponse) response;

    //首次登錄校驗

    if (AfterAuthenticatedProcessingFilter.isFirstTimeLogin(req, rep)) {

        return;

    }

    // 省略後續代碼

}

/**

 * 首次登錄校驗

 *

 * @param req

 * @param rep

 * @return

 * @throws IOException

 */

private static boolean isFirstTimeLogin(HttpServletRequest req,

        HttpServletResponse rep) throws IOException {

    User user = SecurityUtils.getUserFromPrincipal(SecurityContextHolder

        .getContext().getAuthentication());

    // 下一行拋出一行,由於這裏獲取到的user是null

    if (user.getUserType() == UserType.SYSTEM_USER) {

        return false;

    }

    // 省略後續代碼


 

然而,咱們在工程下的spring-thread.xml中已經作了以下配置,確保SpringSecurity不攔截、處理靜態資源。相關配置以下:

spring-security.xml

<http pattern="/js/**" security="none" create-session="stateless" />

<http pattern="/html/**" security="none" create-session="stateless" />

<http pattern="/resources/**" security="none" create-session="stateless" />

 

<beans:import resource="classpath:spring-security-common.xml" />

 

那麼,爲何會出現這個異常呢?


 

分析

這個問題最大的疑點在於,爲何咱們爲靜態資源作了security="none"的配置,但是SpringSecurity仍然攔截到了這個請求?其次,爲何SpringSecurity的三個Filter(preAuthenticatedProcessingFilter、BasicAuthenticationFilter、afterAuthenticatedProcessingFilter)中,只有afterAuthenticatedProcessingFilter攔截並處理了靜態資源的請求?若是preAuthenticatedProcessingFilter處理了請求,應該會打印相關日誌,但始終沒有打印出來。若是BasicAuthenticationFilter處理了請求,那麼afterAuthenticatedProcessingFilter中獲取的user就不會是null了。

 

你們能夠來「我猜我猜我猜猜猜」一下,猜猜看是哪兒的問題。我提供幾個我猜過的選項:

  • application.properties文件中,context-path配置錯了。

  • spring-security.xml中,<http pattern="xxx" ... /> 配置錯了。

  • SpringSecurity被加載了兩次(SpringSecurity一次、Spring Boot下xxxAutoConfiguration一次)。

  • Spring的web容器被加載了兩次。

  • Spring Boot引起版本衝突,致使security="none"對preAuthenticatedProcessingFilter、BasicAuthenticationFilter生效、而對afterAuthenticatedProcessingFilter未生效。

 

 

各類錯誤的猜測我就不贅述了,直接切入正確軌道上來。切入方式麼,仍是打斷點。

斷點位置

通常來講,斷點會打在異常堆棧中的某個類/方法上,從而在合適的位置切入到發生異常時的上下文環境中去。可是此次,我把異常堆棧看了又看,始終不能肯定斷點放在什麼地方比較合適。

雖然異常確實發生在at cn.xxx.thread.common.security.AfterAuthenticatedProcessingFilter.isFirstTimeLogin(AfterAuthenticatedProcessingFilter.java:108) ~[thread_common-2015.jar:?]這個位置上,可是很顯然:代碼執行到這裏時,一切都已經晚了。咱們須要把斷點往前移。

可是異常堆棧的前面幾行,是其它的Filter的doFilter方法。這些Filter只負責本身的一部分任務,與登陸認證無關。所以,這些類也不是合適的斷點位置。

再往前呢?再往前是org.apache.catalina包下的類;這些類離「犯罪現場」有點太遠了,可能須要通過不知道多少行代碼,才能運行到發生問題的位置上去。

 

但是沒辦法,再往前就是java.lang.Thread.run了。就這樣吧。我把斷點打在了StandardWrapperValve.invoke方法中。這個斷點的具體位置其實沒什麼關係,只要足夠「靠前」,就能夠了。由於後來發現問題時,代碼已經運行到很是「靠後」的位置上了。

第一層緣由

中間真的是不知道執行了多少行代碼了,忽然跳到這樣一個代碼位置上:

VirtualFilterChain

private static class VirtualFilterChain implements FilterChain {

    private final FilterChain originalChain;

    private final List<Filter> additionalFilters;

    private final FirewalledRequest firewalledRequest;

    private final int size;

    private int currentPosition = 0;

    private VirtualFilterChain(FirewalledRequest firewalledRequest,

            FilterChain chain, List<Filter> additionalFilters) {

        this.originalChain = chain;

        this.additionalFilters = additionalFilters;

        this.size = additionalFilters.size();

        this.firewalledRequest = firewalledRequest;

    }

    // 省略後面代碼

}

 

這段代碼很不起眼;難得的是其中有一個字段「originalChain」:在這個字段中,存放了當前上下文中加載的全部Filter。以下圖:

Springboot-static-resource.png

 

圖中可見,系統一共加載了12個Filter來攔截、處理當前請求。咱們逐個Filter向下看,它們依次是:

  1. ApplicationFilterConfig[name=log4jServletFilter, filterClass=org.apache.logging.log4j.web.Log4jServletFilter]

  2. ApplicationFilterConfig[name=errorPageFilter, filterClass=org.springframework.boot.web.support.ErrorPageFilter]

  3. ApplicationFilterConfig[name=characterEncodingFilter, filterClass=org.springframework.boot.web.filter.OrderedCharacterEncodingFilter]

  4. ApplicationFilterConfig[name=hiddenHttpMethodFilter, filterClass=org.springframework.boot.web.filter.OrderedHiddenHttpMethodFilter]

  5. ApplicationFilterConfig[name=httpPutFormContentFilter, filterClass=org.springframework.boot.web.filter.OrderedHttpPutFormContentFilter]

  6. ApplicationFilterConfig[name=requestContextFilter, filterClass=org.springframework.boot.web.filter.OrderedRequestContextFilter]

  7. ApplicationFilterConfig[name=springSecurityFilterChain, filterClass=org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1]

  8. ApplicationFilterConfig[name=afterAuthenticatedProcessingFilter, filterClass=cn.xxx.thread.common.security.AfterAuthenticatedProcessingFilter]

  9. ApplicationFilterConfig[name=preAuthenticatedProcessingFilter, filterClass=cn.xxx.thread.common.security.PreAuthenticatedProcessingFilter]

  10. ApplicationFilterConfig[name=org.springframework.security.filterChainProxy, filterClass=org.springframework.security.web.FilterChainProxy]

  11. ApplicationFilterConfig[name=org.springframework.security.web.access.intercept.FilterSecurityInterceptor#0, filterClass=org.springframework.security.web.access.intercept.FilterSecurityInterceptor]

  12. ApplicationFilterConfig[name=Tomcat WebSocket (JSR356) Filter, filterClass=org.apache.tomcat.websocket.server.WsFilter]

 

發現問題了麼?在這些Filter中,除了SpringSecurity的入口springSecurityFilterChain以外,afterAuthenticatedProcessingFilter和preAuthenticatedProcessingFilter也被加載了進來。換句話說,同一個請求,在被springSecurityFilterChain處理過一次以後,還會被afterAuthenticatedProcessingFilter和preAuthenticatedProcessingFilter再處理一遍。

不只如此,第10個、11個Filter,也是在springSecurityFilterChain中就已經加載過的Filter;它們一樣不該該出如今這個Filter列表中。

這樣,咱們就找到第一層緣由:SpringSecurity的Filter被加載了兩次。因此「我猜我猜我猜猜猜」的答案,應該是「SpringSecurity被加載了兩次(SpringSecurity一次、Spring Boot下xxxAutoConfiguration一次)」。

 

那麼,咱們只要找到對應的xxxAutoConfiguration,並將它Exclude掉就能夠了吧。是哪一個AutoConfiguration在這裏搗亂呢?SecurityAutoConfiguration?仍是SecurityFilterAutoConfiguration?

很遺憾,都不是。

 

第二層緣由

 第二層緣由要靠谷歌了。我搜到了這幾個網頁:

Prevent Spring Boot from registering a servlet filter

這是Stack Overflow上的一個問題,問的是怎樣防止Spring Boot把SpringSecurity的filterChainProxy註冊爲一個filter。回頭看看上面的12個Filter,filterChainProxy就躺在其中。雖然問題表現上有點不一致,但緣由都是同樣的。正如這個問題中所說的:

「By default Spring Boot creates a FilterRegistrationBean for every Filter in the application context for which a FilterRegistrationBean doesn't already exist. 」

 

Introduce a mechanism to disable existing filters/servlets beans #2173 

這是GitHub上Spring Boot項目中的一個討論。能夠看到,有很多人都遇到了相似問題。

而關於「bean class that implements javax.servlet.Filter interface is registered to filter automatically」,帖子最後表示,「That's by design」,Spring Boot就是這樣設計的。這一點不會變。

 

Disable registration of a Servlet or Filter 

這是Spring Boot官方文檔中給出的一個「不加載/註冊servlet或filter」的方法。實際上,上面兩篇文章中,也都使用了這個方法。

 

Spring Security FilterChainProxy is registered automatically as a Filter #2171

這裏提供了問題的另外一種解決方案。不過正如dsyer指出的:「That doesn't seem like a great resolution.


 

方案

綜合上面分析的緣由,我採用了Disable registration of a Servlet or Filter 中提供的方案,把重複加載的SpringSecurity四個Filter都「disable」掉了。代碼以下:

@Bean

public FilterRegistrationBean registration(

        AfterAuthenticatedProcessingFilter filter) {

    FilterRegistrationBean registration = new FilterRegistrationBean(

        filter);

    registration.setEnabled(false);

    return registration;

}

@Bean

public FilterRegistrationBean registration1(

        PreAuthenticatedProcessingFilter filter) {

    FilterRegistrationBean registration = new FilterRegistrationBean(

        filter);

    registration.setEnabled(false);

    return registration;

}

@Bean

public FilterRegistrationBean registration2(FilterChainProxy proxy) {

    FilterRegistrationBean registration = new FilterRegistrationBean(proxy);

    registration.setEnabled(false);

    return registration;

}

@Bean

public FilterRegistrationBean registration3(

        FilterSecurityInterceptor proxy) {

    FilterRegistrationBean registration = new FilterRegistrationBean(proxy);

    registration.setEnabled(false);

    return registration;

}

 

配置完成以後,頁面測試、斷點監控的結果都恢復正常。


 

小結

多囉嗦幾句。

從使用xml配置Spring IoC開始,就有「配置優先」仍是「約定優先」的爭論。Spring Boot的「自動裝配」,能夠理解爲「約定優先」的一種升級版。你看,實現了javax.servlet.Filter接口的bean,就會被註冊到web應用的Filter鏈中去;這其實就是Spring Boot和開發者、或者說和系統之間的「約定」。

從「約定優先」到「自動裝配」,主打的都是簡化開發工做、提升開發效率。有些狀況——也許是80%的狀況下,它確實達到了這一目標。可是在另外那20%的狀況下,它會帶來問題;而且,因爲一切都是框架實現、沒有人工干預,開發者甚至很難發現問題出在哪兒。於是,這20%的狀況,有時要佔去開發者80%的時間。

就如此次THREAD系統遷移到Spring Boot下的改造工做:f模塊因爲Validation和Batch的自動裝配引起問題,花費了我一天時間;p模塊因爲這裏記錄的這個問題,花費了我近兩天的時間。而其餘四個模塊,總共也就兩天半時間,這還包括了a和c這兩個「探路」模塊。

並且,f和p這兩個模塊遇到的問題還有些不一樣。f模塊遇到的,是典型的「從傳統Spring項目遷移到Spring Boot框架下」時會發生的問題,若是項目一開始就使用Spring Boot,確實能夠避免這類狀況。但p模塊遇到的,是「即便一開始就是Spring Boot項目也照樣會遇到會蒙圈會花費兩天時間去分析解決」的問題——看看Stack Overflow和GitHub上的討論吧。

這是我不喜歡「約定優先」,於是也不太喜歡「自動裝配」的一點:它們會幫你作不少事情;但有時候作得太多,過猶不及了。

相似的還有hibernate的session管理機制和關聯查詢機制。session管理機制使得JVM內存和數據庫變得透明、統一塊兒來了,開發者只須要操做一下內存對象——調用一下setXxx()方法,hibernate就會在session flush時自動將這個改動寫入數據庫。關聯查詢則將複雜的庫表關聯關係轉變成了更簡單的Java對象關係,不管多少個join都由hibernate完成。沒必要再費心費力去寫SQL、HQL,開發起來真爽利。

可是,若是咱們確實只要修改JVM中的數據、而不想把它持久化呢?若是咱們只須要查詢某個實體中的一小部分數據、而不想把全部關聯表都join一遍呢?咱們須要作一些特殊處理來繞開hibernate的自動處理,不然就會出現功能或性能上的問題。這時,本來用來提供便利的框架,反而變成了攔路石。

然而咱們仍是得使用這些框架,儘管它們不能「按照本身的名分,一分很少、一分很多」地去完成本身的任務。畢竟,在80%的狀況下,它們確實給了咱們很大的幫助。

不過,絕對不要知足於這80%的便利,而忘記那20%的風險。儘量的弄清楚它,預防它,在風險轉化爲問題時儘快地解決它。對系統、對我的,這都是莫大的提升。


 

參考

springboot對靜態資源作了afterAuthFilter和preAuthFilter的問題

Prevent Spring Boot from registering a servlet filter 

Introduce a mechanism to disable existing filters/servlets beans #2173 

Disable registration of a Servlet or Filter 

Spring Security FilterChainProxy is registered automatically as a Filter #2171

Spring Security custom authentication filter using Java Config

相關文章
相關標籤/搜索