Загрузка файла в Struts2 вместе с токеном Spring CSRF

Я использую

  • Spring Framework 4.0.0 RELEASE (GA)
  • Spring Безопасность 3.2.0 RELEASE (GA)
  • Struts 2.3.16

В котором я использую встроенный токен безопасности для защиты от атак CSRF.

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

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

MultipartFilter в 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>
        <url-pattern>/*</url-pattern>
    </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>

И в applicationContext.xml, MultipartResolver регистрируется следующим образом.

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

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

Теперь токен CSRF получен с помощью Spring безопасности, но при этом возникает проблема в Struts.

Загруженные файлы теперь null в классах действий Struts, как показано ниже.

@Namespace("/admin_side")
@ResultPath("/WEB-INF/content")
@ParentPackage(value="struts-default")
public final class CategoryAction extends ActionSupport implements Serializable, ValidationAware, ModelDriven<Category>
{
    private File fileUpload;
    private String fileUploadContentType;
    private String fileUploadFileName;
    private static final long serialVersionUID = 1L;

    //Getters and setters.

    //Necessary validators as required.
    @Action(value = "AddCategory",
        results = {
            @Result(name=ActionSupport.SUCCESS, type="redirectAction", params={"namespace", "/admin_side", "actionName", "Category"}),
            @Result(name = ActionSupport.INPUT, location = "Category.jsp")},
        interceptorRefs={
            @InterceptorRef(value="defaultStack", "validation.validateAnnotatedMethodOnly", "true"})
        })
    public String insert(){
        //fileUpload, fileUploadContentType and fileUploadFileName are null here after the form is submitted.
        return ActionSupport.SUCCESS;
    }

    @Action(value = "Category",
            results = {
                @Result(name=ActionSupport.SUCCESS, location="Category.jsp"),
                @Result(name = ActionSupport.INPUT, location = "Category.jsp")},
            interceptorRefs={
                @InterceptorRef(value="defaultStack", params={ "validation.validateAnnotatedMethodOnly", "true", "validation.excludeMethods", "load"})})
    public String load() throws Exception{
        //This method is just required to return an initial view on page load.
        return ActionSupport.SUCCESS;
    }
}

Это происходит потому, что, по моему мнению, многопользовательский запрос уже обрабатывается и потребляется Spring, следовательно, он недоступен для Struts в качестве многостраничного запроса, и поэтому файловый объект в Struts класс действия null.

Есть ли способ обойти эту ситуацию? В противном случае, теперь я оставил единственную возможность добавить токен к URL-адресу в качестве параметра строки запроса, который сильно обескуражен и не рекомендуется вообще.

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

Короче говоря: Как получить файлы в классе действий Struts, если Spring создан для обработки запроса мульпарта? С другой стороны, если Spring не, сделанный для обработки множественного запроса, то он оденет маркер безопасности. Как преодолеть эту ситуацию?

Ответ 1

Кажется, что лучше всего создать пользовательскую реализацию MultiPartRequest, которая делегирует Spring MultipartRequest. Вот пример реализации:

образец /SpringMultipartParser.java

package sample;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map.Entry;

import javax.servlet.http.HttpServletRequest;

import org.apache.struts2.dispatcher.multipart.MultiPartRequest;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.util.WebUtils;

import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;

public class SpringMultipartParser implements MultiPartRequest {
    private static final Logger LOG = LoggerFactory.getLogger(MultiPartRequest.class);

    private List<String> errors = new ArrayList<String>();

    private MultiValueMap<String, MultipartFile> multipartMap;

    private MultipartHttpServletRequest multipartRequest;

    private MultiValueMap<String, File> multiFileMap = new LinkedMultiValueMap<String, File>();

    public void parse(HttpServletRequest request, String saveDir)
            throws IOException {
        multipartRequest =
                WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);

        if(multipartRequest == null) {
            LOG.warn("Unable to MultipartHttpServletRequest");
            errors.add("Unable to MultipartHttpServletRequest");
            return;
        }
        multipartMap = multipartRequest.getMultiFileMap();
        for(Entry<String, List<MultipartFile>> fileEntry : multipartMap.entrySet()) {
            String fieldName = fileEntry.getKey();
            for(MultipartFile file : fileEntry.getValue()) {
                File temp = File.createTempFile("upload", ".dat");
                file.transferTo(temp);
                multiFileMap.add(fieldName, temp);
            }
        }
    }

    public Enumeration<String> getFileParameterNames() {
        return Collections.enumeration(multipartMap.keySet());
    }

    public String[] getContentType(String fieldName) {
        List<MultipartFile> files = multipartMap.get(fieldName);
        if(files == null) {
            return null;
        }
        String[] contentTypes = new String[files.size()];
        int i = 0;
        for(MultipartFile file : files) {
            contentTypes[i++] = file.getContentType();
        }
        return contentTypes;
    }

    public File[] getFile(String fieldName) {
        List<File> files = multiFileMap.get(fieldName);
        return files == null ? null : files.toArray(new File[files.size()]);
    }

    public String[] getFileNames(String fieldName) {
        List<MultipartFile> files = multipartMap.get(fieldName);
        if(files == null) {
            return null;
        }
        String[] fileNames = new String[files.size()];
        int i = 0;
        for(MultipartFile file : files) {
            fileNames[i++] = file.getOriginalFilename();
        }
        return fileNames;
    }

    public String[] getFilesystemName(String fieldName) {
        List<File> files = multiFileMap.get(fieldName);
        if(files == null) {
            return null;
        }
        String[] fileNames = new String[files.size()];
        int i = 0;
        for(File file : files) {
            fileNames[i++] = file.getName();
        }
        return fileNames;
    }

    public String getParameter(String name) {
        return multipartRequest.getParameter(name);
    }

    public Enumeration<String> getParameterNames() {
        return multipartRequest.getParameterNames();
    }

    public String[] getParameterValues(String name) {
        return multipartRequest.getParameterValues(name);
    }

    public List getErrors() {
        return errors;
    }

    public void cleanUp() {
        for(List<File> files : multiFileMap.values()) {
            for(File file : files) {
                file.delete();
            }
        }

        // Spring takes care of the original File objects
    }
}

Затем вам нужно убедиться, что Struts использует его. Вы можете сделать это в файле struts.xml, как показано ниже:

struts.xml

<constant name="struts.multipart.parser" value="spring"/>
<bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" 
      name="spring" 
      class="sample.SpringMultipartParser"
      scope="default"/>

ПРЕДУПРЕЖДЕНИЕ: абсолютно необходимо убедиться, что для каждого многостраничного запроса создается новый экземпляр MultipartRequest, правильно настроив область действия bean, иначе вы увидите условия гонки.

После этого в ваших действиях Struts будет добавлена ​​информация о файлах, как и раньше. Имейте в виду, что проверка файла (то есть размер файла) теперь выполняется с помощью фильтраMultipartResolver вместо Struts.

Использование Темы для автоматического включения токена CSRF

Вы можете подумать о создании настраиваемой темы, чтобы вы могли автоматически включать токен CSRF в формы. Для получения дополнительной информации о том, как это сделать, см. http://struts.apache.org/release/2.3.x/docs/themes-and-templates.html

Полный пример для Github

Вы можете найти полный рабочий пример github в https://github.com/rwinch/struts2-upload

Ответ 2

Кодирование формы multipart/formdata предназначено для сценариев загрузки файлов, это соответствует документации W3C:

Тип контента "multipart/form-data" должен использоваться для отправки формы, содержащие файлы, данные, отличные от ASCII, и двоичные данные.

Класс MultipartResolver ожидает только загрузку файла, а не другие поля формы, это из javadoc:

/**
 * A strategy interface for multipart file upload resolution in accordance
 * with <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>.
 *
 */

