標籤:

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 plaincopyString?str="中文";??用str.getBytes("iso-8859-1");--中文字元損失了??用String?newStr=newString(str.getBytes("GBK"),"iso-8859-1");??中文字元還有救,通過newString(newStr.getBytes("iso-8859-1"),"GBK");中文字元轉回來了??不同http客戶端有不同的實現和配置,具體情況具體對待,這裡不做分析.這裡還要提到一點:對於 html中超級鏈接<a href="url" >形式的請求,瀏覽器也是按GET方式提交的,跟表單(Form)方式的GET請求還是有點細微差別,中文IE並不對QueryString進行URL encode,而是直接進行encode,中文FireFox則是進行URL encode2.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對應的編碼字符集就是GBKd.如果以上最終得到的 編碼不為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-14.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是寫死的。
推薦閱讀:

冬天皮膚總是干?可能是這裡出現問題了,先給肺保濕!
不善於交往的人如何解決人際關係問題(本人轉中啟心理)
你最大的問題是書讀得太少而想買的太多
回顧 | 睿智共修會7:如何把問題轉化為資源(下)

TAG:編碼 | 問題 |