Akira's Tech Notes

Java/JVM | GNU/Linux | Emacs/Lisp | 知的好奇心駆動

header-icon
ネイティブでない日本語で思い付くことや気になることをダラダラ書く、体裁とかは気にしない。読みづらいと感じた時に随時更新する。

[調査]URLパラメータデコード処理について

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プロトコル上明確に定義されていないため、ベンダによって実装が変わる。主に以下のよ うな処理パターンが存在するでしょう。

  1. HTTPヘッダにContent-Type値に基づく、URLパラメータの文字コードを決める
  2. APサーバの設定パラメータに基づく、URLパラメータの文字コードを決める
  3. 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)
  1. 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)
    
  2. 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)
    
  3. 上記2の説明に含まている
  4. 今回の調査範囲外のため、別途…

3.1 URIEncoding属性値

conf/server.xml にConnectorのURIEncoding属性値にてURLパラメータの文字コードのデフォ ルト値を指定することができる。ほどんどんのブラウザはUTF-8を採用しているため、この値を UTF-8 に見直すべきでしょう。

3.2 useBodyEncodingForURI属性値

conf/server.xml にConnectorのuseBodyEncodingForURI属性値がtrueの場合、 リスト14455 行目が実行され、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 起動パラメータで指定する

URL デコードする際の文字エンコーディングを指定する方法

-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フェーズにて行うのが適切です。

Comments