下载的附件名总乱码?你该去读一下 RFC 文档了!
纸上得来终觉浅,绝知此事要躬行
Web 开发过程中,相信大家都遇到过附件下载的场景,其中,各浏览器下载后的文件名中文乱码问题或许一度让你苦恼不已。
网上搜索一下,大部分都是通过Request Headers
中的UserAgent
字段来判断浏览器类型,根据不同的浏览器做不同的处理,类似下面的代码:
// MicroSoft Browser
if (agent.contains("msie") || agent.contains("trident") || agent.contains("edge")) {
// filename 特殊处理
}
// firefox
else if (agent.contains("firefox")) {
// filename 特殊处理
}
// safari
else if (agent.contains("safari")) {
// filename 特殊处理
}
// Chrome
else if (agent.contains("chrome")) {
// filename 特殊处理
}
// 其他
else{
// filename 特殊处理
}
//最后把特殊处理后的文件名放到head里
response.setHeader("Content-Disposition",
"attachment;fileName=" + filename);
不过,这样的代码看起来很魔幻,为什么每个浏览器的处理方式都不一样?难道每次新出一个浏览器都要做兼容吗?就没有一个统一标准来约束一下这帮浏览器吗?
带着这个疑惑,我翻阅了 RFC 文档,最终得出了一个优雅的解决方案:
// percentEncodedFileName 为百分号编码后的文件名
response.setHeader("Content-disposition",
"attachment;filename=" + percentEncodedFileName +
";filename*=utf-8''" + percentEncodedFileName);
经过测试,这段响应头可以兼容市面上所有主流浏览器,由于是 HTTP 协议范畴,所以语言无关。只要按这个规则设置响应头,就能一劳永逸地解决恼人的附件名中文乱码问题。
接下来课代表带大家抽丝剥茧,通过阅读 RFC 文档,还原一下这个响应头的产出过程。
1. Content-Disposition
一切要从 RFC 6266 开始,在这份文档中,介绍了Content-Disposition
响应头,其实它并不属于HTTP
标准,但是因为使用广泛,所以在该文档中进行了约束。它的语法格式如下:
content-disposition = "Content-Disposition" ":"
disposition-type *( ";" disposition-parm )
disposition-type = "inline" | "attachment" | disp-ext-type
; case-insensitive
disp-ext-type = token
disposition-parm = filename-parm | disp-ext-parm
filename-parm = "filename" "=" value
| "filename*" "=" ext-value
其中的disposition-type
有两种:
- inline 代表默认处理,一般会在页面展示
- attachment 代表应该被保存到本地,需要配合设置
filename
或filename*
注意到disposition-parm
中的filename
和filename*
,文档规定:这里的信息可以用于保存的文件名。
它俩的区别在于,filename 的 value 不进行编码,而filename*
遵从 RFC 5987中定义的编码规则:
Producers MUST use either the "UTF-8" ([RFC3629]) or the "ISO-8859-1" ([ISO-8859-1]) character set.
由于filename*
是后来才定义的,许多老的浏览器并不支持,所以文档规定,当二者同时出现在头字段中时,需要采用filename*
,忽略filename
。
至此,响应头的骨架已经呼之欲出了,摘录 [RFC 6266] 中的示例如下:
Content-Disposition: attachment;
filename="EURO rates";
filename*=utf-8''%e2%82%ac%20rates
这里对filename*=utf-8''%e2%82%ac%20rates
做一下说明,这个写法乍一看可能会觉得很奇怪,它其实是用单引号作为分隔符,将等号右边分成了三部分:第一部分是字符集(utf-8
),中间部分是语言(未填写),最后的%e2%82%ac%20rates
代表了实际值。对于这部分的组成,在RFC 2231.section 4 中有详细说明:
A single quote is used to
separate the character set, language, and actual value information in
the parameter value string, and an percent sign is used to flag
octets encoded in hexadecimal.
2.PercentEncode
PercentEncode 又叫 Percent-encoding 或 URL encoding.
正如前文所述,filename*
遵守的是[RFC 5987] 中定义的编码规则,在[RFC 5987] 3.2中定义了必须支持的字符集:
recipients implementing this specification
MUST support the character sets "ISO-8859-1" and "UTF-8".
并且在[RFC 5987] 3.2.1规定,百分号编码遵从 RFC 3986.section 2.1中的定义,摘录如下:
A percent-encoding mechanism is used to represent a data octet in a
component when that octet's corresponding character is outside the
allowed set or is being used as a delimiter of, or within, the
component. A percent-encoded octet is encoded as a character
triplet, consisting of the percent character "%" followed by the two
hexadecimal digits representing that octet's numeric value. For
example, "%20" is the percent-encoding for the binary octet
"00100000" (ABNF: %x20), which in US-ASCII corresponds to the space
character (SP). Section 2.4 describes when percent-encoding and
decoding is applied.
注意了,[RFC 3986] 明确规定了空格 会被百分号编码为%20
而在另一份文档 RFC 1866.Section 8.2.1 The form-urlencoded Media Type 中却规定:
The default encoding for all forms is `application/x-www-form-
urlencoded'. A form data set is represented in this media type as
follows:
1. The form field names and values are escaped: space
characters are replaced by `+', and then reserved characters
are escaped as per [URL]
这里要求application/x-www-form-urlencoded
类型的消息中,空格要被替换为+
,其他字符按照[URL]中的定义来转义,其中的[URL]指向的是RFC 1738 而它的修订版中和 URL 有关的最新文档恰恰就是 [RFC 3986]
这也就是为什么很多文档中描述空格(white space)的百分号编码结果都是 +
或%20
,如:
w3schools:URL encoding normally replaces a space with a plus (+) sign or with %20.
MDN:Depending on the context, the character ' ' is translated to a '+' (like in the percent-encoding version used in an application/x-www-form-urlencoded message), or in '%20' like on URLs.
那么问题来了,开发过程中,对于空格符的百分号编码我们应该怎么处理?
课代表建议大家遵循最新文档,因为 [RFC 1866] 中定义的情况仅适用于application/x-www-form-urlencoded
类型, 就百分号编码的定义来说,我们应该以 [RFC 3986] 为准,所以,任何需要百分号编码的地方,都应该将空格符 百分号编码为%20
,stackoverflow 上也有支持此观点的答案:When to encode space to plus (+) or %20?
3. 代码实践
有了理论基础,代码写起来就水到渠成了,直接上代码:
@GetMapping("/downloadFile")
public String download(String serverFileName, HttpServletRequest request, HttpServletResponse response) throws IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("application/octet-stream");
String clientFileName = fileService.getClientFileName(serverFileName);
// 对真实文件名进行百分号编码
String percentEncodedFileName = URLEncoder.encode(clientFileName, "utf-8")
.replaceAll("\\+", "%20");
// 组装contentDisposition的值
StringBuilder contentDispositionValue = new StringBuilder();
contentDispositionValue.append("attachment; filename=")
.append(percentEncodedFileName)
.append(";")
.append("filename*=")
.append("utf-8''")
.append(percentEncodedFileName);
response.setHeader("Content-disposition",
contentDispositionValue.toString());
// 将文件流写到response中
try (InputStream inputStream = fileService.getInputStream(serverFileName);
OutputStream outputStream = response.getOutputStream()
) {
IOUtils.copy(inputStream, outputStream);
}
return "OK!";
}
代码很简单,其中有两点需要说明一下:
URLEncoder.encode(clientFileName, "utf-8")
方法之后,为什么还要.replaceAll("\\+", "%20")
。正如前文所述,我们已经明确,任何需要百分号编码的地方,都应该把 空格符编码为
%20
,而URLEncoder
这个类的说明上明确标注其会将空格符转换为+
:The space character " " is converted into a plus sign "{@code +}".
其实这并不怪 JDK,因为它的备注里说明了其遵循的是
application/x-www-form-urlencoded
( PHP 中也有这么一个函数,也是这么个套路)Translates a string into {@code application/x-www-form-urlencoded} format using a specific encoding scheme. This method uses the
所以这里我们用
.replaceAll("\\+", "%20")
把+
号处理一下,使其完全符合 [RFC 3986] 的百分号编码规范。这里为了方便说明问题,把所有操作都展现出来了。当然,你完全可以自己实现一个PercentEncoder
类,丰俭由人。[RFC 6266] 标准中
filename=
的value
是不需要编码的,这里的filename=
后面的 value 为什么要百分号编码?回顾 [RFC 6266] 文档,
filename
和filename*
同时出现时取后者,浏览器太老不支持新标准时取前者。目前主流的浏览器都采用自升级策略,所以大部分都支持新标准------除了老版本IE。老版本的IE对 value 的处理策略是 进行百分号解码 并使用。所以这里专门把
filename=
的value
进行百分号编码,用来兼容老版本 IE。PS:课代表实测 IE11 及 Edge 已经支持新标准了。
4. 浏览器测试
根据下图 statcounter 统计的 2019 年中国市场浏览器占有率,课代表设计了一个包含中文,英文,空格的文件名 下载-down test .txt
用来测试
测试结果:
Browser | Version | pass |
---|---|---|
Chrome | 84.0.4147.125 | true |
UC | V6.2.4098.3 | true |
Safari | 13.1.2 | true |
QQ Browser | 10.6.1(4208) | true |
IE | 7-11 | true |
Firefox | 79.0 | true |
Edge | 44.18362.449.0 | true |
360安全浏览器12 | 12.2.1.362.0 | true |
Edge(chromium) | 84.0.522.59 | true |
根据测试结果可知:基本已经能够兼容市面上所有主流浏览器了。
5.总结
回顾本文内容,其实就是浏览器兼容性问题引发的附件名乱码,为了解决这个问题,查阅了两类标准文档:
HTTP 响应头相关标准
[RFC 6266]、[RFC 1866]
编码标准
[RFC 5987]、[RFC 2231]、[3986]、[1738]
我们以 [RFC 6266] 为切入点,全文总共引用了 6 个 [RFC] 相关文档,引用都标明了出处,感兴趣的同学可以跟着文章思路阅读一下原文档,相信你会对这个问题有更深入的理解。文中代码已上传 github
最后不禁要感叹一下:规范真是个好东西,它就像 Java 语言中的 interface
,只制定标准,具体实现留给大家各自发挥。
如果觉得本文对你有帮助,欢迎收藏、分享、在看三连
6.参考资料
[1]RFC 6266: https://tools.ietf.org/html/rfc6266
[2]RFC 5987: https://tools.ietf.org/html/rfc5987
[3]RFC 2231: https://tools.ietf.org/html/rfc2231
[4]RFC 3986: https://tools.ietf.org/html/rfc3986
[5]RFC 1866: https://tools.ietf.org/html/rfc1866
[6]RFC 1738: https://tools.ietf.org/html/rfc1738
[7]When to encode space to plus (+) or %20?: https://stackoverflow.com/questions/2678551/when-to-encode-space-to-plus-or-20
关注 Java课代表,获取最新 Java 干货
下载的附件名总乱码?你该去读一下 RFC 文档了!的更多相关文章
- PHPMailer发送邮件中文附件名是乱码
可能使用了PHPMailer发送邮件的朋友带中文附件名时会出现乱码,下面我来介绍一个解决办法. 比如我们要发送的附件是"测试.txt",如果在添加附件的时候强制使用指定文件名的方式 ...
- Java邮件开发(三):解决附件名为乱码及显示友好名称
1.附件的名称只能为英文,中文乱码 2.友好名称的显示. 我们使用163等邮箱发送邮件时,我们经常可以看到收件人一栏中会是:张益达 <zyh5540@163.com>这种方式.在上一版本的 ...
- java帮助文档下载
JAVA帮助文档全系列 JDK1.5 JDK1.6 JDK1.7 官方中英完整版下载JDK(Java Development Kit,Java开发包,Java开发工具)是一个写Java的applet和 ...
- JAVA帮助文档全系列 JDK1.5 JDK1.6 JDK1.7 官方中英完整版下载
JAVA帮助文档全系列 JDK1.5 JDK1.6 JDK1.7 官方中英完整版下载JDK(Java Development Kit,Java开发包,Java开发工具)是一个写Java的applet和 ...
- 如何教你在NIPS会议上批量下载历年的pdf文档(另附04~14年NIPS论文下载链接)
如何获得NIPS会议上批量下载的链接? NIPS会议下载网址:http://papers.nips.cc/ a.点击打开上述网站,进入某一年的所有会议,例如2014年,如下图 b.然后对着当前网页点击 ...
- Android Studio API 文档_下载与使用
如何下载API 说明: 时间: 2016/7/9 根据百度经验步骤改编(百度经验), 但是比它更好, 亲测可用 1.1 下载API文档: 1.1.1 SDK Manager 1.1.2 1.1.3 ( ...
- 解决Linux文档显示中文乱码问题以及编码转换
解决Linux文档显示中文乱码问题以及编码转换 解决Linux文档显示中文乱码问题以及编码转换 使vi支持GBK编码 由于Windows下默认编码是GBK,而linux下的默认编码是UTF-8,所以打 ...
- JDK下载API文档
JDK官方下载 JDK1.5 : http://cds.sun.com/is-bin/INTERSHOP.enfinity/WFS/CDS-CDS_Developer-Site/en_US/-/USD ...
- Java全系列帮助文档下载
JDK(Java Development Kit,Java开发包,Java开发工具)是一个写Java的applet和应用程序的程序开发环境.它由一个处于操作系统层之上的运行环境还有开发者编译,调试和运 ...
随机推荐
- 数据聚合与分组操作知识图谱-《利用Python进行数据分析》
所有内容整理自<利用Python进行数据分析>,使用MindMaster Pro 7.3制作,emmx格式,源文件已经上传Github,需要的同学转左上角自行下载或者右击保存图片. 其他章 ...
- 题解 CF 1372 B
题目 传送门 题意 给出 \(n\),输出 \(a\) ,\(b\) (\(0 < a \leq b < n\)),使\(a+b=n\)且 \(\operatorname{lcm}(a,b ...
- 浅谈Python中的深浅拷贝的区别
深.浅拷贝总结 深拷贝 拷贝可变数据类型,如列表容器: a = [1, 2, [3, 4]] b = copy.deepcopy(a) a 与 b 所指的列表容器的空间地址不一致,即 id(a) != ...
- SQL查询基本用法
-- 单列查询 select 编号 from employees -- 多列查询 select 编号,姓名 from employees -- 查询所有列 select * from employee ...
- 前端学习(十三):CSS盒子模型
进击のpython ***** 前端学习--CSS盒子模型 在前面的时候也说过,包括分析网页结构的时候,提到了,网页就其实就是一个一个盒子叠起来的 那现在就是有装饰的盒子,难度就变得深刻 所以说为了能 ...
- Fortify Audit Workbench 笔记 Path Manipulation
Path Manipulation Abstract 通过用户输入控制 file system 操作所用的路径,借此攻击者可以访问或修改其他受保护的系统资源. Explanation 当满足以下两个条 ...
- Nodejs同步和异步编程
同步API:只有当前API执行完成后,才能继续执行下一个API:异步API:当前API的执行不会阻塞后续代码的执行. 同步异步代码执行顺序 同步:从上到下依次执行,前面代码会阻塞后面代码的执行.异步: ...
- Python定义一个函数
Python函数:实现某种功能的代码段 定义一个函数需要遵循的规则: 1.使用 def 关键字 函数名和( ),括号内可以有形参 匿名函数使用 lambda 关键字定义 2.任何传入参数和自变量必须放 ...
- hashlib加密算法
# import hashlib # mima = hashlib.md5()#创建hash对象,md5是信息摘要算法,生成128位密文 # print(mima) # # mima.update(' ...
- luogu P6087 [JSOI2015]送礼物 二分 单调队列 决策单调性
LINK:送礼物 原本想了一个 \(nlog^2\)的做法 然后由于线段树常数过大 T到30. 以为这道题卡\(log^2\)没想到真的有神仙写\(log^2\)的过了 是我常数大了 抱歉. 能过的\ ...