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

Posted

技术标签:

【中文标题】Struts2 中的文件上传以及 Spring CSRF 令牌【英文标题】:File upload in Struts2 along with the Spring CSRF token 【发布时间】:2014-02-27 07:36:24 【问题描述】:

我用过,

Spring Framework 4.0.0 发布(GA) Spring Security 3.2.0 发布(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 安全性不可用,除非正确配置 MultipartFilterMultipartResolver,以便 Spring 处理多部分请求。

web.xml中的MultipartFilter配置如下。

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

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

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

CSRF 令牌现在由 Spring 安全性接收,但这样做会在 Struts 中引发另一个问题。

上传的文件现在在 Struts 动作类中为 null,如下所示。

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

长话短说:如果让 Spring 处理多部分请求,如何在 Struts 操作类中获取文件?另一方面,如果 Spring 用于处理多部分请求,那么它会丢失安全令牌。如何克服这种情况?

【问题讨论】:

也许尝试将struts2过滤器移到Spring MultipartFilter之前。 如果struts2 过滤器移动到MultipartFilter 之前,它会抱怨身份验证引发异常An Authentication object was not found in the SecurityContext。此外,MultipartFilter 必须放在springSecurityFilterChain 之前,否则令牌将不可用,以防请求是多部分请求。 在这种情况下,尝试将 struts2 过滤模式从 /* 更改为 *.action 如果过滤器模式*.actionstruts2过滤器移动到MultipartFilter之前,则完全跳过安全策略。所有资源都无需任何身份验证即可公开访问。 【参考方案1】:

看来你最好的办法是创建一个自定义的MultiPartRequest implementation 来委托给 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"/>

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

完成此操作后,您的 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 上找到完整的工作示例

【讨论】:

非常有价值的答案。很高兴您抽出时间来编写它和示例 试一试。这个完整的示例完全按原样工作。非常感谢您花时间。但是有一件事,我要问:当没有文件上传时,在这个实现中getContentType()方法中接收到的内容类型是application/octet-stream(否则接收到的内容类型是根据上传的文件,@例如,987654329@ 用于jpg 图像)。这是在做正确的事吗? @Tiny 您的浏览器很可能将 Content-Type 默认为 application/octet-stream ,这就是它的来源。我知道在使用 Chrome 时会发生这种情况。您可以使用 Chrome 开发工具查看请求来验证这一点。因此,假设您的浏览器将 application/octet-stream 作为默认内容类型发送,答案是“是的,它的行为正确”。 是的,当没有文件上传时,chrome 开发者工具显示application/octet-stream。因此,没关系。顺便说一句,当没有上传文件时,在操作类中接收到的文件应该为空,但文件对象被初始化为类似upload5500525321992133691.dat 的文件名,从而抑制了强制文件验证。为了避免这种情况(将文件初始化为null,当文件浏览中没有选择文件时),我在解析方法的最里面的foreach 循环中添加了一个额外的条件检查,如if(!file.isEmpty())...。会不会有副作用? parse()方法的略微修改版本可以看here。这是实验性的,我不应该自己决定。赏金将于明天关闭。非常感谢。【参考方案2】:

表单编码multipart/formdata是用于文件上传的场景,这是根据W3C documentation:

内容类型“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 攻击的常用方法是在 HTTP 请求标头而不是 POST 正文中发送 CSRF 令牌。为此,您需要将其设为 ajax POST。

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

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

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

【讨论】:

如果我想在标头中提供令牌,是否需要使用 AJAX(CRUD 操作)执行所有操作?如果是的话,那就很痛苦了。 其余的仍然可以在隐藏字段中使用带有令牌的表单。但是对于文件上传表单,传递 CSRF 令牌的方式是通过请求标头,并且只能使用 ajax 注意,如果你使用AJAX,你需要设置内容安全策略指令。并且可能您需要考虑在所有&lt;script&gt; 标签中添加'nonce-...'。不推荐这种方法,因为它是一个兔子洞。【参考方案3】:

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

我在使用 Spring MVC 而不是 Struts 时遇到了类似的问题,在 Spring Security 团队的帮助下我得以解决。详情请见this answer。

您还可以将您的设置与on Github 提供的工作示例进行比较。我已经在 Tomcat 7、JBoss AS 7、Jetty 和 Weblogic 上对此进行了测试。

如果这些都不起作用,如果您可以创建一个带有演示问题的配置的单个控制器、单页应用程序并将其上传到某处,将会很有帮助。

【讨论】:

如果是单独使用Spring MVC的话,是没有问题的。问题中描述的配置足以使其工作,当有一个多部分请求但两个(或可能更多)框架的集成有时会很痛苦并且很笨拙,就像在这种情况下,Spring 开发人员不太可能愿意回复,因为有是 Struts 和 Struts 开发人员也可能因为 Spring 而不愿意回复。这种问题该问谁,不知道,哈哈:) 如果您可以在某处上传示例应用程序,我可以帮助您进行设置。我已经将 Spring Security 与 Struts 和 JSF 一起使用,所以不介意看看你当前的设置并帮助它工作。 登录后尝试了几个小时在 GitHub 上上传,但找不到上传的方法。 在 Github 上,您必须创建一个 Git 存储库,然后将您的示例代码提交给它。如果你使用的是 Windows 机器,Github 为 Windows 提供了一个 Git 客户端。大多数 Unix 发行版都有自己的 Git 客户端。或者,如果您有一个 ZIP 文件,您可能希望将其上传到 DropBox、Google Drive、SkyDrive 或 Box(如果您有任何这些帐户)并与公众共享上传的文件。一旦我们调试了问题,您就可以删除该文件。 如果你不介意的话,我可以考虑在某个地方上传一个简单的项目,但作为初学者,我使用 NetBeans,其中一个复杂的 ant 脚本由 IDE 本身自动生成。因此,我的应用程序中没有pom.xml 文件,我也不能自己编写它(我还没有构建一个 Maven 项目)。没有这个文件你能管理吗?【参考方案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

【讨论】:

当配置MultipartFilter 时,这种现象应该在底层发生。不需要手动获取 Struts 动作类中的文件。不应该吗? @Tiny 好吧,你可以告诉 Struts 的开发人员:)

以上是关于Struts2 中的文件上传以及 Spring CSRF 令牌的主要内容,如果未能解决你的问题,请参考以下文章

struts2学习(13)struts2文件上传和下载

Struts2文件上传

struts2的常量

Struts2的使用以及Spring整合Struts2

Struts2 文件上传

struts2学习(14)struts2文件上传和下载多个文件上传和下载