如何配置 Spring 安全性以处理 multipart/form-data (POST) 请求中的 CSRF 令牌?
How can I configure Spring Security to process a CSRF token in a multipart/form-data (POST) request?
场景
- Single-page (AngluarJS) Web 应用程序使用 HTTP API 进行 client-server 通信
- Spring 安全配置为使用 CSRF 保护(通过 XML)
- CSRF 令牌通常在请求中发送 header(工作正常)
- 应用需要在IE9中支持文件上传
问题
文件上传是通过multipart/form-data POST请求实现的。通常这是使用 client-side AJAX 完成的,但 IE9 不支持 FileAPI (http://www.w3.org/TR/FileAPI/).
IE9的work-around是在隐藏的iframe中创建一个表单,然后提交表单。通过将 CSRF 令牌添加为表单输入,将 CSRF 令牌添加到请求 body - 原因是我无法在提交表单之前操纵请求 header 来添加 CSRF header。
Spring 安全的 org.springframework.security.web.csrf.CsrfFilter 首先尝试从 header 中获取 CSRF 令牌,如果没有找到则尝试从参数中获取(通过 HttpServletRequest.getParameter())
这不适用于在 body 中使用 CSRF 令牌的多部分请求 -- getParameter() 将始终 return null。
(顺便说一句,对 getParameter() 的调用还将请求 InputStream 读取到最后,因此我们被迫在请求到达 CsrfFilter 之前包装请求,以便请求 InputStream是 'cached')
我想创建一个调用 getPart() 的 CsrfFilter,但在仍然使用 nice+clean Spring 安全性 XML 命名空间元素时无法这样做。
原因是没有地方可以在配置中包含自定义 CSRF 过滤器 -- 并且 CsrfConfigurer hard-coded 使用 org.springframework.security.web.csrf.CsrfFilter,因此无法注入。
我可以将代码添加到我的请求包装器 class 的重写 getParameter() 方法中,以尝试从多部分请求中解析参数——但实际上这很难做到正确,并且会而是避免此类维护成本。
TL;DR
- 我们无法将 CSRF 令牌添加到请求 headers,需要添加到请求 body
- 请求是 multipart/form-data
- Spring 安全 CSRF 过滤器无法配置为从多部分请求中解析 CSRF 令牌
欢迎提供任何帮助——修复 client-side 或 server-side 的建议!
TIA
您应该阅读参考文献中讨论 CSRF and Multipart requests 的部分。您有两个选择:
每个都有参考文献中描述的 pros/cons。
最终,如果您想提供自定义过滤器,您可以使用 XML element 来实现,它仅引用实现过滤器的 Spring Bean。例如:
<http ...>
...
<custom-filter ref="customCsrfFilter" position="CSRF_FILTER"/>
</http>
我已经实施了一个现在似乎有效的解决方案,但其中有一些我真的不喜欢...
- 仍在使用 Spring 安全的
<csrf>
我在 CSRF 过滤器之前添加了一个自定义过滤器:
<security:custom-filter before="CSRF_FILTER" ref="csrfRequestWrapperFilter" />
- 我已经将相同的 RequestMatcher 注入到 CsrfRequestWrapperFilter 中,以仅处理将检查 CSRF 令牌的请求
- 过滤器 wraps/decorates 在
CsrfProtectedHttpServletRequest
中的 HttpServletRequest
,并继续链(下一个是 CsrfFilter)
CsrfProtectedHttpServletRequest
扩展了 HttpServletRequestWrapper
,并覆盖了 getParameter(String name) 方法。类似于:
... 将 Request InputStream 复制到成员 OutputStream ...
if(this.getContentType() != null && requestBody != null) {
if(this.getContentType().contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE) && requestBody.contains(parameterName+"="))
return getFormEncodedParameter(requestBody, parameterName);
else if(this.getContentType().contains(MediaType.MULTIPART_FORM_DATA_VALUE) && requestBody.contains("name="+"\""+ parameterName +"\"")) {
return getMultipartParameter(requestBody, parameterName);
}
}
}
这两种方法都会进行一些字符串解析...这是我非常不喜欢的部分。它与核心业务逻辑完全无关,我对实现没有 100% 的信心,尽管它正在努力从多部分请求中获取 _csrf
"parameter"。
场景
- Single-page (AngluarJS) Web 应用程序使用 HTTP API 进行 client-server 通信
- Spring 安全配置为使用 CSRF 保护(通过 XML)
- CSRF 令牌通常在请求中发送 header(工作正常)
- 应用需要在IE9中支持文件上传
问题
文件上传是通过multipart/form-data POST请求实现的。通常这是使用 client-side AJAX 完成的,但 IE9 不支持 FileAPI (http://www.w3.org/TR/FileAPI/).
IE9的work-around是在隐藏的iframe中创建一个表单,然后提交表单。通过将 CSRF 令牌添加为表单输入,将 CSRF 令牌添加到请求 body - 原因是我无法在提交表单之前操纵请求 header 来添加 CSRF header。
Spring 安全的 org.springframework.security.web.csrf.CsrfFilter 首先尝试从 header 中获取 CSRF 令牌,如果没有找到则尝试从参数中获取(通过 HttpServletRequest.getParameter())
这不适用于在 body 中使用 CSRF 令牌的多部分请求 -- getParameter() 将始终 return null。
(顺便说一句,对 getParameter() 的调用还将请求 InputStream 读取到最后,因此我们被迫在请求到达 CsrfFilter 之前包装请求,以便请求 InputStream是 'cached')
我想创建一个调用 getPart() 的 CsrfFilter,但在仍然使用 nice+clean Spring 安全性 XML 命名空间元素时无法这样做。
原因是没有地方可以在配置中包含自定义 CSRF 过滤器 -- 并且 CsrfConfigurer hard-coded 使用 org.springframework.security.web.csrf.CsrfFilter,因此无法注入。
我可以将代码添加到我的请求包装器 class 的重写 getParameter() 方法中,以尝试从多部分请求中解析参数——但实际上这很难做到正确,并且会而是避免此类维护成本。
TL;DR
- 我们无法将 CSRF 令牌添加到请求 headers,需要添加到请求 body
- 请求是 multipart/form-data
- Spring 安全 CSRF 过滤器无法配置为从多部分请求中解析 CSRF 令牌
欢迎提供任何帮助——修复 client-side 或 server-side 的建议!
TIA
您应该阅读参考文献中讨论 CSRF and Multipart requests 的部分。您有两个选择:
每个都有参考文献中描述的 pros/cons。
最终,如果您想提供自定义过滤器,您可以使用 XML element 来实现,它仅引用实现过滤器的 Spring Bean。例如:
<http ...>
...
<custom-filter ref="customCsrfFilter" position="CSRF_FILTER"/>
</http>
我已经实施了一个现在似乎有效的解决方案,但其中有一些我真的不喜欢...
- 仍在使用 Spring 安全的
<csrf>
我在 CSRF 过滤器之前添加了一个自定义过滤器:
<security:custom-filter before="CSRF_FILTER" ref="csrfRequestWrapperFilter" />
- 我已经将相同的 RequestMatcher 注入到 CsrfRequestWrapperFilter 中,以仅处理将检查 CSRF 令牌的请求
- 过滤器 wraps/decorates 在
CsrfProtectedHttpServletRequest
中的HttpServletRequest
,并继续链(下一个是 CsrfFilter) CsrfProtectedHttpServletRequest
扩展了HttpServletRequestWrapper
,并覆盖了 getParameter(String name) 方法。类似于:
... 将 Request InputStream 复制到成员 OutputStream ...
if(this.getContentType() != null && requestBody != null) {
if(this.getContentType().contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE) && requestBody.contains(parameterName+"="))
return getFormEncodedParameter(requestBody, parameterName);
else if(this.getContentType().contains(MediaType.MULTIPART_FORM_DATA_VALUE) && requestBody.contains("name="+"\""+ parameterName +"\"")) {
return getMultipartParameter(requestBody, parameterName);
}
}
}
这两种方法都会进行一些字符串解析...这是我非常不喜欢的部分。它与核心业务逻辑完全无关,我对实现没有 100% 的信心,尽管它正在努力从多部分请求中获取 _csrf
"parameter"。