摘要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。以下圖:
圖中可見,系統一共加載了12個Filter來攔截、處理當前請求。咱們逐個Filter向下看,它們依次是:
ApplicationFilterConfig[name=log4jServletFilter, filterClass=org.apache.logging.log4j.web.Log4jServletFilter]
ApplicationFilterConfig[name=errorPageFilter, filterClass=org.springframework.boot.web.support.ErrorPageFilter]
ApplicationFilterConfig[name=characterEncodingFilter, filterClass=org.springframework.boot.web.filter.OrderedCharacterEncodingFilter]
ApplicationFilterConfig[name=hiddenHttpMethodFilter, filterClass=org.springframework.boot.web.filter.OrderedHiddenHttpMethodFilter]
ApplicationFilterConfig[name=httpPutFormContentFilter, filterClass=org.springframework.boot.web.filter.OrderedHttpPutFormContentFilter]
ApplicationFilterConfig[name=requestContextFilter, filterClass=org.springframework.boot.web.filter.OrderedRequestContextFilter]
ApplicationFilterConfig[name=springSecurityFilterChain, filterClass=org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1]
ApplicationFilterConfig[name=afterAuthenticatedProcessingFilter, filterClass=cn.xxx.thread.common.security.AfterAuthenticatedProcessingFilter]
ApplicationFilterConfig[name=preAuthenticatedProcessingFilter, filterClass=cn.xxx.thread.common.security.PreAuthenticatedProcessingFilter]
ApplicationFilterConfig[name=org.springframework.security.filterChainProxy, filterClass=org.springframework.security.web.FilterChainProxy]
ApplicationFilterConfig[name=org.springframework.security.web.access.intercept.FilterSecurityInterceptor#0, filterClass=org.springframework.security.web.access.intercept.FilterSecurityInterceptor]
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