前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于Spring6里HTTP multipart/related 文件上传

关于Spring6里HTTP multipart/related 文件上传

原创
作者头像
dhyuan
发布2024-06-16 06:50:30
1270
发布2024-06-16 06:50:30
举报
文章被收录于专栏:响应式编程响应式编程

系统从Spring5升级到Spring6, 除了要进行把javax的api迁移到Jakarta、升级Servlet容器到支持你所选的Jakarta的版本、升级Spring Security对应的API等这些常规操作,还可能遇到一些trick的问题。这里聊一下因为老系统没有限制客户端发送 multipart/related 这种请求而在升级后造成的问题及解决,虽然感觉这种场景 99.999% 的项目都不会遇到。

以流水帐的方式过一下。

升级前Spring5+Jetty9, 升级后Spring6+Jetty11

1)系统上线几天后,有客户说upload csv文件不成功。

2)最后从SumoLogic日志中发现原因是ContentType格式不对,又进一步确认是Spring5的系统支持 multipart/related 而Spring6 不支持造成的。

代码语言:log
复制
Caused by: jakarta.servlet.ServletException: Unsupported Content-Type [Multipart/Related; boundary=AAABBB; type="text/xml"; start="root-part--123"], expected [multipart/form-data]
    at org.eclipse.jetty.server.Request.getParts(Request.java:2324) ~[jetty-server-11.0.19.jar!/:11.0.19]

3)Spring5 是使用Spring自带的MultipartParser,在解析后传给 servlet controller。系统使用Jetty9作为servlet容器。

4)Spring6 之后之前的 CommonsMultipartResolver 被替换为 StandardServletMultipartResolver。而StandardServletMultipartResolver会依赖容器来对Multipart请求做解析。(容器的实现必然有差别)

代码语言:log
复制
Several outdated Servlet-based integrations have been dropped: e.g. Apache Commons FileUpload (org.springframework.web.multipart.commons.CommonsMultipartResolver), and Apache Tiles as well as FreeMarker JSP support in the corresponding org.springframework.web.servlet.view subpackages. We recommend org.springframework.web.multipart.support.StandardServletMultipartResolver 

5)其实不管Jetty 9还是Jetty 11其实都是不支持multipart/related的,之前没有问题是因为CommonsMultipartResolver支持。这样controller直接收到Multipart file这个数据。

6)搭建环境重现、Debug问题。

这里推荐IntelliJ插件jump-to-line

还有个调试技巧是利用IntelliJ的 条件断点及 Evaluate and Log 进行一些变量值的动态修改。

7)修改 org.eclipse.jetty.server.Request,如下

代码语言:java
复制
    public Collection<Part> getParts()

        // if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpField.valueParameters(contentType, null)))
        if (contentType == null)


    private MultiParts newMultiParts(MultipartConfigElement config, int maxParts) throws IOException
    {
        // MultiPartFormDataCompliance compliance = getHttpChannel().getHttpConfiguration().getMultipartFormDataCompliance();
        MultiPartFormDataCompliance compliance = MultiPartFormDataCompliance.LEGACY;

修改 org.eclipse.jetty.server.MultiPartInputStreamParser 中如下

代码语言:java
复制
    protected void parse()
        // if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
        if (_contentType == null)
            return;

绕过条件限制后,发现对普通csv文件通过http Multipart/related上传是可以处理了,controller 可以接收到 MultipartFile 类型的 file 参数了。

其实这个蛮侥幸的,如果Jetty代码压根不支持,估计就得再用其它办法了。

8)后来发现zip格式不支持。Debug后发现是Jetty自己在内部处理时,必须要求临时文件的目录要存在,所以有加了对应逻辑。如下:

代码语言:java
复制
public void write(String fileName) throws IOException
{
    if (_file == null)
    {
        _temporary = false;
        // Make sure the file/directory _tmpDir.getAbsolutePath() + fileName existed.
        touchTmpFileForJetty(_tmpDir.getAbsolutePath(), fileName);

这样修改之后通过了QA的测试。

9)为了测试 multipart/related 请求,也颇费周折。

通过curl命令实现了发送 multipart/related 请求。

代码语言:shell
复制
boundary="upload_boundary"

body=$(cat <<EOF
--$boundary
Content-Disposition: form-data; name="file"; filename="myuploaded.csv"
Content-Type: text/xml; charset=UTF-8
Content-Transfer-Encoding: binary

$(cat /my-path-to-file/my.csv)
--$boundary--
EOF
)

curl -v -X 'POST' \
    -H 'accept: application/json' \
    -H 'Authorization: YOUR-BASE64-USERIDPWD' \
    -H "Content-Type: multipart/related; boundary=$boundary" \
    -d "$body" \
    'https://Your-Server:Port/service-path'

10) 通过这 curl 命令向spring6 发送没问题。但是向 spring5系统发送后却得到500响应。但是通过java程序发送的multipart/releated请求确没问题。???