Итак, поэтому добавление CSRF в качестве поля формы не будет работать, обычным способом защиты запросов на загрузку файлов с CSRF-атак является отправка токена CSRF в заголовок HTTP-запроса вместо тела POST. Для этого вам нужно сделать ajax POST.

Для обычного POST нет способа сделать это, см. этот ответ. Либо сделайте POST запрос ajax и добавьте заголовок с некоторым Javascript, либо отправьте токен CSRF в качестве параметра URL, как вы упомянули.

Если токен CSRF часто регенерируется, так как он идеально идеален между запросами, то отправка его в качестве параметра запроса является менее проблемой и может быть приемлемой.

На стороне сервера вам нужно настроить решение CSRF для чтения маркера из заголовка, это обычно предусмотрено используемым решением CSRF.

Ответ 3

На первый взгляд ваша конфигурация выглядит правильно для меня. Поэтому я думаю, что проблема может быть где-то немного неправильной конфигурации.

У меня возникла аналогичная проблема с Spring MVC вместо Struts, которую я смог решить с помощью команды Spring Security. Подробнее см. этот ответ.

Вы также можете сравнить свою настройку с рабочей выборкой в Github. Я тестировал это на Tomcat 7, JBoss AS 7, Jetty и Weblogic.

Если они не работают, будет полезно, если вы можете создать один контроллер, одностраничное приложение с вашей конфигурацией, которое демонстрирует проблему и загружает ее где-нибудь.

Ответ 4

Я не пользователь Struts, но я думаю, вы можете использовать тот факт, что Spring MultipartFilter завершает запрос в MultipartHttpServletRequest.

Сначала возьмите HttpServletRequest, в Struts я думаю, вы можете сделать это примерно так:

ServletRequest request = ServletActionContext.getRequest();

Затем вытащите из него MultipartRequest, завернув упаковку, если необходимо:

MultipartRequest multipart = null;
while (multipart == null)
{
    if (request instanceof MultipartRequest)
        multipart = (MultipartRequest)request;
    else if (request instanceof ServletRequestWrapper)
        request = ((ServletRequestWrapper)request).getRequest();
    else
        break;                
}

Если этот запрос был множественным, введите file по имени ввода формы:

if (multipart != null)
{
    MultipartFile mf = multipart.getFile("forminputname");
    // do your stuff
}