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 安全性不可用,除非正确配置 MultipartFilter
和 MultipartResolver
,以便 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.xml
,MultipartResolver
注册如下。
<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
。
如果过滤器模式*.action
在struts2
过滤器移动到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,你需要设置内容安全策略指令。并且可能您需要考虑在所有<script>
标签中添加'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 令牌的主要内容,如果未能解决你的问题,请参考以下文章