web请求编码的问题

 一 请求处理的基本过程
  http请求的处理过程

 浏览器或http客户端把 URL(包括post/get提交的内容)经过编码发送给web容器 
 web容器的connector解码URL和其中包含的post/get提交的内容(参数),匹配相应的JSP或Servlet来处理 
 jsp或Servlet处理完毕后,web容器将内容按某种字符集编码返回给浏览器或http客户端 
浏览器或http客户端根据响应头ContentType设置的编码来显示响应

 

一个典型的 URL构成是这样的:

域名:端口/contextPath/servletPath/pathInfo?queryString

说明:

contextPath --web应用的上下文根,也有叫web前缀的, 
servletPath--指在web应用部署描述符web.xml中标签<servlet-mapping>配置的Servlet映射路径

pathInfo--同servletPath一起构成查找jsp或serlvet的路径

queryString--包含请求参数的字符串,以&key=value形式表示参数名(key)及其值(value)

下面分析具体各个部分是怎么处理的:

首先说明几个概念:
编码--将字节转换成字符的过程
解码--将字符转换成字节的过程
URL编码--将URL按照一定的规则转换和编码

URL编码规则是:

字母数字字符 "a" 到 "z"、"A" 到 "Z" 和 "0" 到 "9" 保持不变。 
特殊字符 "."、"-"、"*" 和 "_" 保持不变。 
空格字符 " " 转换为一个加号 "+"。 
所有其他字符(包括中文)都是不安全的,因此首先使用一些编码机制(字符集)将它们转换为一个或多个字节。然后每个字节用一个包含 3 个字符的字符串 "%xy" 表示,其中 xy 为该字节的两位十六进制表示形式。 
二 浏览器(http客户端)的处理
下面以比较主流的浏览器IE和FireFox为例来说明,因为本文讨论中文字符问题,均以中文浏览器为例:

1、GET方式提交,浏览器会对URL进行URL encode,然后发送给服务器。
(1) 对于中文IE,如果在高级选项中选中总以UTF-8发送(默认方式),则PathInfo是URL Encode是按照UTF-8编码,QueryString是按照当前浏览器编码(最开始一般为GB2312(IE)或GBK(FireFox))编码。
比如:http://localhost:8080/example/中国?name=中国
实际上提交是:
GET /example/%E4%B8%AD%E5%9B%BD?name=%D6%D0%B9%FA

(2) 对于中文IE,如果在高级选项中取消总以UTF-8发送,则PathInfo和QueryString是URL encode按照GB2312编码。
实际上提交是:
GET /example/%D6%D0%B9%FA?name=%D6%D0%B9%FA

(3) 对于中文firefox,早期版本的FireFox,pathInfo和queryString都是URL encode按照GBK编码。
实际上提交是:
GET /example/%D6%D0%B9%FA?name=%D6%D0%B9%FA

现在版本(3.0以上)pathInfo也是缺省也按UTF-8发送,QueryString按GBK编码,这个URL编码设置可通过在浏览器地址栏输入about:config,搜索network.standard-url.encode-utf8来设置

很显然,不同的浏览器以及同一浏览器的不同设置,会影响最终URL中PathInfo的编码。对于中文的IE和FIREFOX都是采用GBK编码QueryString。 如果用iso-8859-1来编码发送,无论是PathInfo还是QueryString,如果其中含有中文,毫无疑问,中文字符损失了.损失的意思就是无论再怎么转码和编解码,中文字符再也恢复不过来了。因为中文是按两个(GBK)或多个字节(UTF-8)编码,iso-8859-1是单字节字符集,并不包括中文字符,对于多字节中文 ,iso-8859-1 编码是按单字节编码,这样多字节中文被拆分成单字节进行编码,当遇到iso-8859-1字符集中没有包括的,只能以3f(对应字符?号)代替,因而中文字符最终显示是几个?号,造成乱码.同理,当使用iso-8859-1解码用多字节字符集编码过的含中文字符的字节序列时,也是按单个字节解码,遇到字节(或者说字节对应的编号)在iso-8859-1字符集没有包括的,只能用?号代替

 

[c-sharp] view plaincopy
  1. String str="中文";  
  2.   
  3. 用str.getBytes("iso-8859-1");--中文字符损失了  
  4.   
  5. 用String newStr=new String(str.getBytes("GBK"),"iso-8859-1");  
  6.   
  7. 中文字符还有救,通过new String(newStr.getBytes("iso-8859-1"),"GBK");中文字符转回来了  

 

 

