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

Я использую,

  • ВЫПУСК Spring Framework 4.0.0 (GA)
  • Spring Security 3.2.0 ВЫПУСК (GA)
  • Стойки 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 не обрабатывает многокомпонентный запрос, он создает токен безопасности. Как выйти из этой ситуации?


person Tiny    schedule 03.02.2014    source источник
comment
Возможно, попробуйте переместить struts2 фильтр перед Spring MultipartFilter.   -  person Kamil Chaber    schedule 08.02.2014
comment
Если фильтр struts2 перемещается перед фильтром MultipartFilter, он жалуется на то, что проверка подлинности вызывает исключение An Authentication object was not found in the SecurityContext. Кроме того, MultipartFilter должен стоять перед springSecurityFilterChain, иначе токен будет недоступен, если запрос состоит из нескольких частей.   -  person Tiny    schedule 08.02.2014
comment
В этом случае попробуйте изменить шаблон фильтра struts2 с /* на *.action.   -  person Kamil Chaber    schedule 08.02.2014
comment
В случае, если шаблон фильтра *.action передается фильтру struts2 после его перемещения перед MultipartFilter, стратегия безопасности полностью пропускается. Доступ ко всем ресурсам осуществляется публично без какой-либо аутентификации.   -  person Tiny    schedule 08.02.2014


Ответы (4)


Кажется, лучше всего создать собственный реализация 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 будет добавлена ​​информация о файле, как и раньше. Имейте в виду, что проверка файла (т. е. размера файла) теперь выполняется с помощью filterMultipartResolver, а не Struts.

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

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

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

Вы можете найти полный рабочий образец на github по адресу https://github.com/rwinch/struts2-upload< /а>

person Rob Winch    schedule 12.02.2014
comment
Очень ценный ответ. Я рад, что вы нашли время написать и это, и пример - person Andrea Ligios; 13.02.2014
comment
Учитывая это попробовать. Этот полный пример работает именно так, как есть. Большое спасибо за потраченное время. Однако я должен спросить одну вещь: когда файлы не загружены, тип контента, полученный в методе getContentType() в этой реализации, равен application/octet-stream (в противном случае полученный тип контента соответствует загруженному файлу, image/jpeg для изображения jpg , Например). Это правильно? - person Tiny; 13.02.2014
comment
@Tiny Ваш браузер, скорее всего, по умолчанию использует для Content-Type значение application/octet-stream, и именно отсюда он исходит. Я знаю, что это происходит со мной при использовании Chrome. Вы можете просмотреть запрос с помощью инструментов Chrome Dev, чтобы убедиться в этом. Итак, если ваш браузер отправляет application/octet-stream в качестве типа контента по умолчанию, ответ будет «Да», он ведет себя правильно. - person Rob Winch; 13.02.2014
comment
Да, инструмент разработчика Chrome показывает application/octet-stream, когда файл не загружен. Поэтому все в порядке. Кстати, когда файл не загружен, файл, полученный в классе действий, должен быть нулевым, но объект файла инициализируется именем файла, например upload5500525321992133691.dat, подавляющим обязательную проверку файла. Чтобы избежать этого (чтобы инициализировать файл как null, когда при просмотре файлов не выбран ни один файл), я добавил одну дополнительную условную проверку во внутренний самый внутренний цикл foreach в методе синтаксического анализа, таком как if(!file.isEmpty()){...}. Может ли это иметь какой-то побочный эффект? - person Tiny; 13.02.2014
comment
Слегка измененную версию метода parse() можно увидеть здесь. Это экспериментально, и я не должен решать сам. Баунти будет закрыта завтра. Спасибо большое. - person Tiny; 13.02.2014
comment
С любым кодом вы захотите убедиться, что вы хорошо его протестировали (я только быстро собрал это), но я думаю, что дополнительная проверка, которую вы добавили, в порядке. - person Rob Winch; 13.02.2014

Кодировка формы 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.

person Angular University    schedule 09.02.2014
comment
Нужно ли мне выполнять все с помощью AJAX (операции CRUD), если я думаю о предоставлении токена в заголовке? Если да, то это больно. - person Tiny; 12.02.2014
comment
остальные могут по-прежнему использовать формы с токеном в скрытом поле. Но для форм загрузки файлов способ передачи токена CSRF — через заголовок запроса, и это можно сделать только с помощью ajax. - person Angular University; 12.02.2014

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

Я столкнулся с аналогичной проблемой с Spring MVC вместо Struts, которую мне удалось решить с помощью команды Spring Security. Для получения полной информации см. этот ответ .

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

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

person manish    schedule 10.02.2014
comment
Если это только Spring MVC, то проблем нет. Конфигураций, описанных в вопросе, достаточно, чтобы заставить его работать, когда есть составной запрос, но интеграция двух (или, возможно, более) фреймворков иногда болезненна и неуклюжа, как в этом случае, разработчики Spring вряд ли захотят отвечать, потому что есть Разработчики Struts и Struts также, вероятно, не захотят отвечать из-за Spring. Кому такие вопросы задавать, не знаю, ха-ха :) - person Tiny; 10.02.2014
comment
Я мог бы помочь настроить вашу работу, если вы можете загрузить пример приложения куда-нибудь. Я использовал Spring Security со Struts и JSF, поэтому не возражаю взглянуть на вашу текущую настройку и помочь заставить ее работать. - person manish; 11.02.2014
comment
В течение нескольких часов пытался загрузить на GitHub после входа в систему, но не смог найти способ загрузить на него. - person Tiny; 11.02.2014
comment
На Github вам нужно будет создать репозиторий Git, а затем зафиксировать в нем свой пример кода. Github предоставляет клиент Git для Windows, если вы используете компьютер с Windows. Большинство дистрибутивов Unix имеют собственные клиенты Git. В качестве альтернативы, если у вас есть ZIP-файл, вы можете загрузить его в DropBox, Google Drive, SkyDrive или Box, если у вас есть какая-либо из этих учетных записей, и поделиться загруженным файлом со всеми. После того, как мы отладили проблему, вы можете удалить файл. - person manish; 11.02.2014
comment
Если вы не возражаете, я могу подумать о загрузке простого проекта куда-нибудь, но как новичок я использую NetBeans, в котором сложный сценарий ant создается автоматически самой IDE. Поэтому у меня нет файла pom.xml в моем приложении, и я не могу его написать сам (я еще не построил проект Maven). Можно ли обойтись без этого файла? - person Tiny; 12.02.2014
comment
Конечно, я работаю с файлами ANT и Netbeans, поэтому для меня это не будет проблемой. - person manish; 12.02.2014
comment
Загрузил приложение сюда, прямая ссылка. Мне пришлось исключить все файлы jar из папки WEB-INF/lib из-за ограничения передачи. Вам нужно будет изменить user и password в файле context.xml под META-INF. В своем приложении я использую Spring 4.0.0 GA, Hibernate 4.2.7 final, Spring Security 3.2.0 GA и Tomcat 7.0.35. Можете ли вы создать совместимую среду? - person Tiny; 13.02.2014
comment
Приложение содержит только одну страницу JSP с одним элементом файла, кнопкой отправки и соответствующим классом действия. После успешного входа он будет перенаправлен непосредственно на эту страницу JSP, Test.jsp. Загрузите его в свое время. Ссылка действительна через 5 дней. Спасибо. - person Tiny; 13.02.2014
comment
Я скачал ваш пример приложения. Посмотрю и свяжусь с вами в ближайшее время. - person manish; 13.02.2014
comment
Спасибо маниш. Кстати, принятый ответ теперь работает как положено. Если у вас есть новый способ, не забудьте обновить ответ. Спасибо. - person Tiny; 13.02.2014

Я не пользователь 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
}
person holmis83    schedule 10.02.2014
comment
Это явление должно происходить глубоко под капотом, когда настроен MultipartFilter. Не должно быть необходимости получать файл в классах действий Struts вручную. Разве не должно? - person Tiny; 12.02.2014
comment
@Tiny Ну, ты можешь сказать об этом разработчикам Struts :) - person holmis83; 12.02.2014