如何配置 Spring 安全性以处理 multipart/form-data (POST) 请求中的 CSRF 令牌?

How can I configure Spring Security to process a CSRF token in a multipart/form-data (POST) request?

场景

问题

文件上传是通过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

欢迎提供任何帮助——修复 client-side 或 server-side 的建议!

TIA

您应该阅读参考文献中讨论 CSRF and Multipart requests 的部分。您有两个选择:

每个都有参考文献中描述的 pros/cons。

最终,如果您想提供自定义过滤器,您可以使用 XML element 来实现,它仅引用实现过滤器的 Spring Bean。例如:

<http ...>
   ...
   <custom-filter ref="customCsrfFilter" position="CSRF_FILTER"/>
</http>

我已经实施了一个现在似乎有效的解决方案,但其中有一些我真的不喜欢...

  1. 仍在使用 Spring 安全的 <csrf>
  2. 我在 CSRF 过滤器之前添加了一个自定义过滤器:

    <security:custom-filter before="CSRF_FILTER" ref="csrfRequestWrapperFilter" />

  3. 我已经将相同的 RequestMatcher 注入到 CsrfRequestWrapperFilter 中,以仅处理将检查 CSRF 令牌的请求
  4. 过滤器 wraps/decorates 在 CsrfProtectedHttpServletRequest 中的 HttpServletRequest,并继续链(下一个是 CsrfFilter)
  5. 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"。