Cutting Edge
ASP.NET Identity でのユーザー データの保存
Visual Studio 2013 の ASP.NET Identity とは、ユーザー データを管理し、効果的なメンバーシップ システムを確立するという手間がかかっても不可欠な作業を簡略化する方法です。以前、ASP.NET Identity API の概要について説明し (msdn.microsoft.com/ja-jp/magazine/dn605872)、ソーシャル ネットワークと OAuth プロトコルとのかかわりを調査しました (msdn.microsoft.com/ja-jp/magazine/dn745860)。今回は、ユーザー データの表現や基盤となるデータ ストアなど、ASP.NET Identity のさまざまな拡張ポイントについて説明します。
下準備
まず、Visual Studio 2013 で新しい空の ASP.NET MVC プロジェクトを作成します。ウィザードが示す既定値をすべてそのまま使用してかまいませんが、単一ユーザー認証モデルを選択するようにします。スキャフォールディングされたコードでは、"aspnet-[ProjectName]-[RandomNumber]" という命名規則に従って自動生成された名前の付いたローカルの SQL Server ファイルにユーザー データが格納されます。このコードは、読み取りおよび書き込み時に Entity Framework を使用して、このユーザー データベースにアクセスします。ユーザー データの表現は、ApplicationUser クラスにあります。
public class ApplicationUser : IdentityUser
{
}
ApplicationUser クラスは、システム提供の IdentityUser クラスから継承されています。このユーザー表現をカスタマイズするために、まず、このクラスに新しいメンバーを追加します。
public class ApplicationUser : IdentityUser
{
public String MagicCode { get; set; }
}
クラスの名前 (ApplicationUser) は決まったものではなく、自由に変更できます。今回は、2 つの可能性を示すために意図的にちょっと変わった "マジック コード (MagicCode)" フィールドにしました。生年月日、社会保障番号といった登録時に指定する情報など、UI を必要とするフィールドを追加します。また、必要に応じてフィールドを追加して、実際にユーザー レコードが作成されるときに自動的に計算することもできます (たとえば、アプリ固有の "マジック" コード)。ApplicationUser クラスには、図 1 の一覧に示すメンバーが既定で含まれています。
図 1 IdentityUser 基本クラスで定義されているメンバー
メンバー | 説明 |
Id | 自動生成されるテーブルの一意 ID (GUID)。このフィールドは主キーになります。 |
UserName | ユーザーの表示名。 |
PasswordHash | 指定されたパスワードから求められたハッシュ。 |
SecurityStamp | UserManager オブジェクトの有効期間内の特定の時点で自動的に作成される GUID。通常、これは、パスワードの変更時やソーシャル ログインが追加/削除時に作成および更新されます。セキュリティ スタンプは、一般に、ユーザー情報のスナップショットを取り、変更されていなければ自動的にログインできるようにします。 |
Discriminator | この列は、Entity Framework の保存モデル固有で、特定の行が属するクラスを決定します。IdentityUser をルートとする階層では、クラスごとに一意に識別できる値を保持します。 |
図 1 の一覧に示されているフィールドだけでなく、ApplicationUser クラスの定義にプログラムで追加したその他のフィールドも、データベース テーブルに格納されることになります。既定のテーブル名は AspNetUsers です。IdentityUser クラスは、Logins、Claims、Roles など、多くのプロパティも公開します。このようなプロパティは AspNetUsers テーブルには格納されませんが、同じデータベース内の他のサイド テーブル (AspNetUserRoles、AspNetUserLogins、および AspNetUserClaims) に格納されます (図 2 参照)。
図 2 ASP.NET Identity ユーザー データベースの既定の構造
ApplicationUser クラスを変更しても、追加のフィールドがすぐに UI に反映され、データベースに保存されるわけではありません。ただし、データベースの更新にはそれほど手間はかかりません。
スキャフォールディングされたコードの変更
新しいフィールドを反映するように、アプリケーションのビューとモデルを編集する必要があります。新しいユーザーをサイトに登録する場所になるアプリケーション フォームでは、マジック コードと他の追加のフィールドの UI を表示するマークアップをいくつか追加します。図 3 は、登録フォームをサポートする CSHTML Razor ファイルの変更後のコードを示しています。
図 3 追加フィールドをユーザーに表示する Razor ファイル
@using (Html.BeginForm("Register", "Account",
FormMethod.Post, new
{ @class = "form-horizontal", role = "form" }))
{
@Html.AntiForgeryToken()
<h4>Create a new account.</h4>
<hr />
@Html.ValidationSummary()
<div class="form-group">
@Html.LabelFor(m => m.MagicCode,
new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.MagicCode,
new { @class = "form-control" })
</div>
</div>
...
}
CSHTML 登録フォームは、RegisterViewModel と表記されるビュー モデル クラスに基づきます。図 4 は、ほとんどの ASP.NET MVC アプリケーションのデータ注釈に基づく従来の検証メカニズムに RegisterViewModel クラスをプラグインするのに必要な変更を示しています。
図 4 RegisterViewModel クラスの変更
public class RegisterViewModel
{
[Required]
[Display(Name = "User name")]
public string UserName { get; set; }
[Required]
[StringLength(100, ErrorMessage =
"The {0} must be at least {1} character long.",
MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage =
"Password and confirmation do not match.")]
public string ConfirmPassword { get; set; }
[Required]
[Display(Name = "Internal magic code")]
public string MagicCode { get; set; }
}
ただし、変更点はこれだけではありません。もう 1 つ最も重要な手順があります。追加したデータを、永続ストアにデータを格納する下位レイヤーに渡す必要があります。そのためには、登録フォームから POST アクションを処理するコントローラー メソッドを変更します。
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid) {
var user = new ApplicationUser() { UserName = model.UserName,
MagicCode = model.MagicCode };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded) {
await SignInAsync(user, isPersistent: false);
return RedirectToAction("Index", "Home");
}
}
主な変更点は、ポストされたマジック コードのデータ (ユーザー定義に追加するすべてデータ) を保存する部分です。これを、UserManager オブジェクトの CreateAsync メソッドに渡される ApplicationUser インスタンスに保存します。
永続ストアについて
ASP.NET MVC テンプレートによって生成されたサンプル コードでは、AccountController クラスに、以下のように定義されたメンバーがあります。
public UserManager<ApplicationUser> UserManager { get; private set; }
ここでは、ユーザー ストア オブジェクトを渡して UserManager クラスのインスタンスが作成されています。ASP.NET Identity では、既定のユーザー ストアが用意されます。
var defaultUserStore = new UserStore<ApplicationUser>(new ApplicationDbContext())
ApplicationUser とほぼ同様に、ApplicationDbContext クラスは、システム定義クラス (IdentityDbContext) から継承され、実際の保存作業を実行するように Entity Framework をラップします。ただし、この既定のストレージ メカニズムをまったく使用しないで、独自に作成することも可能です。SQL Server と Entity Framework のカスタム ストレージ エンジンをベースに、スキーマだけを変えてもかまいません。また、MySQL ソリューションや NoSQL ソリューションなど、まったく異なるストレージ エンジンを利用することもできます。今回は、埋め込み型の RavenDB (ravendb.net、英語) に基づくユーザー ストアを調整する方法を見ていくことにします。必要となるクラスのプロトタイプは以下のとおりです。
public class RavenDbUserStore<TUser> :
IUserStore<TUser>, IUserPasswordStore<TUser>
where TUser : TypicalUser
{
...
}
ログイン、ロール、および要求をサポートする場合は、さらに多くのインターフェイスの実装が必要になります。最低限の機能を備えたソリューションとしては、IUserStore と IUserPasswordStore があれば十分です。TypicalUser クラスは、ASP.NET Identity インフラストラクチャからできるだけ分離した状態を維持するために作成したカスタム クラスです。
public class TypicalUser : IUser
{
// IUser interface
public String Id { get; set; }
public String UserName { get; set; }
// Other members
public String Password { get; set; }
public String MagicCode { get; set; }
}
最低限、ユーザー クラスは IUser インターフェイスを実装する必要があります。このインターフェイスには、Id と UserName という 2 つのメンバーを含めます。また、Password というメンバーを追加することも考えられます。これが RavenDB アーカイブに保存されるユーザー クラスです。
RavenDB.Embedded NuGet パッケージを使用して、RavenDB のサポートをプロジェクトに追加します。global.asax では、以下のコードを使用してデータベースを初期化することも必要です。
private static IDocumentStore _instance;
public static IDocumentStore Initialize()
{
_instance = new EmbeddableDocumentStore
{ ConnectionStringName = "RavenDB" };
_instance.Initialize();
return _instance;
}
接続文字列は、データベースの作成先となるパスを指します。ASP.NET Web アプリケーションでは、当然、App_Data のサブフォルダーが適しています。
<add name="RavenDB" connectionString="DataDir = ~\App_Data\Ravendb" />
ユーザー ストア クラスには、IUserStore インターフェイスと IUserPasswordStore インターフェイスのメソッドのコードが含まれています。アプリケーションでは、これらのコードを使用してユーザーとその関連パスワードを管理します。図 5 は、ストアの実装を示しています。
図 5 最低限の機能を備えた RavenDB に基づくユーザー ストア
public class RavenDbUserStore<TUser> :
IUserStore<TUser>, IUserPasswordStore<TUser>
where TUser : TypicalUser
{
private IDocumentSession DocumentSession { get; set; }
public RavenDbUserStore()
{
DocumentSession = RavenDbConfig.Instance.OpenAsyncSession();
}
public Task CreateAsync(TUser user)
{
if (user == null)
throw new ArgumentNullException();
DocumentSession.Store(user);
return Task.FromResult<Object>(null);
}
public Task<TUser> FindByIdAsync(String id)
{
if (String.IsNullOrEmpty(id))
throw new ArgumentException();
var user = DocumentSession.Load<TUser>(id);
return Task.FromResult<TUser>(user);
}
public Task<TUser> FindByNameAsync(String userName)
{
if (string.IsNullOrEmpty(userName))
throw new ArgumentException("Missing user name");
var user = DocumentSession.Query<TUser>()
.FirstOrDefault(u => u.UserName == userName);
return Task.FromResult<TUser>(user);
}
public Task UpdateAsync(TUser user)
{
if (user != null)
DocumentSession.Store(user);
return Task.FromResult<Object>(null);
}
public Task DeleteAsync(TUser user)
{
if (user != null)
DocumentSession.Delete(user);
return Task.FromResult<Object>(null);
}
public void Dispose()
{
if (DocumentSession == null)
return;
DocumentSession.SaveChanges();
DocumentSession.Dispose();
}
public Task SetPasswordHashAsync(TUser user, String passwordHash)
{
user.Password = passwordHash;
return Task.FromResult<Object>(null);
}
public Task<String> GetPasswordHashAsync(TUser user)
{
var passwordHash = user.Password;
return Task.FromResult<string>(passwordHash);
}
public Task<Boolean> HasPasswordAsync(TUser user)
{
var hasPassword = String.IsNullOrEmpty(user.Password);
return Task.FromResult<Boolean>(hasPassword);
}
}
}
RavenDB との対話では、ドキュメント ストア セッションを開いてから閉じるまでの処理を行います。RavenDbUserStore のコンストラクターでは、セッションを開き、ストア オブジェクトの Dispose メソッドでそのセッションを破棄します。ただし、セッションを破棄する前に、SaveChanges メソッドを呼び出し、Unit of Work パターンに従って保留中の変更すべてを保存します。
public void Dispose()
{
if (DocumentSession == null)
return;
DocumentSession.SaveChanges();
DocumentSession.Dispose();
}
RavenDB データベースを操作する API は非常に単純です。新しいユーザーの作成に必要なコードを以下に示します。
public Task CreateAsync(TUser user)
{
if (user == null)
throw new ArgumentNullException();
DocumentSession.Store(user);
return Task.FromResult<Object>(null);
}
特定のユーザーを取得するには、DocumentSession オブジェクトの Query メソッドを使用します。
var user = DocumentSession.Load<TUser>(id);
RavenDB では、保存するクラスには Id プロパティがあることを想定しています。このプロパティがなければ暗黙のうちに作成されます。そのため、常に Load メソッドを使用し、Id を指定して、どのオブジェクトでも取得できます。独自の ID でも、システム生成の ID でも問題はありません。名前を指定してユーザーを取得する場合は、LINQ 構文を使用する従来のクエリを実行します。
var user = DocumentSession
.Query<TUser>()
.FirstOrDefault(u => u.UserName == userName);
さまざまなオブジェクトを選択して管理可能なリストに保存するには、ToList を使用します。パスワードの処理も同様に簡単です。RavenDB では、パスワードがハッシュ形式で格納されますが、ハッシュは RavenDB モジュール外部で管理されます。実際には、SetPasswordHashAsync メソッドが、ユーザーが指定したパスワードのハッシュを既に受け取っています。
図 5 は、ASP.NET Identity と互換性のある RavenDB ユーザー ストアをセットアップするソース コード全体を示しています。埋め込み型の RavenDB を既にインストールしている場合は、ユーザーがログインおよびログアウトするだけです。外部ログインやアカウント管理などの高度な機能に対応する場合は、ASP.NET Identity のユーザー関連のインターフェイスをすべて実装するか、スキャフォールディングから取得した AccountController のコードを大幅に手直しする必要があります。
まとめ
ASP.NET Identity は、Entity Framework ベースのストレージ インフラストラクチャをまったく利用しないで、スキーマのないドキュメント データベースである RavenDB を使用できるようにします。RavenDB は、Windows サービスや IIS アプリケーションとして、または今回説明したように埋め込み型としてインストールできます。ASP.NET Identity のインターフェイスをいくつか実装するクラスを作成し、この新しいストア クラスを UserManager インフラストラクチャに挿入するだけです。ユーザーの型スキーマの変更は、Entity Framework に基く既定の構成でも簡単です。ただし、RavenDB を使用する場合は、ユーザー形式の変更が必要な移行の問題を解決することになります。
Dino Espositoは、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2014 年) および『Programming ASP.NET MVC 5』(Microsoft Press、2014) の共著者です。JetBrains の Microsoft .NET Framework および Android プラットフォームのテクニカル エバンジェリストでもあります。世界各国で開催される業界のイベントで頻繁に講演しており、software2cents.wordpress.com (英語) や Twitter (twitter.com/despos、英語) でソフトウェアに関するビジョンを紹介しています。
この記事のレビューに協力してくれた技術スタッフの Mauro Servienti に心より感謝いたします。