Java で HTTP クライアントを作ってみよう (3)

前へ << Java で HTTP クライアントを作ってみよう (2) 暗号化のお話 (1) >> 次へ

認証

前節で作成した HttpClientHttpURLConnection.java に認証機能を追加しましょう。
HTTP における認証の解説は Basic 認証Digest 認証 をどうぞ。
Java が Basic 認証・Digest 認証に対応したのは (おそらく) Java 1.2 からと思われます。 ただし Digest 認証については Proxy 対応などに不備があったようで、J2SE SDK 1.4 にて Sun 曰く「auth-int を除くすべての機能を提供」というレベルになったようです。 ただし Digest 認証の auth-int には J2SE SDK 5.0 でも未対応です。

取得する URL は当ページ

とします。いずれも、ユーザ名は「hoge」パスワードは「fuga」となっています。

HttpClientHttpURLConnectionAuth.java

   14:         URL url = new URL("http://X68000.q-e-d.net/~68user/net/sample/http-auth/secret.html");
   15:         //URL url = new URL("http://X68000.q-e-d.net/~68user/net/sample/http-auth-digest/secret.html");
   16:         String username = "hoge";
   17:         String password = "fuga";
上の URL が Basic 認証のサンプルページ、下の URL が Digest 認証のサンプルページです。
以下、前節の HTTP クライアント HttpClientHttpURLConnection.java との相違点のみ示します。

まず、HttpAuthenticator というクラスを定義しておきます。これは Authenticator のサブクラスとします。

   61: class HttpAuthenticator extends Authenticator {
   62:     private String username;
   63:     private String password;
   64:     public HttpAuthenticator(String username, String password){
   65:         this.username = username;
   66:         this.password = password;
   67:     }
   68:     protected PasswordAuthentication getPasswordAuthentication(){
   69:         return new
   70:             PasswordAuthentication(username, password.toCharArray());
   71:     }
   72:     public String myGetRequestingPrompt(){
   73:         return super.getRequestingPrompt();
   74:     }
   75: }

そしてサーバに接続する前に上記の HttpAuthenticator オブジェクトを生成し、 Authenticator#setDefault メソッド (static メソッド) で登録しておきます。 こうしておけば、認証が必要になった場合に登録したオブジェクトが自動的に使用されます。
   19:         HttpAuthenticator http_authenticator = new HttpAuthenticator(username, password);
   20:         Authenticator.setDefault(http_authenticator);

認証の成否とは関係ありませんが、サーバが提示した Realm を表示する機能も付けましょう。
   40:         System.out.println
   41:             ("プロンプト(realm)[" + http_authenticator.myGetRequestingPrompt() + "]");
実行結果は以下の通りです。
レスポンスヘッダ:
  Content-Length: [176]
  Connection: [Keep-Alive]
  null: [HTTP/1.1 200 OK]
  Date: [Sat, 05 Mar 2005 15:41:29 GMT]
  Keep-Alive: [timeout=15, max=100]
  Accept-Ranges: [bytes]
  Server: [Apache/2.0.52 (FreeBSD)]
  Content-Type: [text/html; charset=euc-jp]
レスポンスコード[200] レスポンスメッセージ[OK]
プロンプト(realm)[Secret File]

---- ボディ ----
<html>
<head>
  <META HTTP-EQUIV="Content-Type" Content="text/html; charset=EUC-JP">
  <title>ひみつのファイル</title>
</head>

<body>
これは秘密ファイルです。
</body>
</html>
認証に失敗した場合は以下のように例外が発生します。
Exception in thread "main" java.io.IOException
        at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:591)
        at HttpClientHttpURLConnectionAuth.main(HttpClientHttpURLConnectionAuth.java:45)
Caused by: java.net.ProtocolException: Server redirected too many  times (20)
        at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:798)
        at sun.net.www.protocol.http.HttpURLConnection.getHeaderFields(HttpURLConnection.java:1463)
        at HttpClientHttpURLConnectionAuth.main(HttpClientHttpURLConnectionAuth.java:29)
非常に気になるのは「Server redirected too many times (20)」です。 どうやらユーザ名やパスワードが誤っていて認証に失敗した場合、 何度でもリトライをして、最終的にはリトライ回数が 20回に達するときにあきらめてしまうようです。 web サーバのログを見ると、短時間に 19回接続していることがよくわかります。 ある意味、小規模な DoS アタックと言えるでしょう。こんなんでいいんでしょうかね? (J2SE SDK 1.4.2_07 で確認)

ところで当ページ管理人は

  • 「なぜユーザ認証を行いたいだけなのに、わざわざクラスなんぞ作らなくてはならないのか?」 (そんなにコールバックするのが重要か?)
  • 「なぜ Authenticator#setDefault は static メソッドなんだろうか?」
  • 「なぜ PasswordAuthentication というデータを保持するだけのクラスが必要なのだろうか?」
という点が理解できません。例えば、以下のような設計にしなかった理由は何なんでしょうか?
Authenticator authenticator = new Authenticator(username, password);
urlconn.addAuthenticator(authenticator);
System.out.println("Realm[" + authenticator.getRequestingPrompt() + "]");
本節は think or die: HTML 変換処理 を参考にさせていただきました。ありがとうございました。

Proxy 対応

HttpURLConnection で proxy を経由してアクセスするには、 あらかじめ以下のようにプロパティをセットしておきます。
System.setProperty("http.proxyHost", "proxy.example.com");
System.setProperty("http.proxyPort", "8080");
もし proxy を経由させたくないホストがあれば、上記に加えて
System.setProperty("http.nonProxyHosts", "localhost");
と nonProxyHosts を設定します。

プロパティは java コマンドのオプションとしても指定することができます。

% java -Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8080 \
       -Dhttp.nonProxyHosts=localhost ...
ところでなぜこんなしょーもない設計になっているんでしょうかね? HttpURLConnection クラスに setProxy(String host, int port) などというメソッドを用意するのがスジだと思うのですが。
前へ << Java で HTTP クライアントを作ってみよう (2) 暗号化のお話 (1) >> 次へ

ご意見・ご指摘は Twitter: @68user までお願いします。