前回 は、個人的に気になる話題である国際化の実現について、いろいろな html コンポーネントの使い方に先立って書きました。
今回も、またもや通常の流れから離れて、いきなりユーザ認証/認可について書きたいと思います。何故なら、この辺がうまく解決できないとやっぱり業務で使うことができないから…もちろん Wicket でその辺が出来ないなんてことは無いですので、早いうちに実現方法について学んでおくと良い、と思っています。
JDK5 以上を対象にする場合、wicket-auth-roles という拡張パッケージを使って認証/認可の仕組みを実現できそうなのですが、今回はそれを使わずにほとんどの部分を自分でコーディングする単純な方法(書く量は多い)の方について書きます。多分、wicket-auth-rols の中身を書いていく様な感じになるのだと思います。
先に進む前に、まず認証(Authentication) と認可(Authorization) の違いについて、明記しておきます。
認証しようとしている利用者が本人であることを確認すること
認証済の利用者に対して、利用者に応じたリソースへのアクセス権を与えること。または、アクセス権があるかどうかを確認すること
となります。具体的には、ID とパスワードで正しいユーザであることを確認するのが認証で、一般ユーザではアクセスできず管理者ユーザでのみアクセスできる、という様な仕組みが認可となります。
Wicket には org.apache.wicket.authorization.IAuthorizationStrategy というインタフェースがあり、以下の 2 つのメソッドが定義されています。
| メソッド | 説明 |
|---|---|
| boolean isActionAuthorized(Component component, Action action) | 指定 Component に対して、指定 Action を許可するかどうかを判定する。action には Wicket 1.3.4 では Component.ENABLE と Component.RENDER の 2 つが定義されている。Component.ENABLE は component 有効化を許可するかどうか決めるための Action で、Component.RENDER は component およびその子 component の描画を許可するかどうか決めるための Action である |
| boolean isInstantiationAuthorized(Class componentClass) | 指定クラスのインスタンス化を許可するかどうかを判定する。例えばある WebPage クラスに対して false を返すと、その WebPage を閲覧することができなくなる |
現在のログイン状態に応じて処理を行う様に上記メソッドを実装した AuthorizationStrategy 実装クラスを、WebApplication#getSecuritySettings()#setAuthorizationStrategy() を用いて設定しておくと、Wicket がページやコンポーネントを表示しようとする度に、自動的にチェックが実行されます。これにより
といった機能を実現できます。
なお、
辺りの基本処理については、独自に実装しないといけません。
今回作るサンプルアプリケーションの構造は、↓のクラス図の様になります(クリックすると拡大します)。

