SpringBoot+SpringSecurity误拦截静态资源问题调研

简介:

摘要

在将p模块迁移到Spring Boot框架下的过程中,发现了这样一个问题:在访问静态资源时,我们为SpringSecurity配置的AfterAuthenticatedProcessingFilter会错误地拦截请求,并导致抛出异常。经调研发现,这是Spring Boot自动装配javax.sevlet.Filter导致的问题。


 

问题

在将p迁移到Spring Boot架构下之后,正常启动系统,并访问静态资源(如http://localhost:8080/thread/js/fingerprint.json)时,发生如下异常:

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-2

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-2

java.lang.NullPointerException: null

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

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

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.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向下看,它们依次是:

  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%的风险。尽可能的弄清楚它,预防它,在风险转化为问题时尽快地解决它。对系统、对个人,这都是莫大的提高。


 




本文转自 斯然在天边 51CTO博客,原文链接:http://blog.51cto.com/winters1224/2052034,如需转载请自行联系原作者

相关文章
|
2月前
|
JavaScript Java Maven
【SpringBoot(二)】带你认识Yaml配置文件类型、SpringMVC的资源访问路径 和 静态资源配置的原理!
SpringBoot专栏第二章,从本章开始正式进入SpringBoot的WEB阶段开发,本章先带你认识yaml配置文件和资源的路径配置原理,以方便在后面的文章中打下基础
336 4
|
10月前
|
存储 前端开发 Java
Springboot静态资源映射及文件映射
在Spring Boot项目中,为了解决前端访问后端存储的图片问题,起初尝试通过静态资源映射实现,但发现这种方式仅能访问打包时已存在的文件。对于动态上传的图片(如头像),需采用资源映射配置,将特定路径映射到服务器上的文件夹,确保新上传的图片能即时访问。例如,通过`addResourceHandler(&quot;/img/**&quot;).addResourceLocations(&quot;file:E:\\myProject\\forum_server\\&quot;)`配置,使前端可通过URL直接访问图片。
663 0
Springboot静态资源映射及文件映射
|
11月前
|
XML Java 应用服务中间件
Spring Boot 两种部署到服务器的方式
本文介绍了Spring Boot项目的两种部署方式:jar包和war包。Jar包方式使用内置Tomcat,只需配置JDK 1.8及以上环境,通过`nohup java -jar`命令后台运行,并开放服务器端口即可访问。War包则需将项目打包后放入外部Tomcat的webapps目录,修改启动类继承`SpringBootServletInitializer`并调整pom.xml中的打包类型为war,最后启动Tomcat访问应用。两者各有优劣,jar包更简单便捷,而war包适合传统部署场景。需要注意的是,war包部署时,内置Tomcat的端口配置不会生效。
2724 17
Spring Boot 两种部署到服务器的方式
|
9月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——拦截自定义异常
本文介绍了在实际项目中如何拦截自定义异常。首先,通过定义异常信息枚举类 `BusinessMsgEnum`,统一管理业务异常的代码和消息。接着,创建自定义业务异常类 `BusinessErrorException`,并在其构造方法中传入枚举类以实现异常信息的封装。最后,利用 `GlobalExceptionHandler` 拦截并处理自定义异常,返回标准的 JSON 响应格式。文章还提供了示例代码和测试方法,展示了全局异常处理在 Spring Boot 项目中的应用价值。
452 0
|
9月前
|
Java 数据库 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——指定项目配置文件
在实际项目中,开发环境和生产环境的配置往往不同。为简化配置切换,可通过创建 `application-dev.yml` 和 `application-pro.yml` 分别管理开发与生产环境配置,如设置不同端口(8001/8002)。在 `application.yml` 中使用 `spring.profiles.active` 指定加载的配置文件,实现环境快速切换。本节还介绍了通过配置类读取参数的方法,适用于微服务场景,提升代码可维护性。课程源码可从 [Gitee](https://gitee.com/eson15/springboot_study) 下载。
377 0
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
575 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
安全 Java 关系型数据库
springboot整合springsecurity,从数据库中认证
本文介绍了如何在SpringBoot应用中整合Spring Security,并从数据库中进行用户认证的完整步骤,包括依赖配置、数据库表创建、用户实体和仓库接口、用户详情服务类、安全配置类、控制器类以及数据库初始化器的实现。
1388 3
springboot整合springsecurity,从数据库中认证
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
786 2
springboot静态资源目录访问,及自定义静态资源路径,index页面的访问
本文介绍了Spring Boot中静态资源的访问位置、如何进行静态资源访问测试、自定义静态资源路径和静态资源请求映射,以及如何处理自定义静态资源映射对index页面访问的影响。提供了两种解决方案:取消自定义静态资源映射或编写Controller来截获index.html的请求并重定向。
springboot静态资源目录访问,及自定义静态资源路径,index页面的访问
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
514 2