深入分析 web 请求响应中的编码问题(3)解决乱码的关键因素之服务端
- UID
- 1066743
|
深入分析 web 请求响应中的编码问题(3)解决乱码的关键因素之服务端
解决乱码的关键因素之服务端中间件/应用服务器如"浏览器/由地址栏发起的 Get 请求"一节所述,Tomcat 配合浏览器解决请求 URL 乱码问题可以使用 URIEncoding 和 useBodyEncodingForURI 参数。两个参数的具体说明,参见 。
针对 URI 和查询参数使用两种编码的情况(比如表二中 chrome54 在默认情况下 URI 按 UTF-8 编码而查询参数按页面 content type 编码,而页面 content type 不一定为 UTF-8 编码),可以设置 useBodyEncodingForURI=ture,此时中间件会根据 contentType 设置的字符集正确解码查询参数,而对 URI 的解码不产生影响(仍然按 URIEncoding 的设置,即设为 UTF-8 时即可正确解码)。当然推荐的做法还是按 JavaScript/ajaxGet 一节所述,将查询参数也统一为 UTf-8 编码。
应用框架(过滤器)如浏览器/通过表单提交发起的请求一节所述,应用框架配合浏览器解决请求体乱码可以通过添加过滤器解决,比如清单 4 所示的 Spring 提供的编码过滤器
清单 4. Spring 框架中编码过滤器的使用方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| <filter>
/**定义拦截器**/
<filter-name>charsetFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
/**绑定拦截器**/
<filter-name>charsetFilter</filter-name>
<url-pattern>*</url-pattern>
</filter-mapping>
|
下载文件时对 http header 编码的处理众所周知,使用 HTTP Header 的 Content-Disposition: attachment(也可以加上 Content-Type: application/octet-stream)可以实现浏览器弹出下载对话框,这其中会涉及到 Header 的编码问题(文件名是作为 filename 参数放在 Content-Disposition 里面的)。虽然 HTTP Header 中的 Content-Type 可以指定内容的编码,但是 Header 本身的编码却不容易指定。查阅相关 RFC 文档后发现其实应该这样设置 Content-Disposition:
清单 5. 下载文件时如何设置 http 头解决乱码问题1
2
3
4
5
6
7
8
9
| String fileName = "中文文件";
String encodedFileName = java.net.URLEncoder.encode(fileName, "UTF-8");
response.setHeader("Content-disposition", String.format("attachment; filename=\"%s.txt\"; filename*=utf-8''%s.txt", encodedFileName, encodedFileName));//为了兼容 IE6,原始文件名必须包含英文扩展名!
response.setHeader("Content-Type", "application/octet-stream");
response.setContentType("application/txt;charset=utf-8");
String fileContent = "文件内容";
//response.setCharacterEncoding("utf-8");
PrintWriter out = response.getWriter();
|
这么做的根据是什么呢?首先,根据 RFC 2616 Section 4(HTTP 1.1), HTTP 消息格式其实是基于 RFC 822 Section 3,而后者规定消息只能是 ASCII 编码的。RFC 2616 Section 2.2 中还提到 Http 头中的字段值 TEXT 中若要使用非 ASCII 字符,需要先用 RFC 2047 的规定将其编码。
//RFC 2616: TEXT 必须是 ASCII 字符且被直接当做"原文"使用
filename="ASCII TEXT" ;
//RFC 2047,采用 base64 编码
filename*=charset'lang'encoded-text;
然而标准的制定与业界实现是两回事,由于 HTTP 1.1 推出时,Content-Dispostion 这个头(从广泛使用的 RFC 2616 Section 19.5.1 MIME 标准借用而来)还不是正式标准的一部分,所以主流浏览器并未按草案建议使用 RFC2047 进行多语言编码,而是各自推出了自己的方案:
IE 可以自动对按 UTF-8 进行百分号编码的文件名进行解码,不过为了兼容 IE6 其后需要显式跟上后缀名;其他一些浏览器则更诡异——可以解码还原乱码字符串,该乱码字符串是先经 UTF-8 解码 unicode 字符串得到字节数组,然后由使用 ISO-8859-1 编码得到。
清单 6. 下载文件时设置 http 头解决乱码问题的传统方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| String userAgent = request.getHeader("User-Agent");
String fileName = "中文文件";
String encodedFileName = fileName;
//for IE core browser
if (userAgent.contains("MSIE")||userAgent.contains("Trident")) {
encodedFileName = java.net.URLEncoder.encode(fileName, "UTF-8");
} else {
//for non IE core browser
encodedFileName = new String(fileName.getBytes("UTF-8"),"ISO-8859-1");
//模拟浏览器的还原过程
System.out.println(new String(encodedFileName.getBytes("ISO-8859-1"), "UTF-8"));
}
response.setHeader("Content-disposition", String.format("attachment; filename=\"%s.txt\"", encodedFileName));
response.setHeader("Content-Type", "application/octet-stream");
response.setContentType("application/txt;charset=utf-8");
String fileContent = "文件内容";
PrintWriter out = response.getWriter();
out.write(fileContent);
|
由于不同的浏览器处理方式不一样,所以为了跨浏览器使用可以如上先判断浏览器类型然后采用不同的编码方式以此绕过这个问题,不过这终究不是一种优雅的方案。直到 RFC 5987 和 RFC6266 的发布,正式规定了 HTTP Header 中多语言编码的处理方式并把 Content-Disposition 纳入 HTTP 标准。
parameter*=charset'lang'value
其中 charset 和 lang 大小写不敏感;lang 用来标注字段的语言,可取空;value 根据使用百分号编码,并且要求浏览器至少应该支持 ASCII 和 UTF-8。当 parameter 和 parameter 同时出现时,浏览器忽略前者。
这样做可以保证向前兼容:首先 HTTP 头仍然只接受 ASCII 字符,再者遵从 RFC 2616 规范的浏览器会把 parameter 整体当作一个未知的字段忽略,例如:
Content-Disposition: attachment;
filename="EURO rates";
filename*=utf-8''%e2%82%ac%20rates
该例中第一个 filename 是为了保持向前兼容而赋的用 ASCII 表示的同义替代词。更进一步来看,实际应用中残存的旧版浏览器多为 IE,而从前文可知 IE 可以自动对按 UTF-8 进行百分号编码的文件名进行解码,因此我们可以把第一个 filename 的值也改为用 UTF-8 百分号编码的形式:
Content-Disposition: attachment;
filename="%e2%82%ac%20rates.txt";
filename*=utf-8''%e2%82%ac%20rates.txt
这样对于遵从 RFC 5987 和 RFC6266 的新版浏览器,可以通过识别第二个 filename *拿到文件名;而对于旧版的 IE,可以通过识别第一个 filename。拿到文件名。 |
|
|
|
|
|