Spring токен CSRF не работает, когда отправляемый запрос является многостраничным запросом

Я использую

  • Spring Framework 4.0.0 RELEASE (GA)
  • Spring Безопасность 3.2.0 RELEASE (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>

Это отлично работает, если запрос не является multipart, в этом случае запрос заканчивается кодом состояния 403.

Состояние HTTP 403. Недопустимый токен CSRF "null" был найден по запросу параметр "_csrf" или заголовок "X-CSRF-TOKEN".

тип Отчет о состоянии

сообщение Недопустимый токен CSRF "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>

Итак, где искать этот токен, когда запрос multipart? (Это не должно быть связано с Struts вообще.)

Реализация UserDetailsService может быть найдена в этом более раннем вопросе, если это необходимо.


Размещение MultipartFilter до Spring Безопасность тоже не помогло.

Файл 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>

Ответ 1

В этом случае, поскольку это многостраничный запрос, в котором токен CSRF недоступен для безопасности Spring, если только MultipartFilter вместе с MultipartResolver не настроен правильно, так что многопроцессорный запрос может быть обработан с помощью Spring.

MulipartResolver в файле applicationContext.xml должен быть зарегистрирован следующим образом

<bean id="filterMultipartResolver" 
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> 

    <property name="maxUploadSize" value="-1" />
</bean> 

Значение атрибута -1 maxUploadSize не ограничивает размер загруженного файла. Это значение может меняться в зависимости от требований. В случае нескольких файлов размер файла - это размер всех загруженных файлов.


Кроме того,

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

of <filter-mapping> of MultipartFilter необходимо изменить на

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

Это ошибка в документации .

Это будет работать нормально, в случае, если это только Spring MVC.

но если это интеграция Spring и Struts (2), она несет другую проблему в соответствующем классе действий Struts. Информация загруженного файла будет null в соответствующем классе (-ях) действия Struts.

Чтобы решить эту проблему, см. этот ответ, чтобы настроить multipart request.

Ответ 2

Если вы используете @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) должен быть зарегистрирован до SecurityConfig, который разрешает CSRF. Вы можете сделать это с помощью этого:

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, "/*");

}
}

Ответ 3

Я решил эту проблему:

  • отправка многочастного файла с использованием ванильного javascript, например, в руководство Mozilla
  • добавление токена _csrf в заголовке HTML в метатегах, как в Spring для отправка токена CSRF с помощью Ajax
  • вместо использования 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

Вы можете отключить csrf - httpSecurity.csrf(). disable();

 @Configuration
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        ...
        httpSecurity.csrf().disable();
        ...
    }
}