Table of Contents
URLパラメータの日本語文字が化けたので、APサーバのURLパラメータデコード処理について 調べました。
1 URLパラメータのエンコーディング仕様
HTTPプロトコルのGETメソッドで通信する場合、URLパラメータをエンコーディングしなければ いけません。その仕様は rfc3986:Uniform Resource Identifier (URI): Generic Syntax にて 定義されている。
例えばブラウザに下記URLを入力して送信する。
http://www.yahoo.co.jp/?param1=あきら
パケットレベルで実際に送信された内容を確認するとURLに日本語パラメータ部分がパーセン トエンコーディングされていることが分かります。
GET http://www.yahoo.co.jp/?param1=%E3%81%82%E3%81%8D%E3%82%89 HTTP/1.1 Host: www.yahoo.co.jp User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Firefox/38.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: ja,en-US;q=0.7,en;q=0.3 Accept-Encoding: gzip, deflate
param1=あきら
にマルチバイト部分が下記のイメージでエンコードされる。
+-------------------+-------------------+-------------------+ | あ | き | ら | +-----+------+------+-----+------+------+-----+------+------+ | -29 | -127 | -126 | -29 | -127 | -155 | -29 | -126 | -119 | ← ステップ1 +-----+------+------+-----+------+------+-----+------+------+ | 0xe3| 0x81 | 0x82 | 0xe3| 0x81 | 0x8d | 0xe3| 0x82 | 0x89 | ← ステップ2 +-----+------+------+-----+------+------+-----+------+------+ | %E3 | %81 | %82 | %E3 | %81 | %8D | %E3 | %82 | %89 | ← ステップ3 +-----+------+------+-----+------+------+-----+------+------+
表1
- ステップ1: “あきら”文字列をUTF-8符号化バイト表現に変換する
- ステップ2: 16進数に見直す。1オクテットは、2桁の十六進表記で表現することができる。
- ステップ3: 2桁の十六進表現を大文字にして、先頭に”%”を追加する
ステップ1の文字コード(UTF-8)はブラウザの実装/設定に依存する。一般にUTF-8を採用するこ とが多いい。
Javaに下記URLエンコード/デコード用の標準APIが用意されている。
表1
にエンコードの流れについて下記サンプルプログラムで確認することができる。
import java.io.UnsupportedEncodingException; import java.net.URLEncoder; public class URLEncoderTest { public static void main(String[] args) throws UnsupportedEncodingException { System.out.println(URLEncoder1.encode("あきら", "UTF-8")); // UTF-8バイト表現を出力する byte[] bs = "あきら".getBytes("UTF-8"); for (byte b : bs) { System.out.print(b); System.out.print(" | "); } System.out.print("\n"); // 16進数表現を出力する for (byte b : bs) { System.out.println(Integer.toHexString(b)); } } }
出力結果
%E3%81%82%E3%81%8D%E3%82%89 -29 | -127 | -126 | -29 | -127 | -115 | -29 | -126 | -119 | ffffffe3 ffffff81 ffffff82 ffffffe3 ffffff81 ffffff8d ffffffe3 ffffff82 ffffff89
2 文字化けの原因
HTTPリクエストを受ける側(Web/APサーバ)、上記 表1
と逆順でURLパラメータをデコードし
なければいけません。URLパラメータのデコード処理はAPサーバを隠蔽してくれるので業務AP側
あんまり意識しないかもしれないですが。ステップ1でクライアントとAPサーバが異なる文字
コードを使用すると文字化けが起こりえるので要注意です。
URLパラメータの文字コードについて、クライアントとAPサーバ間のネゴシエーション仕様は HTTPプロトコル上明確に定義されていないため、ベンダによって実装が変わる。主に以下のよ うな処理パターンが存在するでしょう。
- HTTPヘッダにContent-Type値に基づく、URLパラメータの文字コードを決める
- APサーバの設定パラメータに基づく、URLパラメータの文字コードを決める
- javax.servlet.ServletRequest.setCharacterEncoding(String env) APIで設定された値に 基づく、URLパラメータの文字コードを決める
HTTPヘッダのContent-Type値はPOSTリクエストにHTTPボディ部分の文字コードを示す項目です が、GETリクエストにContent-Type値を含めることは稀に見ないでしょう。
3 Tomcat7の実装
環境: apache-tomcat-7.0.47
検証用サンプルプログラム profiler.MainServlet.java
package profiler; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class MainServlet */ @WebServlet("/MainServlet") public class MainServlet extends HttpServlet { private static final long serialVersionUID = 1L; public MainServlet() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // URLパラメータを出力する System.out.println(request.getParameter("param1")); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } }
Debugモードで上記サンプルをトレースしてみると、URLパラメータデコード処理時の実行スタッ クは以下となります。
at org.apache.tomcat.util.buf.UDecoder.convert(UDecoder.java:100) at org.apache.tomcat.util.http.Parameters.urlDecode(Parameters.java:489) at org.apache.tomcat.util.http.Parameters.processParameters(Parameters.java:396) at org.apache.tomcat.util.http.Parameters.processParameters(Parameters.java:501) at org.apache.tomcat.util.http.Parameters.handleQueryParameters(Parameters.java:194) at org.apache.catalina.connector.Request.parseParameters(Request.java:3059) at org.apache.catalina.connector.Request.getParameter(Request.java:1151) at org.apache.catalina.connector.RequestFacade.getParameter(RequestFacade.java:384) at profiler.MainServlet.doGet(MainServlet.java:32)
実行スタックから見ると javax.servlet.ServletRequest.getParameter(String name) が初回 呼ばれた時に次のJavaクラスが実行され、URLパラメータのデコード処理が行われることが分か ります。
※記述が冗長のため、以降はRequestとParametersで記述する。
以下はParameters.javaにURLパラメータの文字コード決める部分の抜粋です。
1 |package org.apache.tomcat.util.http; 2 |(中略) 3 |public final class Parameters { 4 | 5 | (中略) 6 | 7 | public void handleQueryParameters() { 8 | (中略) 14行目を呼び出し 9 | processParameters( decodedQuery, queryStringEncoding ); 10 | } 11 | 12 | (中略) 13 | 14 | public void processParameters( MessageBytes data, String encoding ) { 15 | (中略) 22行目を呼び出してから34行目を実行する 16 | processParameters( bc.getBytes(), bc.getOffset(), 17 | bc.getLength(), getCharset(encoding)); 18 | } 19 | 20 | (中略) 21 | 22 | private Charset getCharset(String encoding) { 23 | if (encoding == null) { 24 | return DEFAULT_CHARSET; // ISO-8859-1 25 | } 26 | try { 27 | return B2CConverter.getCharset(encoding); 28 | } catch (UnsupportedEncodingException e) { 29 | return DEFAULT_CHARSET; 30 | } 31 | } 32 | 33 | (中略) 34 | private void processParameters(byte bytes[], int start, int len, 35 | Charset charset) { 36 | 37 | if(log.isDebugEnabled()) { 38 | log.debug(sm.getString("parameters.bytes", 39 | new String(bytes, start, len, DEFAULT_CHARSET))); 40 | } 41 | 42 | (中略) 43 | String name; 44 | String value; 45 | 46 | // ★1 パラメータ名のデコード 47 | 48 | if (decodeName) { 49 | urlDecode(tmpName); 50 | } 51 | 52 | tmpName.setCharset(charset); 53 | name = tmpName.toString(); 54 | 55 | // ★2 パラメータ値のデコード 56 | 57 | if (valueStart >= 0) { 58 | if (decodeValue) { 59 | urlDecode(tmpValue); 60 | } 61 | 62 | tmpValue.setCharset(charset); 63 | value = tmpValue.toString(); 64 | } else { 65 | value = ""; 66 | } 67 | 68 | (中略) 69 | } 70 |}
62
行目でURLパラメータ値パーセントデコード後バイト表現の文字コードを指定しています。
ここのcharset値は次のメカニズムで決められる。
Parameters.javaのqueryStringEncodingフィールド値が設定された場合その値が適用される。
そうでない場合、デフォルト値 ISO-8859-1
が適用される。
Tomcat7にqueryStringEncodingフィールドの値を設定しているところは以下4箇所です。
1| org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:386) 2| org.apache.catalina.connector.Request.parseParameters(Request.java:3048) 3| org.apache.catalina.connector.Request.parseParameters(Request.java:3054) 4| org.apache.catalina.authenticator.FormAuthenticator.restoreRequest(FormAuthenticator.jaba:586)
- HTTPリクエスト受けった時に Request オブジェクトを初期化するところで
conf/server.xml
にConnectorのURIEncoding属性値をRequestオブジェクトの queryStringEncodingに設定する。実行時スタックは下記の通りです。"http-bio-8080-exec-1" - Thread t@23 java.lang.Thread.State: RUNNABLE at org.apache.tomcat.util.http.Parameters.setQueryStringEncoding(Parameters.java:98) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:386) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1041) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:603) at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310) - locked <63f701a0> (a org.apache.tomcat.util.net.SocketWrapper) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745)
- ServletRequest.getParameter(String name)初回実行時にURLパラメータの解析処理、以下
は中身の詳細です。
1 |package org.apache.catalina.connector; 2 |(中略) 3 |public class Request 4 | implements HttpServletRequest { 5 | 6 | (中略) 7 | 8 | /** 9 | * Parse request parameters. 10 | */ 11 | protected void parseParameters() { 12 | 13 | parametersParsed = true; 14 | 15 | Parameters parameters = coyoteRequest.getParameters(); 16 | boolean success = false; 17 | try { 18 | // Set this every time in case limit has been changed via JMX 19 | parameters.setLimit(getConnector().getMaxParameterCount()); 20 | 21 | // getCharacterEncoding() may have been overridden to search for 22 | // hidden form field containing request encoding 23 | 24 | // ★1 25 | // org.apache.coyote.Request.javaのcharEncodingフィールド値、 26 | // もしくはHTTPヘッダのContent-Type値から文字コード情報の取得 27 | String enc = getCharacterEncoding(); 28 | 29 | // ★2 30 | // server.xmlにConnectorのuseBodyEncodingForURI属性値の取得 31 | boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI(); 32 | 33 | 34 | if (enc != null) { 35 | // ★3 36 | // HTTPヘッダのContent-Type値が指定された場合、適用する 37 | parameters.setEncoding(enc); 38 | 39 | // ★4 40 | // useBodyEncodingForURI属性値がtrueの場合、 41 | // 「★1」で取れた値をorg.apache.tomcat.util.http.Parameters.javaの 42 | // queryStringEncodingフィールドに適用する 43 | if (useBodyEncodingForURI) { 44 | parameters.setQueryStringEncoding(enc); 45 | } 46 | } else { 47 | 48 | // ★5 49 | // 「★1」値が取れない場合、デフォルト値(ISO-8859-1)を適用する 50 | 51 | parameters.setEncoding 52 | (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING); 53 | if (useBodyEncodingForURI) { 54 | parameters.setQueryStringEncoding 55 | (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING); 56 | } 57 | } 58 | // org.apache.tomcat.util.http.Parameters.javaの解析処理をコール 59 | parameters.handleQueryParameters(); 60 | 61 | (中略)
リスト1
profiler.MainServlet.java
サンプルプログラムに置いて、上記27
行目実行時のスタッ クトレースを以下に示す。"http-bio-8080-exec-1" - Thread t@29 java.lang.Thread.State: RUNNABLE at org.apache.tomcat.util.http.ContentType.getCharsetFromContentType(ContentType.java:41) at org.apache.coyote.Request.getCharacterEncoding(Request.java:264) at org.apache.catalina.connector.Request.getCharacterEncoding(Request.java:1048) at org.apache.catalina.connector.Request.parseParameters(Request.java:3042) at org.apache.catalina.connector.Request.getParameter(Request.java:1151) at org.apache.catalina.connector.RequestFacade.getParameter(RequestFacade.java:384) at profiler.MainServlet.doGet(MainServlet.java:32)
- 上記2の説明に含まている
- 今回の調査範囲外のため、別途…
3.1 URIEncoding属性値
conf/server.xml
にConnectorのURIEncoding属性値にてURLパラメータの文字コードのデフォ
ルト値を指定することができる。ほどんどんのブラウザはUTF-8を採用しているため、この値を
UTF-8
に見直すべきでしょう。
3.2 useBodyEncodingForURI属性値
conf/server.xml
にConnectorのuseBodyEncodingForURI属性値がtrueの場合、 リスト1
の
44
と 55
行目が実行され、URLパラメータの文字コードとHTTPボディの文字コードが統一
される。
Content-Type HTTPヘッダを含めないGETリクエストの場合 27
行目の値が null のため、URL
パラメータにマルチバイト文字が化けでしょう。
ServletRequest.getParameter(String name)を評価する前に
ServletRequest.setCharacterEncoding(String enc)
で予め
org.apache.coyote.Request.javaのcharEncodingフィールド値を初期化すれば、文字化け問題
を回避することが出来ます。
と言うわけで useBodyEncodingForURI
属性値をtrueに指定する場合、業務APでURLパラメー
タを参照する前に ServletRequest.setCharacterEncoding(String enc)
を実行して文字コー
ド明示した方がよいでしょう。
3.3 設定と挙動のまとめ
No | URIEncoding | useBodyEncodingForURI | Content-Type | 結果 |
---|---|---|---|---|
1 | - | false | 有り/無し | 文字化け |
4 | UTF-8 | false | 有り/無し | OK |
3 | 任意 | true | なし | 文字化け |
2 | 任意 | true | 有り | OK |
Tomcat6の場合、この2つパラメータは次のシステムプロパティとして指定することになります。
- org.apache.catalina.connector.URI_ENCODING
- org.apache.catalina.connector.USE_BODY_ENCODING_FOR_QUERY_STRING
JBossAS7/EAP6もTomcat6と同じ方法です。
4 Weblogic 11gの設定
4.1 起動パラメータで指定する
-Dweblogic.http.URIDecodeEncoding=UTF-8
ただし、この設定は1つのサーバインスタンスで1つのみ可能です。
4.2 weblogic.xmlにて指定する
Mapping IANA Character Sets to Java Character Sets
<input-charset> <resource-path>/foo/*</resource-path> <java-charset-name>UTF-8</java-charset-name> </input-charset>
4.3 ServletRequest.setCharacterEncoding(String env) APIで指定する
文字コード制御用のFilterにて実装することが多いい。
4.4 ADFフレームワーク利用時の注意事項
ADFフレームワーク内でHTTP Requestオブジェクトをラッピング時にFilterで指定した文字コー ド情報をロストしてしまうケースがあります。ADFフレームワーク利用時にJSFフェースリスナー のRESTORE_VIEWフェーズにて行うのが適切です。