表单(form) post 方式提交时的字符集编码与乱码(上)--application/x-www-form-urlencoded 方式

探讨了表单以 post 方式, enctype 为 application/x-www-form-urlencoded 提交时数据所使用的字符集编码, 具体介绍了缺省情况以及设置了 accept-charset 属性时的情况, 同时介绍了后台在取出表单数据前如何使用 setCharacterEncoding 来设置正确的解码.

目录

在上一篇章中谈论了表单以 get 提交时的编码与乱码问题, 这一章中将讨论以 post 方式提交时的编码与乱码问题.

在前面也同时提到, 表单有一个叫 enctype 的属性, 它有两个值, application/x-www-form-urlencodedmultipart/form-data.

这一属性实际只对 post 方式起作用, 因为 get 方式实际只支持前一种类型, 也就是 application/x-www-form-urlencoded, 这是缺省的类型.

在使用 post 方式提交时, 缺省的编码类型也依然是这个 application/x-www-form-urlencoded. 在这一篇章中, 先讨论这一类型;在下篇中, 再讨论 multipart/form-data 类型.

post 方式, application/x-www-form-urlencoded 类型的编码

对于 application/x-www-form-urlencoded 类型, 前面讨论 get 的方式已经详细讨论过了, post 方式用它, 底层原理依旧是相同的, 只不过它的数据此时不是放在 uri 中, 而是在 消息体 中发送过去.

简单地讲, 就是地址栏中是看不出来的.

发送形式有差别, 但在这里关心的编码方式上, 却与 get 是一样的. 比如, 非字母数字的字符要用转义表示, 键值对用等号隔开, 多个键值对用 "&" 符号拼接.

对于要转义的字符, 在编码的使用上, 也是一样的:

  1. 没有用 accept-charset 指定, 就用文档本身的编码;
  2. 设置了 accept-charset 的值, 就用它设置的值.

稍微要注意的反而是后台的接收方面.

一个具体的例子

依旧是构建一个简单的 post 方式提交的表单:

<!DOCTYPE html><html><head>
<meta charset="UTF-8">
<title>表单 post 提交页(页面编码 utf8)</title>
</head><body>
<h2>post 提交, 页面编码: UTF-8</h2><hr>
<form action="form_post_target_default.jsp" method="post">
	<input name="english" value="hi">
	<input name="chinese" value="你好">
	<input type="submit" value="submit"></form>
</body></html>

然后提交到一个后台的 jsp 上, 依旧是简单地取出显示:

<%@ page pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>表单 post 接受页(缺省)</title>
</head>
<body>
<h3>request keep default</h3>
${param.english} ${param.chinese}
</body>
</html>

提交后会发现这样的方式地址栏将不会再有 "查询字符串(query component)" 了:

表单 post 地址栏 乱码

但能注意到接收页面的显示是乱码的. 此时查看请求头的信息:

表单 post 请求头 Form data

表单数据是正常的, 点击 view source, 发现确实是 utf-8 的转义编码:

表单 post 请求头 Form data view source

整体上也是 urlencoded 的方式, 跟上篇说到的 get 一样, 因为还是缺省的 application/x-www-form-urlencoded 方式的编码.

后台用 setCharacterEncoding 设置解码字符集

所以, 整个发送环节是没有问题的, 问题出在后台接收处理上. 因为数据不是在 uri 中, 自然之前的所谓设置 URIEncoding 是没有用了, 这时需要不同的策略.

post 情况下, 在取出数据前, 就 java servlet 平台而言, 需要用 request.setCharacterEncoding("utf-8") 先设置一下, 编码的具体值与浏览器所用的一致即可, 因为表单用的是 utf-8, 这里也就设置为 utf-8, 像这样:

<%@ page pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>表单 post 接受页(utf-8)</title>
</head>
<body>
<h3>request.setCharacterEncoding("utf-8")</h3>
<%request.setCharacterEncoding("utf-8"); %>
${param.english} ${param.chinese}
</body>
</html>

调整之后, 再测试就正常了:

表单 post 浏览器 测试 正常

假如没有作这一步骤的设置, 那么 servlet 中将使用缺省的 iso-8859-1 来解码.