不同http客户端有不同的实现和配置,具体情况具体对待,这里不做分析.

这里还要提到一点:对于 html中超级链接<a href="url" >形式的请求,浏览器也是按GET方式提交的,跟表单(Form)方式的GET请求还是有点细微差别,中文IE并不对QueryString进行URL encode,而是直接进行encode,中文FireFox则是进行URL encode

2.POST方式提交

 PathInfo部分编码参照GET方式,这里没有QueryString了,如果有按GET方式处理,参数部分是在请求体(request body)中按当前浏览器编码传送给web容器的,中文IE和FireFox都是如此

3.浏览器对web容器响应的处理

   浏览器根据web容器响应头Content-Type中charset设置的编码来显示响应内容,并设置为当前浏览器的编码,注意这也是下次请求时的编码,可通过浏览器菜单栏-查看-编码来查看当前浏览器设置的编码

  综合浏览器对请求和响应的处理,可看出Web应用和Web容器在处理编码时,请求和响应的编码设置为一致的是最为科学的.

 

 

三 web容器的处理
 http请求的处理:

1.PathInfo的处理

   无论是get方式还是post方式的请求,web容器在对PathInfo进行url 解码的方式是一致的,web容器提供参数URIEncoding来解码PathInfo,这个参数的缺省值就是UTF-8,这也是跟主流浏览器缺省的URL发送编码为UTF-8是吻合的.

 在http connector接收到浏览器或http客户端发来的请求后,需将请求进行URL解码来交给web容器匹配相应的jsp或servlet来处理,具体解码的过程是在org.apache.coyote.tomcat5.CoyoteAdapter类的postParseRequest()方法中,先进行URL解码,去除URL中的"%","/"以及"+"字符,取出URL 中的有效字符,再根据URIEncoding参数设置的字符集来将URL中的字符进行解码, 在web容器中的每个请求对象中org.apache.coyote.tomcat5.CoyoteRequest对象中均持有一个org.apache.tomcat.util.buf.UDecoder对象的引用,调用其convert()方法用于URL初步解码,调用CoyoteRequest 的convertURI()方法进行有效字符的解码,从而完成整个URL解码的过程

2.请求参数的处理

  请求参数包括get方式提交的QueryString 和post方式提交的在请求体中发送的参数

  请求参数的解析并解码并不发生在请求被http connector接受和处理后,而发生在某次请求,web应用在jsp或Servlet程序中调用了HttpServletRequest对象(web容器含有具体的请求对象实现)的getParameter(),getParameterNames()和getParameterValues()方法中的任意一个,在此之前,请求参数只是作为未解码的字节数组而存在.对于该次请求来说,请求参数的解析并解码仅发生一次.

  解析并解码具体过程:

 这个过程发生在org.apache.coyote.tomcat5.CoyoteRequest的parseRequestParameters()方法中

 (1)通过调用getCharacterEncoding()获取当前请求对象设置的编码字符集,getCharacterEncoding()里获取编码字符集的逻辑对QueryString 的解码起着至关重要的作用,如果编码字符集不对,会直接造成QueryString中文解码错误而最终乱码 ,后面会详细介绍。

 (2)设置参数解码的字符集,调用org.apache.coyote.tomcat5.Parameters对象的setEncoding()和setQueryStringEncoding()方法,其中setEncoding()设置编码字符集的作用于POST方式提交的参数的解码,setQueryStringEncoding()设置的编码字符集作用于GET方式提交的参数的解码.值得一提的是在CoyoteAdapter的service方法中曾有req.getParameters().setQueryStringEncoding(connector.getURIEncoding());用URIEncoding设置编码字符集,这个是没有意义的,最终在这儿会覆盖前面的设置

   如果getCharacterEncoding()获取编码为null,即没有设置编码字符集,使用web容器缺省编码iso-8859-1,因此如果QueryString含有中文,必须保证 getCharacterEncoding()获取到的不是null或iso-8859-1,否则中文字符必乱无疑

 (3)调用handleQueryParameters()进行QueryString参数解析和解码 ,在这里,根据前面获取的字符集,对参数名和值进行解码操作


 (4)读取请求体,并解析和解码参数.处理方式和第(3)步是一样的,这一步只对POST提交的请求有效,GET方式提交的请求在第(3)步已经返回, 如果使用了getReader()或getInputstream()方法读取POST请求,这一步也不会执行 .(这是Servlet规范)

 