アプリケーションサーバに配置後のファイル構成は↓の通り。
以下の順序で説明します。
package wicket;
import org.apache.wicket.Request;
import org.apache.wicket.protocol.http.WebSession;
/**
* ユーザ情報を格納するための、カスタムセッションクラス。
* サンプル用に、ハードコーディングしたユーザ名で認証を行う。
* また、簡易的にロールベースセキュリティを実現するために
* isUser()/isAdmin() を用意(本来はロールを使うべき)。
*/
public final class LoginSession extends WebSession
{
private static final String USER = "riorio";
private static final String ADMIN = "admin";
private String userName;
public LoginSession(Request request)
{
super(request);
}
public final boolean authenticate(final String userName, final String password)
{
if (this.userName == null) {
if ((USER.equals(userName) && USER.equals(password)) ||
(ADMIN.equals(userName) && ADMIN.equals(password))) {
this.userName = userName;
}
}
return (this.userName != null);
}
public final boolean isLoggedIn()
{
return (userName != null);
}
public final boolean isUser()
{
return USER.equals(userName);
}
public final boolean isAdmin()
{
return ADMIN.equals(userName);
}
}
Web アプリケーションでは、通常はセッションと呼ばれる領域にユーザ毎の一時データを保存します。ユーザの認証情報も同様です。ここでは org.apache.wicket.protocol.http.WebSession を拡張して、認証および認証状態確認用のヘルパーメソッドを実装しています。これは本サンプルで便利に使うための実装で、認証を行うアプリケーションで必須というわけではありません。
2 ユーザのユーザ名を実験用にハードコーディングしていて、authenticate() にて引数のユーザ情報が一致するかどうか判定します。isUser() および isAdmin() は、ユーザ種別に応じてアクセス権限を変えるロールベースのアクセス制御を簡易に行うために用意しました。ちゃんとやるためには、別途 Role クラスを用意する必要があるでしょう。実際 wicket-auth-roles では用意されています。
実際のアプリケーションでは、authenticate() にてデータベースや LDAP サーバ等からユーザ情報/ロール情報を取得することになるでしょう。
package wicket;
import org.apache.wicket.Component;
import org.apache.wicket.Request;
import org.apache.wicket.Response;
import org.apache.wicket.RestartResponseAtInterceptPageException;
import org.apache.wicket.Session;
import org.apache.wicket.authorization.Action;
import org.apache.wicket.authorization.IAuthorizationStrategy;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.markup.html.WebPage;
import wicket.pages.*;
public class LoginApplication extends WebApplication
{
public LoginApplication() {}
@Override
public Class<? extends WebPage> getHomePage()
{
return HomePage.class;
}
@Override
public Session newSession(Request request, Response response)
{
return new LoginSession(request);
}
@Override
protected void init()
{
super.init();
getSecuritySettings().setAuthorizationStrategy(new IAuthorizationStrategy()
{
public boolean isActionAuthorized(Component component, Action action)
{
if (component instanceof AdminLabel && action == Component.RENDER) {
if (((LoginSession)Session.get()).isAdmin()) {
return true;
}
return false;
}
return true;
}
public boolean isInstantiationAuthorized(Class componentClass)
{
if (PrivatePage.class.isAssignableFrom(componentClass)) {
if (((LoginSession)Session.get()).isLoggedIn()) {
return true;
}
throw new RestartResponseAtInterceptPageException(LoginPage.class);
}
return true;
}
});
}
}
本サンプルのアプリケーションクラスです。newSession() をオーバーライドすることで、先ほど説明した LoginSession を本アプリケーションのセッションクラスとして使う様にしています。
init() 内では、getSecuritySettings().setAuthorizationStrategy() を使って、IAuthrizationStrategy の実装を設定しています。これにより、コンポーネントにアクセスする度にカスタム実装した isActionAuthorized() および isInstantiationAuthorized() が実行される様になり、意図通りのセキュリティを実現することができます。
isActionAuthorized() では、アクセス対象のコンポーネント AdminLabel(後述する独自コンポーネント)に対する描画要求(Component.RENDER アクション)の場合、ログインユーザが管理者の場合のみ true を返すことで許可しています。それ以外のユーザ(本サンプルでは1ユーザしかいませんが…)の場合、AdminLabel は表示されません。
isInstantiationAuthorized() では、PrivatePage(後述する、アクセス制限ページが継承する abstract ページ)に対するアクセスの場合、ログイン済みの場合のみ true を返すことで許可しています。そうでない場合、RestartResponseAtInterceptPageException() を投げることで、引数に渡したページ内容をレスポンスとして返しています。ここでは LoginPage(後述)を渡すことでユーザにログインを促します。
package wicket.pages;
import org.apache.wicket.markup.html.basic.Label;
public class AdminLabel extends Label
{
public AdminLabel(final String id, final String label)
{
super(id, label);
}
}
管理者のみ参照できる Label として使うために Label を継承しているだけです。アクセス制御は IAuthorizationStrategy で行うため、本クラスでは何もしていません。
package wicket.login.pages;
import org.apache.wicket.markup.html.WebPage;
public abstract class PrivatePage extends WebPage
{
}
ログインユーザのみが参照できるページを表すために用意したページクラスです。IAuthorizationStrategy にてページ種別を判別するためだけに使います。
<html> <body> <h1>Login Page</h1> <span wicket:id="feedback">エラー表示部</span> <form wicket:id="loginForm"> <table> <tr> <td align="right">Username:</td> <td><input wicket:id="userName" type="text" size="50"/></td> </tr> <tr> <td align="right">Password:</td> <td><input wicket:id="password" type="password" size="50"/></td> </tr> <tr> <td></td> <td> <input type="submit" name="submit" value="Login"/> <input type="reset" value="Reset"/> </td> </tr> </table> </form> </body> </html>
普通のログイン用ページです。
package wicket.pages;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.PasswordTextField;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.util.value.ValueMap;
import wicket.LoginSession;
public class LoginPage extends WebPage
{
public LoginPage()
{
add(new FeedbackPanel("feedback"));
add(new LoginForm("loginForm"));
}
class LoginForm extends Form
{
private final ValueMap props = new ValueMap();
public LoginForm(final String id)
{
super(id);
add(new TextField("userName", new PropertyModel(props, "userName")));
add(new PasswordTextField("password", new PropertyModel(props, "password")));
}
@Override
public void onSubmit()
{
if (((LoginSession)getSession()).authenticate(
props.getString("userName"),
props.getString("password"))) {
if (! continueToOriginalDestination()) {
setResponsePage(getApplication().getHomePage());
}
}
else {
error("Invalid username/password...");
}
}
}
}
org.apache.wicket.markup.html.panel.FeedbackPanel と独自コンポーネントの LoginForm をコンポーネントとして設定しています。
FeedbackPanel は、Wicket アプリケーションで良く使う、エラーメッセージ表示領域です。ページに設定しておくと、ページ上のコンポーネントにて
public final void fatal(String message); public final void error(Serializable message); public final void warn(String message); public final void info(String message); public final void debug(String message);
を使ってメッセージを表示することができます。1.3.2 のオンライン javadoc を見て書いたのですが、何故か error だけ Serializable が引数になっている…?多分誤記だと思うのですが、踏み込んでいません。
LoginForm は TextField および PasswordTextField を持つフォームを表しています。org.apache.wicket.markup.html.form.Form を継承することで、submit 実行時には onSubmit() がコールバックとして実行される様になっています。
TextField および PasswordTextField のモデル(IModel)には、org.apache.wicket.model.PropertyModel を使いました。PropertyModel を使うと、Bean とプロパティを指定してコンポーネントに対するモデルとすることができます(第1引数が Bean 参照で、第2引数がプロパティアクセスのための式表現)。これにより、Java 側で Bean のプロパティを設定すればその値が表示され、ブラウザで値を入力して submit すれば onSubmit() にて Bean のプロパティにアクセスすればその値を取得できます。ここでは Bean として org.apache.wicket.util.value.ValueMap を使いました。
onSubmit() にて、入力されたユーザ名およびパスワードを ValueMap から取得し、先に説明した LoginSession#authenticate() にて認証を実行します。認証に成功した場合、現在のアクセスが本来のアクセス先に対して割り込んだ結果かどうか(本サンプルでは、アクセス制限されたページにアクセスした結果、ログインページに飛ばされたかどうか)を Compoent#continueToOriginalDestination() を使って判定し、false の場合(飛ばされていない)は Component#setResponsePage() にホームページを遷移先として指定しています。true の場合は自動的に本来のアクセス先に遷移します。
Java にて遷移先ページを指定するには、Component#setResponsePage() を使います(他にもあるかもしれませんが、現在の所これを使います)。
public final void setResponsePage(Page page); public final void setResponsePage(Class cls); public final void setResponsePage(Class cls, PageParameters parameters);
ページのインスタンスを渡す1つ目のシグナチャと、その他のクラスを渡すシグナチャに分かれています。これらの違いについては、ここ で詳しく説明されていますので、参照してください。org.apache.wicket.PageParameters は、ページに対してパラメータを渡したい場合に使います。
上記 URL の解説によると、クラス渡しの場合は mountBookmarkablePage() 等で各ページに対する固定 URL を設定する必要がある様です。本サンプルではうっかりしてやっていないのですが、一応動いているみたいです。多分遷移先が Application クラスで指定してあるホームページで、ホームページの場合は "Web アプリケーションroot/" という URL でアクセスできるためではないか?と思います。他のページに遷移する場合は、ちゃんと mountBookmarkablePage() あるいは他の方法で固定 URL を割り当てる必要があると思います。
なお、認証に失敗した場合は、error() を使って FeedbackPanel にエラーメッセージを表示します。setResponsePage() による遷移先指定を行っていないので、同じページに戻ります。
<html> <body> <h1><span wicket:id="welcome">Welcome message here...</span></h1> <p><span wicket:id="admin">Admin message here...</span></p> <wicket:link><a href="LogoutPage.html">Logout...</a></wicket:link> </body> </html>
package wicket.pages;
import org.apache.wicket.markup.html.basic.Label;
public class HomePage extends PrivatePage
{
public HomePage()
{
add(new Label("welcome", "Welcome!"));
add(new AdminLabel("admin", "This is visible only to admin user."));
}
}
Label と AdminLabel をコンポーネントとして持ちます。AdminLabel は、管理者ユーザでログインしないと表示されません。
<html> <body> <h1>Successfully logged out!</h1> <wicket:link><a href="LoginPage.html">Back to login page...</a></wicket:link> </body> </html>
package wicket.pages;
public class LogoutPage extends PrivatePage
{
public LogoutPage()
{
getSession().invalidate();
}
}
使用していたセッションを破棄することで、ユーザ情報を削除します。
<?xml version="1.0" encoding="iso-8859-1"?> <web-app> <display-name>Login Application</display-name> <filter> <filter-name>LoginApplication</filter-name> <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class> <init-param> <param-name>applicationClassName</param-name> <param-value>wicket.LoginApplication</param-value> </init-param> </filter> <filter-mapping> <filter-name>LoginApplication</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
↓Web アプリケーションにアクセスして、最初に表示されるページ

↓ログイン失敗の時に表示されるページ

↓通常のユーザでログインした後のページ

↓管理者ユーザでログインした後のページ

↓ログアウトを実行した直後のページ

FC2 Blog Ranking に参加してます。クリックよろしくお願いします!
<<デミオが戻ってきました | ホーム | Wicket の勉強 (2) 国際化(i18n) について>>
Author:いちのせ りょう
1974年北海道生まれ、育ちは沖縄/宮崎、で現在は東京在住のSEです。社会人10年目、未だにばりばり作ってます!Rio's Laboratory もよろしく…
性別:男性
車:MAZDA DEMIO
好きな音楽:B'z、The Yellow Monkey、Bon Jovi
好きなゲーム:MGS、Bio Hazard、Zeldaとか
やりたい事:F1観に行きたい、沖縄の海に潜りたい、空を飛びたい