在某些情况下, 如果你无法在这一层面进行调整, 也可以保持按 iso-8859-1 的方式来接收, 然后使用一种 hack 的方式来倒腾出正确的 string 的值, 具体见 文本在内存中的编码(3) 中的介绍, 这里不再详述.

一般来说, 除非不得已, 不太建议使用 hack 的方式.

使用 accept-charset 属性指定 post 时的编码

假如表单页面增加 accept-charset 字段, 值设为与文档本身编码 utf-8 不一样的 GBK:

<!DOCTYPE html><html><head>
<meta charset="UTF-8">
<title>表单 post 提交页(页面编码 utf8)</title>
</head><body>
<h2>post 提交, 页面编码: UTF-8</h2><hr>
<form action="form_post_target_utf8.jsp" method="post" accept-charset="gbk">
	<input name="english" value="hi">
	<input name="chinese" value="你好">
	<input type="submit" value="submit"></form>
</body></html>

那么这样之后, 提交的数据就是 GBK 编码了, 不是 utf-8 编码的情况下调试工具中无法正常为字符:

表单 post accept-charset gbk 请求头

不过点击 view source, 还是可以看到原始的值:

表单 post accept-charset gbk 请求头 view source

四个字节确实为 gbk 编码.

而后台的解码此时也会再一次出现问题:

表单 post accept-charset 与 setCharacterEncoding 不一致 乱码

因为提交数据的实际编码是 gbk, 而后台代码却还是按 utf-8 来解码, 所以就出错了. 而调整方式也很简单, 就是把 request.setCharacterEncoding("utf-8") 改为 request.setCharacterEncoding("gbk") 即可, 改完后的代码及具体测试这里就不再列出了, 读者可以自行实验.

总之, 一句话, 编码与解码两端要在使用的字符集编码上保持一致即可.

配置 filter 统一解码时所用的字符集

如果不想在每次接收前都来调用 request.setCharacterEncoding 这么设置一下, 可以统一配置一个过滤器, 以 tomcat8 为例, 可以这样在 web.xml 中去增加一个 filter, 并在初始化参数 init-param 下设置参数名 param-name 为 encoding, 设置参数值 param-value 为 UTF-8:

<filter>  
  <filter-name>Character Encoding Filter</filter-name>  
  <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class>  
  <init-param>  
    <param-name>encoding</param-name>  
    <param-value>UTF-8</param-value>  
  </init-param>  
</filter>  
<filter-mapping>  
  <filter-name>Character Encoding Filter</filter-name>  
  <url-pattern>/*</url-pattern>  
</filter-mapping>

这样就统一设置为使用 utf-8 来解码了.

如果你不是使用 tomcat, 或者 tomcat 的版本太旧不包含上述 filter, 那么也可以自行定义一个类似的 filter, 原理上简单讲就是在 doFilter 方法里设置一下编码:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    request.setCharacterEncoding("utf-8");
    chain.doFilter(request, response);
}

更复杂完善一点的可以先看看 request 中是否已经设置过了 charset.

许多的 MVC 框架中关于编码的设置其实就是着力在这里.

关于 Filter 方面不了解的读者可以自行阅读 servlet 相关规范以了解.

当然了, 这样之后, 表单就不能用 gbk 编码方式来提交了. 如果你有一些遗留的 gbk 页面, 那么就要在其表单上设置 accept-charset="utf-8", 指示它用 utf-8 来编码提交的数据.

注意: 如果你不太确定这种统一的调整会造成哪些冲击, 那么就要慎重地去处理, 特别是在运行已久的遗留系统上, 可能还存在前面提到的各种混乱的 hack 的处理方式, 统一的调整可能会使得这样的代码失效.

这也是不提倡使用 hack 的原因, 它代表了一种不太正确的处理方式, 并给后续的调整带来了挑战, 后来者往往只能"将错就错", 继续地 hack 下去.

那么, 关于 post 方式以 application/x-www-form-urlencoded 的类型来提交时的编码与乱码问题, 因为与之前的 get 方式提交还是很相似的, 这里就简单介绍到这里.