后台错误日志:

代码语言:log
复制
Caused by: org.apache.commons.fileupload.MultipartStream$MalformedStreamException: Stream ended unexpectedly

11) 为了搞清原因,在本地把 Mitmproxy 跑起来抓包。

从界面上怎么也看不出root cause,直到把请求通过 mitmproxy 导出成curl命令,才发现是换行表示的不同造成的。

通过Java程序发送的能被Spring5处理的请求是\r\n作为换行。

代码语言:log
复制
-d '--upload_boundary\x0d\x0aContent-Disposition: form-data; name="file"; filename="myuploaded.csv"\x0d\x0aContent-Type: text;

而curl发送的就是\n。

代码语言:log
复制
-d '--upload_boundary\x0aContent-Disposition: form-data; name="file"; filename="myuploaded.csv"\x0aContent-Type: text/xml;

12)为了证实确实是换行符造成的, 把/n转为 /r/n后通过curl命令发送后 Spring5也能处理了。

这个规范 rf7230 上也说有的请求接受者做得更“健壮”可以接受LF结尾的请求。

实际上也就是这些“健壮”破坏了规范。(另外,如果从window系统上用curl命令,应该默认就是CRLF的吧?)

代码语言:shell
复制
echo $body > body.txt
cat -e body.txt

unix2dos body.txt
cat -e body.txt

RNBody=$(cat body.txt)
echo $RNBody | cat -e

curl -v -X 'POST' \
    -H 'accept: application/json' \
    -H 'Authorization: YOUR-BASE64-USERIDPWD' \
    -H "Content-Type: multipart/related; boundary=$boundary" \
    -d "$RNBody" \
    'https://Your-Server:Port/service-path'

下面代码演示如何发送zip这样的二进制格式文件。

代码语言:shell
复制
boundary="upload_boundary"

# 这里使用系统默认回撤换行。
{
echo "--$boundary"
echo "Content-Disposition: form-data; name=\"file\"; filename=\"example.zip\""
echo "Content-Type: text; charset=UTF-8"
echo "Content-Transfer-Encoding: binary"
echo ""
cat /your/path/to/zipfile
echo ""
echo "--$boundary--"
} > zip_body.txt


curl -X POST --proxy http://127.0.0.1:7070 -k \
-H "Content-Type: multipart/related; boundary=$boundary" \
-H "Authorization: Basic XXXXXX" \
--data-binary @zip_body.txt \
https://Your-Server:Port/service-path


# 这里明确使用 \r\n
{
echo -ne "--$boundary\r\n"
echo -ne "Content-Disposition: form-data; name=\"file\"; filename=\"example.zip\"\r\n"
echo -ne "Content-Type: text; charset=UTF-8\r\n"
echo -ne "Content-Transfer-Encoding: binary\r\n"
echo -ne "\r\n"
cat /your/path/to/zipfile
echo -ne "\r\n"
echo -ne "--$boundary--\r\n"
} > zip_body_inCRCL.txt

curl -X POST --proxy http://127.0.0.1:7070 -k \
    -H "Content-Type: multipart/related; boundary=$boundary" \
    -H "Authorization: Basic XXXXXX" \
    --data-binary @zip_body_inCRCL.txt \
    https://Your-Server:Port/service-path

13)中间也尝试通过filter在中间使用 Commons FileUpload 2

但是遇到 Stream ended unexpectedly 的问题。另外,在Tomcat做容器的POC中,也是遇到类似的问题。当时debug时发现似乎是跟回车换行有关。当时也都是通过curl命令验证的。但因为自定义Jetty的方案已经可以work,所以就没再继续看。现在回头看很当时遇到的问题很可能跟Spring5遇到的一样。也许发送前对回撤换行处理一下,或许也可以解决。

14)如果通过 Commons FileUpload 2 + Filer 的方式可以解决,那这个方案就是最好的。最不好的方法其实就是这种定制Jetty代码,对以后的升级维护都是潜在的极大风险。

15)想起那句话,重要的是系统要限制能做什么。 为了这个patch前后花费的人天挺多的。。。新版本还delay了好久。


References:

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档