既然getCharacterEncoding()至关重要, 下面详细分析获取请求对象编码的逻辑getCharacterEncoding():

获取请求对象编码的逻辑:

(1) 检查当前请求对象charEncoding(代表编码的字符集)是否已经有值,(有可能之前调用过setCharacterEncoding()设置过编码),直接返回charEncoding,如果在这之前已经解析过当前的charEncoding(有可能为null),标记charEncodingParsed的为true的情况,也直接返回charEncoding

(2)如果(1)没有直接返回,尝试从请求头ContentType中的 charset对应的值获取编码字符集设置为charEncoding,置charEncodingParsed标志为true

   如果charEncoding不为null,直接返回charEncoding

(3) 如果(2)中charEncoding为null,尝试从web应用的自定义部署描述符文件tongweb-web.xml中获取编码

    a.首先从隐藏字段获取编码字符集,这个隐藏字段的名字在标签<parameter-encoding>中的属性form-hint-field定义,web应用需要在jsp或这html中的Form表单中加上该隐藏字段确定请求编码的字符集加以提交,或者直接通过在URL 中QueryString中加上该隐藏字段以GET方式提交

    b.如果从隐藏字段获取编码仍为null,使用标签parameter-encoding中的属性default-charset定义的编码

    c.如果从default-charset获得的编码仍然是null,从标签<locale-charset-map>中获取Locale对应编码,比如zh-cn对应的编码字符集就是GBK

    d.如果以上最终得到的 编码不为null,调用setCharacterEncoding()设置请求对象的编码


    从以上web容器处理的逻辑可知,web应用设置请求对象编码有以下方式:

 显示的调用setCharacterEncoding() 
 在自定义部署描述符文件中定义标签<parameter-encoding>的属性form-hint-field,并且使用这个属性定义的名字作为隐藏字段,POST提交或GET提交设置请求对象编码 
在自定义部署描述符文件中定义标签<parameter-encoding>的属性default-charset设置请求对象编码,这个只有在form-hint-field无效时可用 
在自定义部署描述符文件中定义标签<locale-charset-map>,这个只有在default-charset无效时可用 
    优先级是从上到下 ,而且前两种作用于某次请求,后两种是全局性的,在整个web应用中任何一次请求都有效


3.应答的处理

  web容器使用当前应答对象Response设置的编码字符集(可通过调用Response对象的setCharacterEncoding()设置)来将经过jsp或Servlet处理的内容编码成字节数组准备发送给web容器,如果没有设置setCharacterEncoding,缺省使用iso-8859-1 
 web应用中如果没有设置应答(Response)对象的应答头ContentType或者没有设置ContentType中的charset,web容器使用缺省的ContentType,其中charset值是iso-8859-1,浏览器使用此字符集来解码web容器发送的应答内容 
从以上分析可看出,将应答对象的编码设置成ContentType中charset指定的编码以致是不会乱码的,同时应答内容如果含有中文的话,不要使用缺省编码iso-8859-1

 

 

 

4.forward/included,重定向的请求编码

included请求

included请求是在一个jsp中include另一个jsp内容,在include另一个jsp可传递参数,一般使用类似以下jsp标签include另一个请求

<jsp:include flush="true" page="中国.jsp">
 <jsp:param name="name" value="中国"/>
 <jsp:param name="title" value="中文"/>
</jsp:include>

其中<jsp:param>代表传递的参数名及其值

Web容器在将include的jsp转化成Servlet时,对于include部分会按下面部分生成代码:

org.apache.jasper.runtime.JspRuntimeLibrary.include(request, response, "中国.jsp" + (("中国.jsp").indexOf('?')>0? '&': '?') + org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode("name", request.getCharacterEncoding())+ "=" + org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode("中国", request.getCharacterEncoding()) + "&" + org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode("title", request.getCharacterEncoding())+ "=" + org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode("中文", request.getCharacterEncoding()), out, true);

从以上代码可看出,访问included的jsp也是以一个请求URL的形式存在,参数部分按QueryString来传递.<jsp:param>的参数名值已经进行了URL编码,编码采用的字符集就是从请求对象getCharacterEncoding()获取的编码,getCharacterEncoding()的逻辑前面已经详细介绍,可见请求对象的编码字符集也影响到included请求的参数,但是PathInfo部分并未做URL编码,这是因为Web容器在处理include请求时,不再经过Connector部分进行URIEncoding 的解码。因此如果PathInfo部分含有中文字符,要保证不乱码,从而能访问到included的jsp,需保证在jsp在转换成Servlet时并编译成class文件时没有乱码,这就要正确设置jsp的pageEncoding或contentType中charSet,以及编辑jsp文件的IDE使用的编码,Java虚拟机编译时的编码,这些编码字符集保持一致是最好的,当然不能是单字节字符集iso-8859-1

