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

相关文章
相关标签/搜索