PrismBox > Tips >

 
JavaServlet Tips
 
No.0001 コンテナの機能を使わずにBASIC認証を行う
BASIC認証はほとんどのWebサーバー、Webブラウザが対応しており、簡単に使えることが特徴ですが、パスワードはクリアテキスト(平文)で送られます。 実際はE-Mailの添付ファイルでよく使われるBASE64という形式でエンコードされていますが、暗号化されているわけではないのでデコードして改ざんすることも容易です。 ですから、多くの場合はSSLとあわせて暗号化して用いられます。

Tomcat等のサーブレットコンテナにも、このBASIC認証(基本認証)を行う機能が付いています。「web.xml」ファイルで認証方式等を指定し、 コンテナで実際の認証処理を行います。この方法で簡単に認証を実装できるコンテナもありますが、当然ながらコンテナに依存してしまいます。 たとえば、Tomcat用に作ったものは、そのままではWebSphereやWebLogicでは動作しないでしょう。 これが問題になる場合(コンテナに依存したくない場合)に、ここで紹介する方法が1つの解決手段になるのではないかと思います。 また、認証と同時に何か別の処理を行いたい場合にも使えるかもしれません。

そういうわけでSSLによる暗号化までは行っていませんが、サンプルを作成しました。認証の仕組みはフィルタとして実装します。
package sample;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class BasicAuthFilter implements Filter {

    public void init(FilterConfig config) throws ServletException {
        // 何もしない
    }

    public void destroy() {
        // 何もしない
    }

    public void doFilter(ServletRequest req,
                         ServletResponse res,
                         FilterChain chain) throws IOException, ServletException {

        // ServletRequestとServletResponseをHttp*にキャストする
        HttpServletRequest hreq = (HttpServletRequest)req;
        HttpServletResponse hres = (HttpServletResponse)res;

        // セッション変数を調べて承認済みでない場合(=null)は以下の処理を行う
        HttpSession session = hreq.getSession();

        if (session.getAttribute("USER_INFO") == null) {
            // Authorizationヘッダを取得する
            String auth = hreq.getHeader("Authorization");

            if (auth == null) {
                // 未承認でAuthorizationヘッダがない場合は認証を行うように要求する
                requireAuth(hres);
                return;

            } else {
                try {
                    // 未承認でAuthorizationヘッダがある場合はヘッダの内容を
                    // デコードして入力されたユーザー名とパスワードを取得する
                    String decoded = decodeAuthHeader(auth);

                    // ユーザー名とパスワードを分離
                    int pos = decoded.indexOf(":");
                    String username = decoded.substring(0, pos);
                    String password = decoded.substring(pos + 1);

                    // 認証処理
                    UserInfo user = authenticateUser(username, password);

                    if (user.userId == null || user.userId.equals("")) {
                        // ユーザー名がNULLの場合は認証に失敗したとみなして
                        // 再度認証を行うように要求する
                        requireAuth(hres);
                        return;

                    } else {
                        // UserInfoをセッション変数に保存する
                        session.setAttribute("USER_INFO", user);
                    }

                } catch(Exception ex) {
                    // 失敗したら再度認証を行うように要求する
                    requireAuth(hres);
                    return;

                }
            }

        }

        chain.doFilter(req, res);
    }

    private void requireAuth(HttpServletResponse hres) throws IOException {
        // 認証が成功していない場合は「WWW-Authenticate」ヘッダを付加する
        hres.setHeader("WWW-Authenticate", "BASIC realm=\"Authentication Test\"");
        // 応答をクリアして401(UNAUTHORIZED)を設定する
        hres.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }

    private String decodeAuthHeader(String header) {
        String ret = "";

        try {
            // 「Basic 」以降の文字列を抽出
            String encStr = header.substring(6);

            // BASE64をデコードする(sun.misc.BASE64Decoderを使用)
            sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
            byte[] dec = decoder.decodeBuffer(encStr);
            // Authorizationヘッダの内容をデコードしたものを代入
            ret = new String(dec);

        } catch(Exception ex) {
            ret = "";
        }

        return ret;
    }

    private UserInfo authenticateUser(String username, String password) {
        // ユーザー情報を保存するクラス(後述)のインスタンスを作成
        UserInfo u = new UserInfo();

        // 認証処理
        if (username.equals("test") && password.equals("test")) {
            // ユーザー名とパスワードを設定
            u.userId = username;
            u.password = password;
            // ユーザーに割り当てるロールを作成
            u.roles = new String[] {"Users"};
        }

        return u;
    }
}
フィルタを作成するには「javax.servlet.Filter」インターフェースを実装する必要があります。 「Filter」インターフェースを実装すると、init(),destroy(),doFilter() の3つのメソッドを必ず実装しなければいけません。ここでは doFilter() 以外は必要ないので、空のメソッドを用意します。
認証済みか否かはセッション変数の有無で調べています。ここでは UserInfo というクラスを用意し、まずはこれがセッション変数に入っているか否かを調べます。 入っている場合は chain.doFilter で要求された処理をそのまま実行します。入っていなかった場合は、次に「Authorization」ヘッダの有無を調べます。 これが見つからなかった場合は認証が必要であることを通知するために「WWW-Authenticate」ヘッダを付加して応答をクリアし、401のステータス(=認証が必要)を返します。

