第19回 フォーム認証を実装したASP.NETアプリケーション:連載 プログラミングASP.NET ―ASP.NETによるWebアプリケーション実践開発講座― (2/2 ページ)
フォーム認証を利用したWebアプリケーションでは、独自のログインページと認証プロセスを用いて柔軟な資格情報の検証が可能だ。
フォーム認証その2
以上のように、FormsAuthenticationクラスに用意されたヘルパ・メソッドを利用すれば簡単にフォーム認証を利用できるが、ロールベースの認定が利用できないなど柔軟性に欠けるところがある。また、web.configに資格情報を登録しなければならず、セキュリティ上問題になる場合もある。そこで、これらのデメリットを解消したフォーム認証のサンプルであるアプリケーション「formauth2」を次に作成する。
このformauth2アプリケーションでは、FormsAuthenticationクラスのヘルパ・メソッドに頼らず、資格情報を独自のXMLファイルに登録し、これを用いて認証を行う。また、formauthアプリケーションでは利用できなかったロールベースの認定も可能にする。
■カスタム認証
formauth2アプリケーションは、次の6つのファイルから構成されている。default.aspxとadmin.aspxは先のformauthと同じものなので、ここでは残る4つのファイルについて解説する。
- web.config
- login.aspx(ログイン・フォーム)
- default.aspx(formauthアプリケーションと同じ)
- admin.aspx(formauthアプリケーションと同じ)
- global.asax(Application_AuthenticateRequestイベント・ハンドラ)
- users.xml(資格情報を登録したxmlファイル)
formauth2アプリケーションのダウンロード(formauth2.zip)
formauth2アプリケーションのweb.configは、formauthアプリケーションのそれとほとんど同じだが、FormsAuthenticationクラスを利用しないので、次のリスト19.3に示すようにcredentials要素やuser要素は取り除かれている。
<configuration>
<system.web>
<authentication mode="Forms">
<forms name="formauth" loginUrl="login.aspx" />
</forms>
</authentication>
<authorization>
<deny users="?" />
</authorization>
</system.web>
</configuration>
代わりに資格情報は、リスト19.4に示すようなXMLファイル(users.xml)に登録される。このXMLの構造は特に定められたものではなく、筆者が扱いやすいように適当に定義したものだ。各ユーザーに1つのUserInfo要素が対応し、その子要素としてユーザー名とパスワード、それにユーザーが所属するロールを示す、name要素、password要素、role要素が含まれるようになっている。role要素に指定したロールによって認定が行えるところが、FormsAuthenticationクラスを使った認証との大きな違いである。
<AuthInfo>
<Users>
<UserInfo>
<name>user1</name>
<password>hoge</password>
<role>admin</role>
</UserInfo>
<UserInfo>
<name>user2</name>
<password>foo</password>
<role>users</role>
</UserInfo>
</Users>
</AuthInfo>
以上のusers.xmlを読み込み、認証を行っているのがFormsAuthentication.Authenticateメソッドの代替版として定義しているlogin.aspxのAuthenticateメソッドである。このメソッドに関連するコードをlogin.aspxより抜粋して次のリスト19.5に示す。
string usersfile = "users.xml";
public class AuthInfo {
public UserInfo[] Users;
}
public class UserInfo {
public string name;
public string password;
public string role;
}
UserInfo Authenticate(string username, string password) {
AuthInfo authinfo = null;
UserInfo result = null;
string filename = Server.MapPath(".") + "\\" + usersfile;
try {
XmlSerializer serializer = new XmlSerializer(typeof(AuthInfo));
FileStream stream
= new FileStream(filename, FileMode.Open, FileAccess.Read);
authinfo = (AuthInfo) serializer.Deserialize(stream);
stream.Close();
}
catch (Exception ex) {
Message.Text = ex.Message;
}
if (authinfo != null) {
foreach (UserInfo u in authinfo.Users) {
if (u.name == username && u.password == password)
result = u;
}
}
return result;
}
ここで行われている処理はごく単純なもので、XmlSerializerクラスを利用してusers.xmlを読み込み、メソッドのパラメータに指定されたユーザー名(username)とパスワード(password)に一致するエントリが存在するかを確認しているだけである。非常に簡単な処理だが、これだけでも認証は可能だ。もっとも、より実際的には、このサンプルのようにパスワードを平文のまま保存せずに、暗号化処理を追加する必要があるし、できればXMLファイルよりもデータベースを利用すべきだろう。
ところで、XmlSerializerとは、XMLファイルの構造に対応するクラスを宣言し(ここではルート要素に対応するAuthInfoクラスとUserInfoクラス)、Deserializeメソッドを呼び出すだけで、XMLファイルの構造を保ったまま適切なオブジェクトに読み込んでくれる大変便利な仕組みである。いずれ川俣氏の「連載:.NETで簡単XML」で丁寧に解説されると思われるので、ここでは詳しい解説は割愛する。
■フォーム認証におけるロールベースの認定
Windows認証を利用したときは、特別な処理を行わなくても、web.configのauthorization要素を使ったり、Page.UserプロパティからIsInRoleメソッドを呼び出したりして、ロールベースの認定を行うことができた。これは、ユーザーが認証されると、そのユーザーが所属しているグループがそのままロールとして参照される仕組みが提供されていたからだ。一方フォーム認証では、認証プロセスがアプリケーションに任されているため、このような便利な仕組みは用意されていない。ロールベースの認定を利用したければ、それなりの処理を実装しなければならないのである。
フォーム認証でロールベースの認定を利用するには、Page.Userプロパティ(またはContext.User)に格納されているIPrincipalオブジェクトを適切にセットアップしなければならない。前回Windows認証のwinformアプリケーションで利用したように、IPrincipal.IsInRoleメソッドを呼び出したり、authorization要素でロールベースの認定を行ったりすると、このIPrincipalオブジェクトが参照されるのである。従って、このオブジェクトの作成時に、ユーザーとロールの関係を指定してやれば、フォーム認証でもWindows認証と同じようにロールベースの認定が利用できるようになる。
それでは、どこでIPrincipalオブジェクトのセットアップを行えばよいのだろうか。IsInRoleメソッドを利用するだけならば、その手前でオブジェクトを作成すればよいのだが、web.configのauthorization要素を利用するとなるとそうはいかない。authorization要素による認定は、ページのコードが実行される前に、暗黙的に行われるからだ。そこでこの処理はアプリケーション・レベルのイベント・ハンドラとして実装する必要がある。具体的には、ファイルglobal.asaxにApplication_AuthenticateRequestの名前でメソッドを宣言し、ここに処理を記述すればよい。このメソッドはHttpApplicationクラスのAuthenticateRequestイベントのハンドラとして機能し、ASP.NETのセキュリティ・モジュールによってユーザーが認証されると呼び出されるようになる。なお、このメソッドはFormsAuthentication_OnAuthenticateの名前で宣言することもできる。こちらはFormsAuthenticationクラスのAuthenticateイベントのハンドラとして機能するもので、フォーム認証専用に用いられるものである。
ここでIPrincipalオブジェクトを作成すればよいのだが、そのためには認証されたユーザーが所属するロールを調べる手段が必要になる。その方法はいろいろと考えられるが、ここでは認証チケットにその情報を格納することにする。認証チケットは認証時に作成され、以後セッションが終了するまでクッキーとして保存されている。このため必要なときにはいつでも参照でき、この種の用途には適している。
■ロールベースによる認定処理の実装
さて、それでは実装について解説していこう。まずはロール情報を含めたカスタム認証チケットを作成する過程から見ていくことにする。formauthアプリケーションでは、FormsAuthentication.RedirectFromLoginPageメソッドを利用していたので、認証チケットは自動的に作成されていたが、今回はロール情報を含めるために同等の処理を自前で実装しなければならない。その処理を行っているのが、login.aspxで宣言されているRedirectFromLoginPageメソッドである(リスト19.6)。
void RedirectFromLoginPage(string username, string role, bool persistent) {
// 認証チケットの作成
// web.configの<forms timeout="..." />は参照しない
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1, // バージョン番号
username, // 認証されたユーザーの名前
DateTime.Now, // チケットが発行された日時
// チケットの有効期限
// (60分を指定。forms要素のtimeoutは参照していない)
DateTime.Now.AddMinutes(60),
// trueならば認証チケットは永続(有効期限が無視される)
persistent,
// ユーザー定義データ(ロールの格納に利用)
role);
// チケットをクッキーに。クッキー名は<forms name="..." />を参照
// <forms protection="..." />は参照せず無条件で暗号化
HttpCookie cookie = new HttpCookie(
FormsAuthentication.FormsCookieName,
FormsAuthentication.Encrypt(ticket));
// 永続化が指定されたら、クッキーの有効期限を50年に設定
if (persistent) {
cookie.Expires = DateTime.Now.AddYears(50);
}
Response.Cookies.Add(cookie);
// ログイン・フォームへリダイレクトされる前に
// リクエストされていたページへ
Response.Redirect(FormsAuthentication.GetRedirectUrl(username, persistent));
}
ここでは認証チケットの実体であるFormsAuthenticationTicketオブジェクトを作成し、これをクッキーとして登録する処理を行っている。なお、FormsAuthenticationクラスのメソッドを利用するときは、web.configのforms要素に指定されたパラメータが参照されて認証チケットが作成されるが、ここではFormsAuthenticationクラスにてアクセス用のメソッドが用意されている値(クッキー名とリダイレクトURL)だけは参照し、それ以外の値(チケットの有効期限(timeout)と保護(protection))には固定値を指定している。
さて、このRedirectFromLoginPageメソッドの第2パラメータにロールを文字列として渡せば、ユーザー定義データを格納するために用意されているUserDataプロパティを利用して、認証チケットにロール情報が含まれるようになる。そこに指定されるロール情報は、前節で解説したAuthenticateメソッドによってusers.xmlから読み出されたもので、[ログイン]ボタンのClickイベント・ハンドラで以下のように受け渡される。
void login_Click(object sender, EventArgs e) {
UserInfo userinfo;
// 認証に成功すると、認証されたユーザーの情報がuserinfoに格納される
if ((userinfo = Authenticate(tbUsername.Text, tbPassword.Text))
!= null) {
RedirectFromLoginPage(
userinfo.name, userinfo.role, persist.Checked);
} else {
Message.Text = "失敗しました。パスワードを確認してください";
}
}
こうして認証チケットに格納されたロール情報は、リスト19.7に示すApplication_AuthenticateRequestメソッドで参照され、IPrincipalオブジェクトの作成に利用される。フォーム認証の場合、IPrincipalオブジェクトの実体はGeneralPrincipalクラスのオブジェクトとして作成され、これはFormsIdentityオブジェクト(資格情報を格納する)とロール情報から構成される。1つの資格情報には複数のロール情報を対応付けできるため、ロール情報は文字列の配列(string[])として指定できる。ただ、formauth2アプリケーションでは処理を単純にするため、ロール情報を単一のstringオブジェクトとして扱っている。
<%@ Application %>
<%@ Import Namespace="System.Web.Security" %>
<%@ Import Namespace="System.Security.Principal" %>
<script runat="server" language="C#">
void Application_AuthenticateRequest(object sender, EventArgs e) {
// 認証チケットをクッキーから取り出す
HttpCookie cookie = Context.Request.Cookies[
FormsAuthentication.FormsCookieName];
if (cookie == null) return;
// 復号化する
FormsAuthenticationTicket ticket = null;
try {
ticket = FormsAuthentication.Decrypt(cookie.Value);
}
catch (Exception ex) {
}
if (ticket == null) return;
// UserDataプロパティに格納されているロール情報を参照する
string role = ticket.UserData;
// フォーム認証ではIPrincipalオブジェクトにGenericPrincipalクラス
// が利用される。このクラスは、資格情報を表すFormsIdentityクラスと
// ロール情報(string[]オブジェクト)から構成される
FormsIdentity identity = new FormsIdentity(ticket);
GenericPrincipal principal = new GenericPrincipal(
identity, new string[] { role });
// FormsIdentityオブジェクトをContext.Userに代入すると
// Page.Userから参照可能になる
Context.User = principal;
}
</script>
こうしてContext.Userにロール情報を含む資格情報がセットアップされると、ロールベースの認定が利用できるようになる。例えば、次に示すようにlocation要素以下に認定の設定を行えば、admin.aspxにはロールadminに所属するユーザーだけがアクセスできるようになる。
<configuration>
<system.web>
<authentication mode="Forms">
<forms name="formauth" loginUrl="login.aspx" />
</authentication>
<authorization>
<deny users="?" />
</authorization>
</system.web>
<location path="admin.aspx">
<system.web>
<authorization>
<allow roles="admin" />
<deny users="*" />
</authorization>
</system.web>
</location>
</configuration>
さて、長らくご愛読いただいた本連載だが、今回でひとまず最終回とさせていただく。ASP.NETのフレームワークは感心するほど巨大で、解説すべき事項は尽きることはなさそうだが、基本的な機能についてはおおむね網羅できたと思う。より実践的なプログラミング講座でお会いできることを期待しつつ、最後までお付き合いいただいた読者の方々に感謝したい。
Copyright© Digital Advantage Corp. All Rights Reserved.