纸上得来终觉浅,绝知此事要躬行
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/r…
[2]RFC 5987: https://tools.ietf.org/html/r…
[3]RFC 2231: https://tools.ietf.org/html/r…
[4]RFC 3986: https://tools.ietf.org/html/r…
[5]RFC 1866: https://tools.ietf.org/html/r…
[6]RFC 1738: https://tools.ietf.org/html/r…
[7]When to encode space to plus (+) or %20?: https://stackoverflow.com/que…
???? 关注 Java 课代表,获取最新 Java 干货????