「Authorization」ヘッダが見つかった場合は認証の処理を行います。ヘッダの中身は次のようになっています。
Basic [BASE64でエンコードされたユーザー名とパスワード]
ここからユーザー名とパスワードを取り出すために「Substring」メソッドで文字列の切り出し、デコードを行います。 今回はデコードに sun.misc.BASE64Decoder を用いています。これはJDKのドキュメントには載っていないクラスなので、今後削除・変更される可能性があり、 利用する際には注意が必要です。さて、デコード後の文字列は
[ユーザー名]:[パスワード]
となっています。「:」の位置を「indexOf」メソッドで探し、「substring」メソッドで分割してユーザー名とパスワードを取得します。

ユーザー名とパスワードが得られたので、次に「authenticateUser」で実際の認証を行います。「authenticateUser」は「UserInfo」クラスのインスタンスを返します。 (「UserInfo」クラスは自作するクラスで、後述します。)
認証に成功すると、「UserInfo」にユーザー名、パスワード、ロールを設定して返します。失敗した場合はすべてにnullを設定した状態で返します。 サンプルでは単純にソースコードに「test」「test」を埋め込んでユーザーロールとして「Users」を返すようにしていますが、 実際にはここでDBへのアクセスを行い、 ユーザー名、パスワードをチェック、そのユーザーが属するロールの配列を取得、などの処理を行います。 ここではユーザーロールとして「Users」のみを割り当てていますが
u.roles = new String[] {"Users", "Administrators", "Developers"};
などとして、複数割り当てることもできます。

認証処理を終えて、失敗した場合(ユーザー名にnullが入っていた場合)は再度「WWW-Authenticate」ヘッダを付加して応答をクリアし、 401のステータス(=認証が必要)を返します。成功した場合は「UserInfo」をセッション変数に保存し、chain.doFilter で要求された処理をそのまま実行します。 これでBASIC認証の処理は完了です。

次にセッション変数に保存する「UserInfo」クラスですが、これは単純にユーザー名、パスワード、ユーザーロールのString配列を持っているだけです。 この3項目に加えて、権限の有無を確認するために「isInRole」というメソッドを持たせています。各ページでの権限の確認は「UserInfo」をセッション変数から取り出し、 ロールの照らし合わせ(「isInRole」メソッドを使用)を行うことになります。
package sample;

public class UserInfo {
    // ユーザー名
    public String userId;

    //パスワード
    public String password;

    // ユーザーが所有する権限のString配列
    public String[] roles;

    // コンストラクタ
    public UserInfo() {
        userId = null;
        password = null;
        roles = null;
    }

    // 引数に指定した権限をこのユーザーが持っているかどうかを確認
    public boolean isInRole(String role) {
        for (int i = 0; i < roles.length; i++) {
            if (roles[i].equals(role)) {
                return true;
            }
        }
        return false;
    }
}

WebアプリケーションにBASIC認証をかけるには、作成したフィルタを通常のフィルタのように「web.xml」に登録するだけです。 例を示します。
<web-app>
    <!-- Filter Configuration -->
    <filter>
        <filter-name>basicAuthFilter</filter-name>
        <filter-class>sample.BasicAuthFilter</filter-class>
    </filter>

    <!-- Filter Mapping -->
    <filter-mapping>
        <filter-name>basicAuthFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
...
この例ではすべてのURL(/*)にBASIC認証をかけます。


以上のようにしてコンテナの機能を使わずにBASIC認証は実装できます。上のソースはそのままを使えるのでコピー&ペーストで簡単に使えます。