Struts2中的文件上传以及Spring CSRF令牌

时间:2014-02-03 17:32:01

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

我用,

  • Spring Framework 4.0.0 RELEASE(GA)
  • Spring Security 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>

这是一个多部分请求,其中除了MultipartFilterMultipartResolver以及MultipartFilter已正确配置以便Spring处理多部分请求之外,CSR安全性无法使用CSRF令牌。

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> 注册如下。

null

现在,Spring安全性收到了CSRF令牌,但这样做会导致Struts出现另一个问题。

现在,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; } } ,如下所示。

null

这是因为我的猜测,多部分请求已经被Spring处理和使用,因此Struts不能将其作为多部分请求使用,因此,Struts动作类中的文件对象是<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>

有办法解决这种情况吗?否则,我现在唯一的选择是将令牌作为查询字符串参数附加到URL,这是非常不鼓励的,根本不推荐。

{{1}}

长话短说:如果Spring处理多个请求,如何在Struts动作类中获取文件?另一方面,如果Spring 处理多部分请求,那么它就会使安全令牌成为可能。如何克服这种情况?

4 个答案:

答案 0 :(得分:9)

似乎最好的办法是创建一个委托给Spring的MultipartRequest的自定义MultiPartRequest implementation。以下是一个示例实现:

<强>样品/ 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"/>

警告:绝对有必要确保通过正确设置bean的范围为每个多部分请求创建一个新的MultipartRequest实例,否则您将看到竞争条件。

执行此操作后,您的Struts操作将添加文件信息,就像之前一样。请记住,文件验证(即文件大小)现在使用filterMultipartResolver而不是Struts完成。

使用主题自动包含CSRF令牌

您可以考虑创建自定义主题,以便您可以在表单中自动包含CSRF令牌。有关如何执行此操作的详细信息,请参阅http://struts.apache.org/release/2.3.x/docs/themes-and-templates.html

Github上的完整示例

您可以在https://github.com/rwinch/struts2-upload

的github上找到完整的工作示例

答案 1 :(得分:4)

表单编码multipart/formdata旨在用于文件上传方案,这是根据W3C documentation

  

内容类型&#34; multipart / form-data&#34;应该用于提交   包含文件,非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攻击的文件上载请求的常用方法是在HTTP请求标头而不是POST主体中发送CSRF令牌。为此,你需要使它成为一个ajax POST。

对于正常的POST,无法执行此操作,请参阅此answer。要么使POST成为ajax请求并使用一些Javascript添加标头,要么将CSRF令牌作为URL参数发送,如上所述。

如果CSRF令牌经常被重新生成,理想情况下应该在请求之间重新生成,那么将其作为请求参数发送则不是问题,可能是可以接受的。

在服务器端,您需要配置CSRF解决方案以从标头中读取令牌,这通常是使用CSRF解决方案预见的。

答案 2 :(得分:1)

乍一看,您的配置对我来说是正确的。因此,我认为这个问题可能会在某处出现一些错误配置。

我遇到了类似Spring Spring MVC而不是Struts的问题,我可以在Spring Security团队的帮助下解决这个问题。有关详细信息,请参阅this answer

您也可以将自己的设置与可用的工作样本on Github进行比较。我在Tomcat 7,JBoss AS 7,Jetty和Weblogic上测试了这个。

如果这些不起作用,如果您可以使用配置演示单个控制器,单页面应用程序来演示问题并将其上传到某个位置,将会很有帮助。

答案 3 :(得分:1)

我不是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
}