Content-Disposition头部有两个用途:第一是在响应的Header中,用于表示响应的内容是如何展示的,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。第二个用途是在multipart/form-data类型的请求报文中,用于表示每个字段的的名称等信息。

本文主要讨论第一种使用场景,在强制浏览器将文件作为附件下载时,我们会用到Content-Disposition头部,并制定保存后文件的文件名。使用方式为:

1
2
Content-Disposition: attachment; filename="filename.jpg"
Content-Disposition: attachment; filename*=UTF-8''filename.jpg

但是在实际使用中,对于非ASCII字符,不同浏览器中存在编码问题。需要测试一下什么方式兼容性是最高的。

说明:以下测试使用的文件名为"abc +&?/中文.txt",但是其实在windows等操作系统中?/是不能作为文件名的。从下面的测试来看,在遇到这种情况时,所有的浏览器都是会使用_进行替换后保存。

Test1

直接把中文设置到Header中:

1
2
String name = "abc +&?/中文.txt";
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + name + "\"");

最终输出的Header为:

1
Content-Disposition: attachment; filename="abc +&?/??.txt"

说明无法在Header中直接设置UTF-8字符串,如果设置,会被转义为问号,导致信息丢失。

Test2

把中文按照ISO8859-1编码的方式输出到Header中,这是我在项目代码中看到的一种挺诡异的做法:

1
2
3
String name = "abc +&?/中文.txt";
name = new String(name.getBytes(StandardCharsets.UTF_8), "ISO8859-1");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + name + "\"");

最终输出的Header为:

1
Content-Disposition: attachment; filename="abc +&?/中文.txt"

在不同浏览器中的表现:

  • Chrome73:abc +&__中文.txt(正确)
  • Firefox63:abc +&__中文.txt(正确)
  • IE11:abc +&__涓枃.txt(乱码)

可以看到在IE中出现了乱码,所以该方法不采用。

Test3

将文件名进行URL编码后放入Header:

1
2
3
String name = "abc +&?/中文.txt";
name = URLEncoder.encode(name, "utf-8");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + name + "\"");

最终输出的Header为:

1
Content-Disposition: attachment; filename="abc+%2B%26%3F%2F%E4%B8%AD%E6%96%87.txt"

在不同浏览器中的表现:

  • Chrome73:abc++&__中文.txt(空格变成了+号)
  • Firefox63:abc+%2B%26%3F%2F%E4%B8%AD%E6%96%87.txt(乱码)
  • IE11:abc++&__中文.txt(空格变成了+号)

这种情况所有浏览器都异常。

Test4

将文件名进行URL编码后放入Header,并且使用%20来编码空格,而不是使用+

1
2
3
String name = "abc +&?/中文.txt";
name = URLEncoder.encode(name, "utf-8").replaceAll("\\+", "%20");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + name + "\"");

最终输出的Header为:

1
Content-Disposition: attachment; filename="abc%20%2B%26%3F%2F%E4%B8%AD%E6%96%87.txt"

在不同浏览器中的表现:

  • Chrome73:abc +&__中文.txt(正确)
  • Firefox63:abc%20%2B%26%3F%2F%E4%B8%AD%E6%96%87.txt(乱码)
  • IE11:abc +&__中文.txt(正确)

可以看到,使用%20来编码空格可以在一定程度解决空格变成加号的问题。

Test5

根据规范,filename*才能支持非ASCII字符,因为它可以指定编码,用这个字段,结合URL编码名称试试:

1
2
3
String name = "abc +&?/中文.txt";
name = URLEncoder.encode(name, "utf-8");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + name);

最终输出的Header为:

1
Content-Disposition: attachment; filename*=UTF-8''abc+%2B%26%3F%2F%E4%B8%AD%E6%96%87.txt

在不同浏览器中的表现:

  • Chrome73:abc++&__中文.txt(空格变成了+号)
  • Firefox63:abc++&__中文.txt(空格变成了+号)
  • IE11:abc++&__中文.txt(空格变成了+号)

可以看到,使用filename*后,Firefox变成比较正常了,他会正常的解析其中的编码。

Test6

上面的测试中,filename*可以解决编码问题,剩下的空格问题可以用%20试试:

1
2
3
String name = "abc +&?/中文.txt";
name = URLEncoder.encode(name, "utf-8").replaceAll("\\+", "%20");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + name);

最终输出的Header为:

1
Content-Disposition: attachment; filename*=UTF-8''abc%20%2B%26%3F%2F%E4%B8%AD%E6%96%87.txt

在不同浏览器中的表现:

  • Chrome73:abc +&__中文.txt(正确)
  • Firefox63:abc +&__中文.txt(正确)
  • IE11:abc +&__中文.txt(正确)

可以看到所有浏览器都正常了。

Test7

因为filename*可能不是所有浏览器都支持,所以网上还提到了同时提供filenamefilename*的方案,新浏览器使用后者,旧浏览器使用前者。我手上没有旧浏览器所以就看看新的是什么表现吧:

1
2
3
String name = "abc +&?/中文.txt";
name = URLEncoder.encode(name, "utf-8");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"test\"; filename*=UTF-8''" + name);

最终输出的Header为:

1
Content-Disposition: attachment; filename="test"; filename*=UTF-8''abc+%2B%26%3F%2F%E4%B8%AD%E6%96%87.txt
  • Chrome73:abc++&__中文.txt(空格变成了+号)
  • Firefox63:abc++&__中文.txt(空格变成了+号)
  • IE11:abc++&__中文.txt(空格变成了+号)

说明了这三个浏览器都是优先使用filename*。我们可以放心的同时提供filenamefilename*

结论

综上最好的Content-Disposition编码方式为:

1
attachment; filename="{URL编码的文件名,+号使用%20}"; filename*=UTF-8''{URL编码的文件名,+号使用%20}

参考资料