前回の投稿の続きになります。
ウェブアプリを作っているとユーザ情報を登録させて、ページアクセスを制限したいことが良くあると思います。今回はPlayFrameworkを利用して認証機能を実装してみました。ソースはGitHubにあります。
実現する機能
- ユーザ情報の登録、照合による認証(前回の投稿)
- ホーム画面へのアクセス制限
- 認証中ユーザと同ユーザで別ブラウザからサインインした場合の認証権限移行
前回の投稿で、ユーザ情報をDBに保存しサインインするまでのことを書きました。サインインした後はホーム画面に遷移するのですが、ホーム画面にはサインインしたユーザ(ユーザ情報認証済のブラウザ)にだけアクセスさせたいことがあります。この投稿では、ブラウザのセッションを見て現在認証中か否かを判断して画面遷移をさせたいと思います。
ざっくりこんな感じで遷移させたいです。Playではセッション(session)をブラウザのクッキーで保持する情報、キャッシュ(cache)をサーバのメモリに保持する情報としています。
UUID発行
アクセス制限をするにはブラウザを識別する必要があります。そのためにUUIDを発行してセッションに持たせたいと思います。これにはplay.http.ActionCreatorインターフェースを実装します。そして実装したクラス名をapplication.confに定義してください。
package common.global;
// import文省略
public class AppActionCreator implements ActionCreator {
/** UUID セッションキー */
public static final String UUID = "UUID";
@Override
public Action<?> createAction(Request arg0, Method arg1) {
return new Action.Simple() {
@Override
public CompletionStage<Result> call(Http.Context ctx) {
/*
* セッションにUUIDが存在しない場合、
* UUIDを生成しセッションに設定する。
*/
if(ctx.session().get(UUID) == null){
String uuid = java.util.UUID.randomUUID().toString();
ctx.session().put(UUID, uuid);
}
return delegate.call(ctx);
}
};
}
}
play.http {
actionCreator = "common.global.AppActionCreator"
}
このインターフェースを実装すると、リクエストを受信してroutesに入る前にこいつを経由してくれます。セッションにUUIDが無ければUUIDを発行しセッションに設定します。
UUIDの生成にはjava.util.UUIDを使っていますが、これで生成するUUIDは完全にユニークなものだという保証はないそうです(ユニークでない可能性は限りなく低いが)。ここではUUID発行方法に適切かどうかの議論はしませんが、実装される際は適切なUUID発行方法を検討してください。
認証ユーザ情報の保持
ブラウザを識別することができたら、そのブラウザがどのユーザ情報で認証を受けたのかを知っておく必要があります。それにはサインインで認証したユーザ情報とそのリクエストを投げたブラウザのUUIDをサーバのキャッシュに保存すれば良いですね。それをSigninController.javaのPOST処理の中のsetCacheUser(user)で行なっています。
package controllers;
// import文省略
@Singleton
public class SigninController extends AppController {
//キャッシュ保存関係以外のメンバー省略
/** ユーザ情報キー */
public static final String USER_KEY = "USER";
/**
* キャッシュにユーザ情報保存
* @param user ユーザ情報
*/
public void setCacheUser(User user){
setCache(USER_KEY, user);
this.cache.set((USER_KEY + user.id), getClientId(), savetime);
}
/**
* キャッシュからユーザ情報取得
* @return ユーザ情報
*/
public User getCacheUser(){
Object objectUser = getCache(USER_KEY);
if(objectUser == null) return null;
/*
* ユーザ情報を保存したプラウザのセッションUUIDと
* 現在アクセスしているセッションのUUIDを比較し、
* 異なる場合、ユーザ情報を取得させない。
*/
User user = User.class.cast(objectUser);
String uuid = this.cache.get(USER_KEY + user.id).toString();
if(!uuid.equals(getClientId())) return null;
return user;
}
/**
* キャッシュのユーザ情報消去
*/
public void clearCacheUser(){
clearCache(USER_KEY);
}
}
ユーザ情報保存時に保存したユーザのIDをキーにセッションのUUIDを保存し、ユーザ情報取得時に取得したユーザのIDよりキャッシュからUUIDを取得しセッションのUUIDと比較し、異なるUUIDであればユーザ情報を取得させません。これにより複数のブラウザが同時に一人のユーザ情報で認証を得ることを防いでいます。
制限条件
ブラウザの識別とユーザ情報の保持をすることができたら、あとは制限をかけたいページにアクセスしたときに認証確認をするだけです。Playではplay.mvc.Securityパッケージを使用してそれを実現することができます。まずAuthenticatorを継承したクラスを作成し、二つのメソッドを実装します。
package common.secure;
// import文省略
public class AppAuthenticator extends Authenticator {
/** キャッシュ */
private CacheApi cache;
@Inject
public AppAuthenticator(CacheApi cache){
this.cache = cache;
}
@Override
public String getUsername(Context ctx) {
/*
* キャッシュからユーザ情報を取得する。
* ユーザ情報が存在すれば認証中としアクセスを許可する。
*/
SigninController signinController = new SigninController(cache);
User user = signinController.getCacheUser();
if(user != null){
signinController.setCacheUser(user);
return user.toString();
}else{
return null;
}
}
@Override
public Result onUnauthorized(Context ctx) {
/*
* アクセスが許可されなかった場合、
* サインイン画面にリダイレクトする。
*/
return redirect(routes.SigninController.get());
}
}
getUsername()で何かしらの文字列を返却すればアクセス許可を得たことになります。nullを返却した場合、onUnauthorized()が呼ばれることになっています。後は、この制限をかけたいホーム画面コントローラーにAuthenticatedのannotationを付与するだけです。
package controllers;
// import文省略
@Singleton
public class IndexController extends AppController {
@Inject
public IndexController(CacheApi cache) {
super(cache);
}
@Authenticated(AppAuthenticator.class) // <-これ
@Override
public Result get() {
/*
* ユーザ情報でホーム画面を作成し返却する。
*/
User user = new SigninController(cache).getCacheUser();
return ok(views.html.index.render(user));
}
@Authenticated(AppAuthenticator.class) // <-これ
@Override
public Result post() {
/*
* キャッシュからユーザ情報を消去し、
* サインイン画面にリダイレクトする。
*/
new SigninController(cache).clearCacheUser();
return redirect(routes.SigninController.get());
}
}
制限をかけたい処理にannotationを付与し、認証確認を実装したクラスを指定するだけでアクセス制限をかけてくれます。
最後に
ユーザの認証方法は他にもたくさんあるかと思いますが、今回は一番簡単でシンプルなサインイン、サインアップを実装してみました。何かの参考になればと思います。
GitHub