SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Javaによる簡易HTTPサーバーの作成

Basic認証・Digestアクセス認証の実装

Javaによる簡易HTTPサーバーの作成 第2回


解説

HTTP認証の種類

 HTTPの認証はRFC2617で規定されています。1つはBasic認証で、もう1つはDigestアクセス認証です。

Basic認証

 HTTPリクエストヘッダに、ユーザー名とパスワードの組をBase64でエンコードして送信します。Bas64はバイナリデータを文字列化するための標準的な手法のため、平文の利用と変わりません。このため、盗聴に対して弱い認証方法です。

Digestアクセス認証

 HTTPリクエストヘッダに、ユーザー名とパスワード、およびnonceと呼ばれるサーバーが発行する固有の文字列やURIを組み合わせたハッシュ(メッセージダイジェスト)を生成して送信します。ハッシュから元の文字列を求めることはできないため、Basic認証と異なりパスワードが盗聴により暴露されることはありません。また本記事では利用していませんが、Digestアクセス認証には、コンテント全体の完全性(改ざんされていないこと)を保証するためのプロトコルも用意されています。ただし、Digestアクセス認証が標準で利用するMD5は衝突に対する脆弱性が発見されているため、完全性については信用できないと考えたほうが無難だと思います。

 ところで、J2EEでWebアプリケーションを開発する場合に良く利用される、フォーム認証が出て来ないことを疑問に思われるかも知れません。フォーム認証は、HTTPというプロトコルでサポートされた認証方式ではなく、Webアプリケーションのフレームワークによる認証なので、ここには出て来ません。

 ユーザーから見た場合、Basic認証やDigestアクセス認証と、フォーム認証の差は認証ダイアログをブラウザが表示するかWebアプリケーションがフォームとして表示するかの違いですが、HTTP上では認証情報がリクエストヘッダに含まれるのかそれともコンテントボディに含まれるのかといった根本的な差となります。

認証処理の実装

 認証処理は、前回取り上げたHttpServerクラスに対し、実際に認証処理を行うクラス(Authorityクラス)を追加するようにして実装しています(図)。

クラス図
クラス図

 HttpServerの利用者は、引数として認証の種類を指定してserviceメソッドを呼び出します。

 次のリストはHttpServermainメソッドで、Digestアクセス認証を指定してHttpServerserviceメソッドを呼び出している個所です。

HttpServerの利用コード
HttpServer server = new HttpServer(port);
server.setDebug(debug);
server.service(Authority.DIGEST);
server.close();

 呼び出されたserviceメソッドでは、指定された認証の種類に対応したオブジェクトをAuthorityクラスのファクトリメソッドから取得してフィールドに設定します。

