【问题标题】:Spring CSRF token does not work, when the request to be sent is a multipart request当要发送的请求是多部分请求时,Spring CSRF 令牌不起作用
【发布时间】:2014-02-26 03:43:05
【问题描述】:

我用过,

  • Spring Framework 4.0.0 发布(GA)
  • Spring Security 3.2.0 发布(GA)
  • Struts 2.3.16

其中,我使用内置的安全令牌来防范 CSRF 攻击。

Struts 表单如下所示。

<s:form namespace="/admin_side"
        action="Category"
        enctype="multipart/form-data"
        method="POST"
        validate="true"
        id="dataForm"
        name="dataForm">

    <s:hidden name="%{#attr._csrf.parameterName}"
              value="%{#attr._csrf.token}"/>
</s:form>

生成的HTML代码如下。

<form id="dataForm"
      name="dataForm"
      action="/TestStruts/admin_side/Category.action"
      method="POST"
      enctype="multipart/form-data">

    <input type="hidden"
           name="_csrf"
           value="3748c228-85c6-4c3f-accf-b17d1efba1c5" 
           id="dataForm__csrf">
</form>

这很好用,除非请求是多部分的,在这种情况下,请求以状态码 403 结束。

HTTP 状态 403 - 在请求中发现无效的 CSRF 令牌“null” 参数“_csrf”或标头“X-CSRF-TOKEN”。

类型状态报告

message 在请求参数中发现无效的 CSRF Token 'null' '_csrf' 或标题 'X-CSRF-TOKEN'。

说明已禁止访问指定资源。

spring-security.xml文件如下。

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
           http://www.springframework.org/schema/security
           http://www.springframework.org/schema/security/spring-security-3.2.xsd">

    <http pattern="/Login.jsp*" security="none"></http>

    <http auto-config='true' use-expressions="true" disable-url-rewriting="true" authentication-manager-ref="authenticationManager">
        <session-management session-fixation-protection="newSession">
            <concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
        </session-management>

        <csrf/>

        <headers>
            <xss-protection />
            <frame-options />
            <!--<cache-control />-->
            <!--<hsts />-->
            <content-type-options /> <!--content sniffing-->
        </headers>

        <intercept-url pattern="/admin_side/**" access="hasRole('ROLE_ADMIN')" requires-channel="any"/>
        <form-login login-page="/admin_login/Login.action" authentication-success-handler-ref="loginSuccessHandler" authentication-failure-handler-ref="authenticationFailureHandler"/>
        <logout logout-success-url="/admin_login/Login.action" invalidate-session="true" delete-cookies="JSESSIONID"/>
    </http>

    <beans:bean id="encoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

    <beans:bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
        <beans:property name="userDetailsService" ref="userDetailsService"/>
        <beans:property name="passwordEncoder" ref="encoder" />
    </beans:bean>

    <beans:bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
        <beans:property name="providers">
            <beans:list>
                <beans:ref bean="daoAuthenticationProvider" />
            </beans:list>
        </beans:property>
    </beans:bean>

    <authentication-manager>
        <authentication-provider user-service-ref="userDetailsService">
        </authentication-provider>
    </authentication-manager>

    <beans:bean id="loginSuccessHandler" class="loginsuccesshandler.LoginSuccessHandler"/>
    <beans:bean id="authenticationFailureHandler" class="loginsuccesshandler.AuthenticationFailureHandler" />

    <global-method-security secured-annotations="enabled" proxy-target-class="false" authentication-manager-ref="authenticationManager">
        <protect-pointcut expression="execution(* admin.dao.*.*(..))" access="ROLE_ADMIN"/>
    </global-method-security>
</beans:beans>

那么,当请求是多部分的时,在哪里寻找这个令牌? (这根本不应该与 Struts 相关。)

UserDetailsService 的实现可以在我之前的问题this 中找到,如果需要的话。


Placing MultipartFilter before Spring Security 也没有帮助。

web.xml 文件如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
         xmlns="http://java.sun.com/xml/ns/javaee" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
         http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/applicationContext.xml
            /WEB-INF/spring-security.xml
        </param-value>
    </context-param>

    <filter>
        <filter-name>MultipartFilter</filter-name>
        <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
    </filter>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>MultipartFilter</filter-name>
        <servlet-name>/*</servlet-name>
    </filter-mapping>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>AdminLoginNocacheFilter</filter-name>
        <filter-class>filter.AdminLoginNocacheFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>AdminLoginNocacheFilter</filter-name>
        <url-pattern>/admin_login/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>NoCacheFilter</filter-name>
        <filter-class>filter.NoCacheFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>NoCacheFilter</filter-name>
        <url-pattern>/admin_side/*</url-pattern>
    </filter-mapping>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <listener>
        <description>Description</description>
        <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
    </listener>

    <listener>
        <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
    </listener>

    <filter>
        <filter-name>struts2</filter-name>
        <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
        <init-param>
            <param-name>struts.devMode</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>struts2</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

仅当令牌作为查询字符串参数附加时才有效,但不鼓励这样做。

<s:form namespace="/admin_side"
        action="Category?%{#attr._csrf.parameterName}=%{#attr._csrf.token}"
        enctype="multipart/form-data"
        method="POST"
        validate="true"
        id="dataForm"
        name="dataForm">
    ...
<s:form>

【问题讨论】:

    标签: spring struts2 spring-security csrf csrf-protection


    【解决方案1】:

    如果你使用@annotations,并且jsp视图是这样的:

        <form:form id="profileForm" action="profile?id=${param.id}" method="POST" 
              modelAttribute="appUser" enctype="multipart/form-data" >
                 ...
                <input type="file" name="file">
                 ...
                <input type="hidden" name="${_csrf.parameterName}"
                    value="${_csrf.token}" />
        </form:form>
    

    这可能会有所帮助:

    AppConfig.java:

    @EnableWebMvc
    @Configuration
    @Import({ SecurityConfig.class })
    public class AppConfig {
    
       @Bean(name = "filterMultipartResolver")
       public CommonsMultipartResolver filterMultipartResolver() {
          CommonsMultipartResolver filterMultipartResolver = new CommonsMultipartResolver();
          filterMultipartResolver.setDefaultEncoding("utf-8");
          // resolver.setMaxUploadSize(512000);
          return filterMultipartResolver;
    }
    ...
    

    SecurityConfig.java 扩展了 WebSecurityConfigurerAdapter 并且是 SpringSecurity 的配置

    multipart/form-data 过滤器(MultipartFilter)需要在启用 CSRF 的 SecurityConfig 之前注册。你可以这样做:

    SecurityInitializer.java:

    public class SecurityInitializer extends
    AbstractSecurityWebApplicationInitializer {
    
    @Override
    protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
       super.beforeSpringSecurityFilterChain(servletContext);
    
       // CSRF for multipart form data filter:
       FilterRegistration.Dynamic springMultipartFilter;
       springMultipartFilter = servletContext.addFilter(
        "springMultipartFilter", new MultipartFilter());
       springMultipartFilter.addMappingForUrlPatterns(null, false, "/*");
    
    }
    }
    

    【讨论】:

      【解决方案2】:

      在这种情况下,因为它是一个多部分请求,其中 CSRF 令牌对 Spring 安全性不可用,除非正确配置 MultipartFilterMultipartResolver,以便 Spring 可以处理多部分请求。

      applicationContext.xml文件中的MulipartResolver必须注册如下

      <bean id="filterMultipartResolver" 
            class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> 
      
          <property name="maxUploadSize" value="-1" />
      </bean> 
      

      maxUploadSize 的属性值-1 对上传的文件大小没有限制。该值可能会因要求而异。如果是多个文件,则文件大小为所有上传文件的大小。


      还有,

      <servlet-name>/*</servlet-name> 
      

      &lt;filter-mapping&gt;MultipartFilter 需要更改为

      <url-pattern>/*</url-pattern>
      

      这是documentation 中的bug

      这可以正常工作,以防万一它是单独的 Spring MVC。

      但如果它是 Spring 和 Struts(2) 的集成,则会在关联的 Struts 动作类中引发另一个问题。上传文件的信息将在关联的 Struts 动作类中为null

      要解决此特定问题,请参阅this 答案以自定义multipart request

      【讨论】:

        【解决方案3】:

        我通过以下方式解决了这个问题:

        • 使用普通 javascript 发送多部分文件,如 Mozilla's guide
        • 在 HTML 标头中的元标记中添加 _csrf 标记,就像在 sending the CSRF token with Ajax 的 Spring 指南中一样
        • 不使用jquery,直接添加到XHR对象中

          var csrfToken = $("meta[name='_csrf']").attr("content");
          var csrfHeader = $("meta[name='_csrf_header']").attr("content");
          XHR.setRequestHeader(csrfHeader, csrfToken);
          XHR.setRequestHeader('Content-Type','multipart/form-data; boundary=' + boundary);
          
          XHR.send(data);
          

        【讨论】:

          【解决方案4】:

          在 Spring Boot + Security + CSRF + Multipart 的情况下,multipart 文件既不绑定 ModelAttribure 也不绑定 RequestParam(MultipartFile 文件)

          下面的代码对我来说很好。

          1.MvcConfiguration.java

          @Configuration
          @EnableWebMvc
          @ComponentScan
          public class MvcConfiguration extends WebMvcConfigurerAdapter { 
          
          .......
          ......
          
          /*
               * Case : Spring Boot + Security + CSRF + Mulitpart 
               * In this case, since it is a multipart request in which the CSRF token is unavailable to Spring security unless MultipartFilter along with MultipartResolver 
               * is properly configured so that the multipart request can be processed by Spring.
               * 
               * And 
               * 
               * The multipart/form-data filter (MultipartFilter) needs to be registered before the SecurityConfig that enables the CSRF.
               * So that's why 
               * 1. reg.setOrder(1); //below
               * 2. security.filter-order=2 // in application.properties
               */
          
              @Bean
              public FilterRegistrationBean registerMultipartFilter() {
                  FilterRegistrationBean reg = new FilterRegistrationBean(new MultipartFilter());
                  reg.setOrder(1);
                  return reg;
              }
          
              @Bean(name = "filterMultipartResolver")
              public CommonsMultipartResolver filterMultipartResolver() {
                  CommonsMultipartResolver filterMultipartResolver = new CommonsMultipartResolver();
                  filterMultipartResolver.setDefaultEncoding("utf-8");
                  // resolver.setMaxUploadSize(512000);
                  return filterMultipartResolver;
              }
          .....
          .....
          }
          

          2。应用程序.properties

          security.filter-order=2
          

          【讨论】:

            【解决方案5】:

            你可以禁用 csrf - httpSecurity.csrf().disable();

             @Configuration
                public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
            
                @Override
                protected void configure(HttpSecurity httpSecurity) throws Exception {
                    ...
                    httpSecurity.csrf().disable();
                    ...
                }
            }
            

            【讨论】:

            • 没有。禁用该令牌不是解决方案。它将打开 CSRF 漏洞。
            猜你喜欢
            • 2012-08-12
            • 1970-01-01
            • 2020-05-25
            • 1970-01-01
            • 2018-06-09
            • 1970-01-01
            • 2019-12-06
            相关资源
            最近更新 更多