也可以在Servlet中include另一个请求,代码类似这样的 request.getRequestDispatcher("中国.jsp?name=中国&title=中文").include(request,response);

这时PathInfo和QueryString都没有进行过 URL编码,这时跟上面PathInfo的处理一样,必须Servlet在编译成class文件时没有乱码

forward请求

forward请求是在一个jsp中forward到另一个jsp,最终发送给浏览器的是最后一个jsp的内容,跟include一样在另一个jsp可传递参数,一般使用类似以下jsp标签forward到另一个请求

<jsp:forward page="中国.jsp">
   <jsp:param name="name" value="中国.jsp"/>
    <jsp:param name="title" value="中文"/>
</jsp:forward>

Web容器在forward的前一个jsp转化成Servlet时,对于forward部分会按下面部分生成代码:

 _jspx_page_context.forward("中国.jsp" + (("中国.jsp").indexOf('?')>0? '&': '?') + org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode("name", request.getCharacterEncoding())+ "=" + org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode("中国.jsp", request.getCharacterEncoding()) + "&" + org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode("title", request.getCharacterEncoding())+ "=" + org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode("中文", request.getCharacterEncoding()));

也可以在Servlet中forward到另一个请求,代码类似这样的 request.getRequestDispatcher("中国.jsp?name=中国&title=中文").forward(request,response);

从以上代码看出forward请求在URL编码上的处理更Include请求是一样的

重定向

重定向指的是从当前请求转向到另一个请求,按照http协议,web容器和浏览器交互过程是这样的:

web容器设置应答状态码为302 
web容器设置应答头Location 及其值(重定向请求的url) 
将应答状态码和应答头发送给浏览器 
浏览器按照状态码302确认为重定向请求,并按应答头Location中的url向web容器发送新的请求 
 这个过程中存在新的 URL,如果其中含有中文,按照Glassfish现有的代码实现,在设置应答头时,直接将一个中文字符强制转换成一个字节,这会造成中文字符损失以致乱码,也就是说在发送给浏览器之前已经乱了,从而重定向请求失败,Tomcat6也有此问题,这方面weblogic9.2中文版有好的表现,没有设置任何参数,用GBK进行了编码,对于重定向中文URL处理得较好,但是如果URL中PathInfo 部分也含有中文字符,不同浏览器却有不同的表现,中文 IE对于应答头Location,将URL是按照PathInfo用UTF-8进行URL编码,其他部分不变,而中文FireFox将 URL中 PathInfo和QueryString都用GBK做了URL编码,按照前面所述,如果 URIEncoding一直为UTF-8(这个值相对比较稳定,不会常修改),这会导致在中文Firefox重定向到一个URL时会访问失败,报404错误

Cookie的编解码

含有Cookie的请求时web容器和浏览器的交互过程:

  web 应用中调用应答对象(Response)的addCookie()方法设置Cookie 
  web容器设置应答头"Set-Cookie" , 
 web容器在发送应答前将应答头发送给浏览器 
浏览器根据应答头"Set-Cookie"在浏览器端创建Cookie 
下次请求时浏览器将Cookie作为请求头"Cookie"发送给web容器 
服务端web应用程序调用请求对象(Request)的getCookies ()方法时,会解析 Cookies,对于该次请求来说,仅仅一次,下次直接获得 
   当然,如果是通过其他途径创建的Cookie,比如JavaScript程序创建的Cookie,只存在浏览器发送Cookie请求头的步骤

从以上的交互过程看出,Cookie也是作为请求头/应答头在浏览器和web容器之间传递的,同重定向一样,Cookie的处理也存在编解码的过程,幸运的是,Glassfish在自定义部署表述符文件中提供标签encodeCookies,容许对Cookie中的名称和值进行URL编码,而tomcat如果 Cookie中含有中文字符,

addCookie()时直接抛出异常,weblogic虽不抛出异常,但是会乱码,Glassfish在Cookie的创建和解析时均可以用UTF-8对Cookie进行编解码,字符集为UTF-8是写死的。

猜你喜欢

转载自hejiajunsh.iteye.com/blog/1775582