Загрузка файла в 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
}