HttpServerのサービスメソッド
public void service(String authtype) throws IOException {
    assert serverSocket != null;
    authority = Authority.newAuthority(authtype);

 実際にAuthorityオブジェクトを利用するのは、リクエスト処理の過程です。

 次のリストは、HttpServer.Requestオブジェクト(個々のクライアントごとのリクエストを処理するクラス)のコンストラクタです。リクエストヘッダをすべて読み終えた時点で、認証処理を呼び出しています。

HttpServer.Requestのコンストラクタ
Request(Socket sock) throws IOException {
    assert authority != null;
    in = sock.getInputStream();
    header();
    // この時点でヘッダメタデータは確保され、
    // InputStreamにはコンテンツが残っている。
    if (debug) {
        System.out.println(this);
        for (int i = 0; i < metadata.length; i++) {
            System.out.println(metadata[i]);
        }
    }
    authority.authorize(metadata);
}

 Authority#authorizeは、ヘッダメタデータ(リクエストヘッダデータ)を引数に取り、認証するメソッドです。認証出来ない場合には例外をスローするため、呼び出しの戻り値判定はありません。

 認証に失敗した場合、サーバーはクライアントへエラーを返します。この時、サーバーは認証のための情報をレスポンスヘッダへ含めます。

HttpServerのレスポンス出力
void response(int stat, String msg, OutputStream out)
    throws IOException {

    PrintWriter prn = new PrintWriter(out);
    prn.print("HTTP/1.1 ");
    prn.print(stat);
    prn.print(" ");
    prn.print(msg);
    prn.print("\r\n");
    authority.challenge(prn);
    prn.print("\r\n\r\n");
    prn.flush();
}

 以上の2個所を見ておわかりのように、Authorityクラスを利用するHttpServerクラスは、認証を行うか行わないか、あるいはBasic認証を利用するのかDigestアクセス認証を利用するのかに関わらず、常にAuthorityオブジェクトの同一メソッドを呼び出します。

多態とNullObjectパターン

 Javaのようなオブジェクト指向言語を利用する場合、プログラミングで意識すべきことは、条件判断を可能な限り少なくすることです。

 たとえば、上に引用したauthorizeメソッド呼び出しの個所は、非オブジェクト指向言語では以下のようなコードになるのではないでしょうか。

非オブジェクト指向プログラミングスタイルでの認証処理
if (authority != null) {
    // もし認証処理を行うのであれば
    if (authority.type == Authority.BASIC) {
        // Basic認証処理
        authority.basicAuthorize(metadata);
    } else if (authority.type == Authority.DIGEST) {
        // Digestアクセス認証処理
        authority.digestAuthorize(metadata);
    } else {
        assert false;
    }
}

 構造化言語によってgotoが追放された(少なくても推奨されなくなった)理由は、コードが複雑化しバグが入りやすくなったり、制御の流れを追いにくくなるため保守性が損なわれることでした。

 オブジェクト指向言語の特徴として、オブジェクトによるモデルの写像や、継承による再利用性ということが良く言われますが、おそらくそれ以上に重要なことは、多態を利用することで、コードから条件判断による複数のブロックを利用した制御構造を排除できることです。

 上のif文を利用したコードと次のリストを見比べれば、多態を利用することによるコードの読みやすさの向上は明らかです。

authority.authorize(metadata);

 多態によるコードの整理という意味で、この例であればBasic認証とDigestアクセス認証の2つについては、いずれも認証処理の実体があるため通常は多態を利用することになると思います。しかし、認証しない場合については認証処理がそもそも存在しないため対応するクラスも定義されず、したがって次のようにコードすることはありがちです。

if (authority != null) {
    authority.authorize(metadata);
}

 NullObjectパターンは、このような本来nullかどうかの条件判断が必要な場合であっても条件判断を不要にするために、nullの場合の処理を実装したオブジェクトを用意するデザインパターンです。

 以下にAuthorityクラスでの、NullObjectパターンの実装方法について説明します。なお、実装でstaticネステッドクラスを利用しているのは、ソースファイルの数を少なくしたいことが理由で、必要性があるわけではないことに注意してください。おそらくNullAuthクラスについてはわざわざ独立させるほどの処理があるわけでもないのでstaticネステッドクラスで構わないと思いますが、DigestAuthクラスは独立したソースファイルにしたほうが修正の可能性から望ましいでしょう。

認証処理クラスのファクトリメソッド
public static Authority newAuthority(String type) {
    if (BASIC.equals(type)) {
        return new BasicAuth();
    } else if (DIGEST.equals(type)) {
        return new DigestAuth();
    }
    return new NullAuth();
}

 ファクトリメソッドでは、引数から認証の型を判定してBasic認証、Digestアクセス認証、そして認証無しの場合、それぞれのオブジェクトを生成しています(より堅牢にコードするのであれば、BASIC、DIGEST、nullに当てはまらない場合はIllegalArgumentExceptionをスローするようにすべきでしょう)。多態を利用する場合、この例のように条件判断はオブジェクト生成時に1回だけ行われることになります。

認証処理のテンプレート
/**
 * 認証を行う。
 * @param metadata ヘッダメタデータの配列
 */
public void authorize(String[] metadata) {
    for (int i = 0; i < metadata.length; i++) {
        Matcher m = AUTHORIZATION.matcher(metadata[i]);
        if (m.find()) {
            if (checkCredentials(m.group(1))) {
                return;
            }
            break;
         }
    }
    handleNoCredential();
}
/**
 * レスポンスヘッダを出力する。
 * @param writer 出力先
 */
public abstract void challenge(PrintWriter writer)
    throws IOException;
/**
 * 証明書を検証する。
 * 既定ではすべてエラーとする。
 * @param credentials 証明書
 * @return 不正ならば偽(またはAuthorizationExceptionをスロー)
 */
boolean checkCredentials(String credentials) {
    return false;
}
void handleNoCredential() {
    throw new AuthorizationException("Authorization Required");
}

 HttpServerが呼び出している2つのメソッドのうち、認証を行うauthorizeメソッドはテンプレートメソッドで、デフォルトでは認証エラーとなるように実装しています。また認証情報を出力するchallengeメソッドは抽象メソッドです。

 これらのうち、単純なNullAuthBasicAuthの実装を次に示します。DigestAuthは認証のためのフィールドが多く、RFC2617の詳細に入る必要があるためここでは割愛します。

認証処理の実装(NullAuthとBasicAuth)
static class NullAuth extends Authority {
    boolean checkCredentials(String credentials) {
        return true;
    }
    void handleNoCredential() {
    }    
    public void challenge(PrintWriter writer) throws IOException {
    }
}
static class BasicAuth extends Authority {
    boolean checkCredentials(String credentials) {
        Matcher m = BASIC_CREDENTIAL.matcher(credentials);
        if (m.find()) {
            String upass = new String(Base64Util.decode(m.group(1)));
            if ("web:2.0".equals(upass)) {
                return true;
            }
        }
        return super.checkCredentials(credentials);
    }    
    public void challenge(PrintWriter writer) throws IOException {
        writer.print("WWW-Authenticate: ");
        writer.print("Basic");
        printRealm(writer);
    }
}

 NullAuthの場合、認証は無条件にOK、認証情報の出力は何も行わない、という実装です。一方、BasicAuthの方は、ユーザー名/パスワードの検証や認証情報の出力を行います。

 このように、行わないという場合も含めてオブジェクトを作ることで、呼び出し側での条件判断を不要とするというNullObjectパターンは非常に重要な実装テクニックなので、ぜひともプログラミングに取り入れるようにしてみましょう。

 たとえば、こういうことも可能だという例を以下に示します。

処理をしないというバグの調査のためにログを入れる
// null判断をHttpServerで行う場合
if (authority != null) {
    log("with auth, type = " + authority.getClass());
    authority.authorize(metadata);
} else {
    log("no auth");
}

// NullObjectを利用する場合
    public void authorize(String[] metadata) {
        log("auth:" + getClass());
        ...

 NullObjectを利用するということは、必ず特定のクラスのメソッドが呼ばれるということなので、このような場合に修正個所が少なくて済む(あるいは漏れにくい)ということや、将来的にAOPフレームワークの適用が可能となるということもメリットです。

メッセージダイジェストの利用方法

 Javaでメッセージダイジェストを利用するには、java.security.MessageDigestクラスを利用します。

 MessageDigestクラスも、上のAuthorityクラスのようにstaticなファクトリメソッドによってサブクラスのインスタンスを取得するパターンを実装しているため、直接newするのではなく、利用するアルゴリズムを指定してインスタンスを取得します。

MD5によるハッシュ(Authority.java)
static String md5(String s) throws NoSuchAlgorithmException {
    MessageDigest md = MessageDigest.getInstance("MD5");
    return toLowerHex(md.digest(s.getBytes()));
}

 ここではハッシュ対象のデータが少ないため2行で済ませていますが、ファイルのように大きなサイズのデータをハッシュする場合には、MessageDigest#udpateメソッドを呼び出して、最後にdigestメソッドで結果を得るようにします。

大きなサイズのハッシュ(md変数がMessageDigestのインスタンス)
byte[] buff = new byte[BUFFSIZE];
int len;
while ((len = input.read(buff)) > 0) {
    md.update(buff, 0, len);
}
byte[] result = md.digest();

 なお、既に触れたようにMD5(およびSHA-1)には問題が見つかっています。本記事で扱っているHTTPのDigestアクセス認証についてはともかくとして、ハッシュを利用するアプリケーションを開発する場合、もしアルゴリズムを選択できるのであれば、J2SE 5以降で実装されているSHA-256以上を利用した方が良いでしょう。

 また、暗号分野の専門家は問題が見つかることを前提として、アルゴリズムを交換できるようにアプリケーションを開発することを勧めています。J2SEのMessageDigestはファクトリメソッドに利用したいアルゴリズム名を指定することでオブジェクトを交換できることと、そのようにして生成したオブジェクトは多態によってアプリケーションからは同じオブジェクトとして扱うことができることから、比較的良い条件にあると考えることができるでしょう。

仕様と実際が異なる場合の対応

 RFC2617には以下の記述があります。

3.2.1 The WWW-Authenticate Response Headerから抜粋
qop-options       = "qop" "=" <"> 1#qop-value <">
qop-value         = "auth" | "auth-int" | token
3.2.2 The Authorization Request Headerから抜粋
message-qop      = "qop" "=" qop-value
qop

(略)Note that this is a single token, not a quoted list of
 alternatives as in WWW- Authenticate.(略)

 これを読む限り、リクエストヘッダに含まれるqopパラメータは、

qop=auth

 のように""無しとなるはずです。最初に引用したqop-optionsは明示的に""を付けていますが、これはレスポンスヘッダのパラメータで、その理由はレスポンスはqop-valueのリストとなる可能性があるからです。

 したがって、最初、DigestAuthの実装でレスポンスヘッダのqopを検証する個所は

static final Pattern DIGEST_CREDENTIAL_QOP
    = Pattern.compile("qop=auth");

 と定義した正規表現を利用していました。HttpServer側がauthのみを利用しているため、これ以外の値(auth-intなど)をクライアントが返すことは無いからです。

 しかし、実際に試してみたところ、Firefoxはqop=authと設定しますが、IE6とSafariはqop="auth"と設定することがわかりました。

 このように、仕様と実装が異なることは良くあります。こういう場合に、マイクロソフト社やアップル社に修正を依頼すべきでしょうか? 少なくとも現時点でネットワークにつながっているコンピュータでは、その実装が動いているということを鑑みれば、RFCにどう書いてあるかは実際には重要ではないということになるでしょう。

 そのため、上の正規表現を

static final Pattern DIGEST_CREDENTIAL_QOP
    = Pattern.compile("qop=\"?auth\"?");

 と変更してqop="auth"でもqop=authでも受け付け可能なようにしています。

 このように、実際に利用されているプログラムが仕様的には正しくないことがあります。したがって、広く利用されることを前提としたプログラムを開発する場合には、できるだけ早め早めに実際の環境でテストするようにすべきです。

まとめ

 HTTPの認証には、平文をBase64でエンコードしただけのBasic認証、MD5でハッシュするDigestアクセス認証という2つの方法があります。

 本記事では、これらの実装を例として

  • NullObjectパターンの利用
  • MessageDigestクラスの利用
  • 仕様と実装が異なることがあるということ

 などについて説明しました。

 Digestアクセス認証の実装そのものはパラメータが非常に多いため、本記事では説明を割愛していますが、アーカイブのソースではある程度の実装はしてあるため、参照してください。

 また、テストクラス(JUnitを利用)では、HttpURLConnectionクラスを利用して認証処理のテストを行うコードが記述してあります。このコードは、Basic認証やDigestアクセス認証を行っているサーバーに対してアクセスする、クライアントコードを記述する場合に多少の参考となるでしょう。

参考資料

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
Javaによる簡易HTTPサーバーの作成連載記事一覧

もっと読む

この記事の著者

arton(アートン)

専門は業界特化型のミドルウェアやフレームワークとそれを利用するアプリケーションの開発。需要に応じてメインフレームクラスから携帯端末までダウンサイジングしたりアップサイジングしたりしながらオブジェクトを連携させていくという変化に富んだ開発者人生を歩んでいる。著書に『Ruby③ オブジェクト指向とはじめての設計...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/225 2006/06/14 18:56

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー