|
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 {
HttpServletRequest hreq = (HttpServletRequest)req;
HttpServletResponse hres = (HttpServletResponse)res;
HttpSession session = hreq.getSession();
if (session.getAttribute("USER_INFO") == null) {
String auth = hreq.getHeader("Authorization");
if (auth == null) {
requireAuth(hres);
return;
} else {
try {
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("")) {
requireAuth(hres);
return;
} else {
session.setAttribute("USER_INFO", user);
}
} catch(Exception ex) {
requireAuth(hres);
return;
}
}
}
chain.doFilter(req, res);
}
private void requireAuth(HttpServletResponse hres) throws IOException {
hres.setHeader("WWW-Authenticate", "BASIC realm=\"Authentication Test\"");
hres.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
private String decodeAuthHeader(String header) {
String ret = "";
try {
String encStr = header.substring(6);
sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
byte[] dec = decoder.decodeBuffer(encStr);
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;
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>
<filter-name>basicAuthFilter</filter-name>
<filter-class>sample.BasicAuthFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>basicAuthFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
|
|
この例ではすべてのURL(/*)にBASIC認証をかけます。
以上のようにしてコンテナの機能を使わずにBASIC認証は実装できます。上のソースはそのままを使えるのでコピー&ペーストで簡単に使えます。
|
|
|
|