SimpleMembershipで認証と認可(BootstrapのNabvarにログインメニューも入れてみる)
ザクです。10月になりましたね。食べ物が美味しいです。
.Net MVC4で認証(Authentication)と認可(Authorization)をやるために、SimpleMembershipをプロジェクトに組込しました。結構面倒くさかったですね。しかし、一度、これをやっておけば認証と認可はSimpleMembershipにお任せできるようになります。
SimpleMembershipの素晴らしさを実感するために、コントローラに簡単な修正を加えましょう。
[Authorize]フィルターで簡単ログイン&アクセスコントロール!
試しに既存のHomeを使って特定のアクションを実行するのに認証が必要なようにしてみます。方法は簡単です。コントローラ内でアクセスするのにログインが必要にしたいアクションの真上に[Authorize]フィルターを追加します。
[Authorize] public ActionResult Contact() { ViewBag.Message = "Your contact page."; return View(); }
Contactの上に付けてみました。ビルドしてトップページからリンクをクリックしてみましょう。ログインフォームに遷移しましたね。先の説明の中で作ったシードデータのユーザーを使ったログインします。
あらあらエラーが出てしまいました。「"WebSecurity.InitializeDatabaseConnection" メソッドを呼び出してから」とあります。そうです、SimpleMembershipが提供している機能(主にWebSecurityクラス)を利用するには、必ず一度WebSecurity.InitializeDatabaseConnectionを実行している必要があるのです。先の説明でプロジェクトから除外したフィルターを覚えているでしょうか?あれがまさしくこれをやるためのものだったのです。あのままフィルターを残しておいても良いのですが、このWebSecurity.InitializeDatabaseConnectionはアプリ起動時に一度実行すれば十分なので、それをやるための別の場所に移動させることにしました。Global.asaxを使います。Application_Startに追加します。
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); AuthConfig.RegisterAuth(); WebSecurity.InitializeDatabaseConnection( ←これを追加 "DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true); }
「using WebMatrix.WebData;」を追加するのも忘れないでください。ビルドして、再びログインします。画面上の変化はありませんが、今度はエラーを出さずにログインができたようです。Contactにも見れるようになりました。
Bootstrapを使ってログインメニューをかっこ良くする
このままでは、あまりにもアレなので、Bootstrapを使ってログインメニューを作ります。まず、レイアウトにナビゲーションバーを追加します。_Layout.cshtmlファイルを開、class="container"となっているdivを以下のnavタグに置き換えてみてください。(Bootstrap3を使っています)
<nav class="navbar navbar-default" role="navigation"> <div class="container-fluid"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">Application Name</a> </div> <div class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li>@Html.ActionLink("Home", "Index", "Home", null, new { @class = "active" })</li> <li>@Html.ActionLink("About", "About", "Home")</li> <li>@Html.ActionLink("Contact", "Contact", "Home")</li> </ul> @Html.Partial("_LoginMenu") </div><!--/.nav-collapse --> </div> </nav>
_LoginMenuのパーシャルを作ります。プロジェクトの「Views/Shared」フォルダを右クリックして、「_LoginMenu.cshtml」を新規作成してください。中身はこんな感じになります。
<ul class="nav navbar-nav navbar-right"> <li><a href="#">Today: @DateTime.Now.ToString("M/d/yyyy")</a></li> @if (Request.IsAuthenticated) { <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown">@User.Identity.Name <b class="caret"></b></a> <ul class="dropdown-menu" role="menu"> <li class="divider"></li> <li><a href="javascript:document.getElementById('logoutForm').submit()">Logout</a></li> </ul> </li> } else { <li>@Html.ActionLink("Login", "Login", "Account")</li> } </ul>
いい感じになりました。
SimpleMembershipをCode Firstに組み込む
ザクです。
MVC4アプリで認証(authentication)、認可(authorization)をしたい時はSimpleMembershipが便利です。何しろMicrosoft製ですし、MVC4アプリのプロジェクトをVS2012で作成したら自動で組み込まれています。ただ、ユーザー関連のモデル(4つあります。DBのテーブルとしては5つです)は、そのままではCode Firstで管理されていません。こまごま調整して組み込んでやる必要があります。
DAL、モデル、コントローラでのコンテキストを調整する
まず、DALにSimpleMembershipのモデル群を登録しましょう。
public SchoolContext() : base("DefaultConnection") { } public DbSet<UserProfile> UserProfiles { get; set; } public DbSet<Membership> Membership { get; set; } public DbSet<Role> Roles { get; set; } public DbSet<OAuthMembership> OAuthMembership { get; set; }
//OnModelCreatingの中に以下のコードを追加 modelBuilder.Entity<Membership>() .HasMany<Role>(r => r.Roles) .WithMany(u => u.Members) .Map(m => { m.ToTable("webpages_UsersInRoles"); m.MapLeftKey("UserId"); m.MapRightKey("RoleId"); });
DALのファイルはSchoolContex.csでしたね。DefaultConnectionの真下に上の4行を追加しましょう。UsersInRolesテーブルも必要ですが、これはリンクテーブルなのでOnModelCreatingの中に定義します。
次にモデルです。SimpleMembershipで使うモデルは、MVC4アプリを作ったらModelsフォルダの中にAccountModels.csとして作られているはずです。UserProfilesだけがありますね。他の3つのモデルを定義してやります。
//UserProfilesの下 [Table("webpages_Membership")] public class Membership { public Membership() { Roles = new List<Role>(); OAuthMemberships = new List<OAuthMembership>(); } [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] public int UserId { get; set; } public DateTime? CreateDate { get; set; } [StringLength(128)] public string ConfirmationToken { get; set; } public bool? IsConfirmed { get; set; } public DateTime? LastPasswordFailureDate { get; set; } public int PasswordFailuresSinceLastSuccess { get; set; } [Required, StringLength(128)] public string Password { get; set; } public DateTime? PasswordChangedDate { get; set; } [Required, StringLength(128)] public string PasswordSalt { get; set; } [StringLength(128)] public string PasswordVerificationToken { get; set; } public DateTime? PasswordVerificationTokenExpirationDate { get; set; } public ICollection<Role> Roles { get; set; } [ForeignKey("UserId")] public ICollection<OAuthMembership> OAuthMemberships { get; set; } } [Table("webpages_OAuthMembership")] public class OAuthMembership { [Key, Column(Order = 0), StringLength(30)] public string Provider { get; set; } [Key, Column(Order = 1), StringLength(100)] public string ProviderUserId { get; set; } public int UserId { get; set; } [Column("UserId"), InverseProperty("OAuthMemberships")] public Membership User { get; set; } } [Table("webpages_Roles")] public class Role { public Role() { Members = new List<Membership>(); } [Key] public int RoleId { get; set; } [StringLength(256)] public string RoleName { get; set; } public ICollection<Membership> Members { get; set; } } //RegisterExternalLoginModelの上
ここらで一度ビルドしてみます。Filterフォルダ内にあるInitializeSimpleMembershipAttribute.csファイルやAccountController.csで、「'UsersContext'が見つかりませんでした。」というエラーが出ます。UsersContextは、MVC4プロジェクトを作成したときに、勝手につけられるSimpleMembership用のDBコンテキスト名です。Code Firstを使う時は、DBコンテキストはDALで定義しているもの一つにしたいので、これを使わないように既存の設定を調整します。なぜ、DBコンテキストを一つにしたいかというと、Code Firstで管理できるDBコンテキストが1つまでと決まっているからです。(MVC4に含まれるEF5の場合に限る。MVC5のEF6は複数定義できるそうです)
FilterフォルダのInitializeSimpleMembershipAttribute.csファイルをプロジェクトから除外します。ファイルを右クリックで、同名のメニュー選択ですね。次に、AccountController.csの先頭のほうにある[InitializeSimpleMembership]を消します。これは、コントローラで利用できるフィルター属性で、先ほど除外したファイルを呼び出し、ユーザー関連のモデルのテーブルがDBに存在しなければ、自動で作成するものです。Code Firstでユーザーのテーブルを作るので、自動生成はしません。
ビルドします。まだ、AccountController.csでエラーが出ますね。ExternalLoginConfirmationアクションの中です。ここでもUsersContextを使った行があります。そのまま使うには、このコンテキストではなく、SchoolContextの方を使うように変えます。「using (UsersContext~」となっている辺りをコメントアウトしてください。下のクローズする部分も同様にコメントアウトです。次に、コントローラの先頭でSchoolContextの呼び出しを追加してやります。
namespace MvcApplication1.Controllers { [Authorize] public class AccountController : Controller { private SchoolContext db = new SchoolContext(); // // GET: /Account/Login
SchoolContextがどこにあるのかわからないので、usingを追加してやりましょう。
using MvcApplication1.DAL;
ビルドします。今度はエラーを出さずにビルドできました。
ユーザーモデルをマイグレーション
ここまで準備できたら、Code Firstのマイグレーションでユーザーモデルを取り込みます。add-migrationします。
add-migration AddSimpleMembership
DBにテーブルを作成するマイグレーションファイルが作成されました。テーブルだけできても仕方がないので、updateする前にユーザーのシードデータも用意してやりましょう。Configuration.csに書くんでしたよね。
//以下はコメントアウトするか削除する。 //public Configuration() //{ // AutomaticMigrationsEnabled = false; //} protected override void Seed(SchoolContext context) { // Seed for UserProfile WebSecurity.InitializeDatabaseConnection( "DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true); if (!WebSecurity.UserExists("testuser")) WebSecurity.CreateUserAndAccount( "testuser", "testuser", new{} );
Seedメソッドの先頭に上のようにコードを追加します。上の部分のConfigurationのコンストラクタは使わないので、コメントアウトするか削除してください。SimpleMembershipを使うときに使えるメソッドは、WebSecurityに入っているので、これをusingに加えてください。
ロールマネージャーを有効にする
もういいかと思って、update-databseしてしまった人は悔い改めてください。「ロール マネージャー機能は有効にされていません。」というエラーが出たと思います。そうです、SimpleMembershipをちょー便利に使うには、RoleMangerを有効にせねばなりません。これを有効にすることでコントローラに「Autorization(認可)」の属性フィルターが使えるようになります。これについてはまた後日解説します。
RoleMangerを有効にするには、Web.configに設定を追加します。(いろいろ面倒くさいですね)
<roleManager enabled="true" defaultProvider="SimpleRoleProvider"> <providers> <clear /> <add name="SimpleRoleProvider" type="WebMatrix.WebData.SimpleRoleProvider, WebMatrix.WebData" /> </providers> </roleManager> <membership defaultProvider="SimpleMembershipProvider"> <providers> <clear /> <add name="SimpleMembershipProvider" type="WebMatrix.WebData.SimpleMembershipProvider, WebMatrix.WebData" /> </providers> </membership>
Web.configの
update-database
SimpleMembershipのテーブルが作成されました。シードデータのtestuserもちゃんと入っています!
One-to-One 再入門(カスケードデリートとかもあるよ)
ザクです。
先の解説では、おざなりになっていたOne-to-Oneの細かい設定をやります。
まず、きっちりOne-to-Oneにする方法から行きます!
きっちりOne-to-One!
前に説明したOne-to-Oneで作ったテーブルのリレーションシップはこのようになっていたと思います。
子テーブル側のOfficeAssignmentに「0..1」と入っていますね。これは、親であるInstructorはOfficeAssignmentを子として持つ必要がないことを意味します。いわゆる、One-to-Zero or Oneというやつです。
しかし厳密にテーブルのリレーションシップを定義したい場合が多々あります。プロパティ数が多すぎるモデルをデータを取りやすいように分けるときなんかがそうですね。
では、いまあるInstructorとOfficeAssignmentをきっちりOne-to-Oneにしてみましょう。方法は2つあります。まず簡単な方から。
Required属性を使う
きっちりOne-to-Oneではモデルにナビゲーションプロパティを使えません。なので、それぞれのモデルで相手になるモデルのナビゲーションプロパティの部分の「virtual」を消して、ただのモデル参照にしてください。そして、Instructor側のOfficeAssignmentの真上に[Required]属性を付けます。これだけです。
// Instructor側 public virtual ICollection<Course> Courses { get; set; } [Required] public OfficeAssignment OfficeAssignment { get; set; } // OfficeAssignment側 public Instructor Instructor { get; set; }
上の変更は、DBのスキーマ設定の変更は発生しないので、DALにあるSchoolContext.csからダイアグラムを出力するだけで変更点が確認できます。
Fluent APIを使う
もう一つの方法はFluent APIを使う方法です。Fluent APIはDALにモデル定義を記述する場合に使います。したがって、各場所はDALのSchoolContext.csです。modelBuilder.Entity
modelBuilder.Entity<Instructor>() .HasRequired(u => u.OfficeAssignment) .WithRequiredPrincipal(o => o.Instructor);
同じくダイアグラムを出してみましょう。
きっちりOne-to-Oneになりました。
One-to-Oneをカスケードデリート
One-to-OneやOne-to-Zero or Oneの関係を定義した時、One-to-Manyの時と違い、なぜか勝手にカスケードデリートがONになりません。カスケードデリートはDBのスキーマ上で定義する必要があるので、マイグレーションのファイルの中でcascadeDeleteのフラグがtrueになっていなければなりません。カスケードデリートのフラグを立てるには、Fluent APIを使います。
//One-to-Zero or Oneの場合 modelBuilder.Entity<Instructor>() .HasOptional(i => i.OfficeAssignment) .WithRequired(o => o.Instructor) .WillCascadeOnDelete(true); //One-to-Oneの場合 modelBuilder.Entity<Instructor>() .HasRequired(i => i.OfficeAssignment) .WithRequiredPrincipal(o => o.Instructor) .WillCascadeOnDelete(true);
きっちりとそうでないのと両方のパターンを用意しました。定義に応じて、SchoolContextを上書きしてください。この変更では、DBのスキーマ変更が発生するので、書き換え後、add-migrationしてください。名前はCascadeDeleteとかでいいでしょう。
cascadeDeleteがtrueになりましたね。
Code Firstのエンティティを簡単出力!Entity Framework Power Tools
ザクです。
前回に書くと約束してた、モデルのダイアグラムを簡単に描く方法です。Visual Studio 2012には、Entity Framework Power Toolsという拡張機能が提供されています。
説明によるとなんか色々できるらしいですが、ザクはモデルを作るのにXMLもDLLも使うのは嫌なので、Read-onlyのモデルダイアグラムの作成にしか使っていません。しかしながら、納品物として要求されることが多いので、Code Firstで管理している最新のモデル図が簡単に出力できるだけでもメリットは大きいです。
では早速VS2012にインストールしてみましょう。拡張機能のインストールは、「ツール」メニューから「拡張機能と更新プログラム」を選びます。ダイアログが表示されるので、検索に「Power」と入れます。はい、出ましたね。ボタンを押して、そいつをインストールするだけです。
インストールできたら、早速使ってみます。プロジェクトのDALフォルダに、SchoolContex.csがあると思います。このファイルを右クリックすると、下のようなメニューが表示されると思います。
「Entity Framework」というメニューが追加されているので、その中の「View Entity Data Model(Read only)」を選びます。すると現在のモデル構成のダイアグラムが自動で出力されます。エンジョイ!
Entity Framework Code First でのリレーションシップ
こんにちは。ザクです。遅めの夏休みをとってリフレッシュしてきました。こう涼しいと夏休み感はゼロですね。
今日はCode Firstでモデル定義する際のリレーションシップの設定方法について書きます。こちらRailsほどちゃんとした情報がなくて大変困ったやつです。Microsoftさんにおかれましては、善処していただきたく存じます。
One-to-Manyリレーションシップ
簡単なのから行きます。1:Nの関係の場合です。
Creating an Entity Framework Data Model for an ASP.NET MVC Application (1 of 10) | The ASP.NET Site
前回紹介したチュートリアルで作成したモデルが早速そうなっています。StudentとEnrollmentの関係がそうですね。モデルのダイアグラム(Visual Studio 2012から自動生成できます。方法はまた後日説明します)を見ると、Student側に「1」、Enrollment側に「*」がついています。つまりStudentが親で、Enrollmentが子という関係です。では、コードをみてみましょう。
public class Student { public int StudentId { get; set; } public string LastName { get; set; } public string FirstMidName { get; set; } public DateTime EnrollmentDate { get; set; } public virtual ICollection<Enrollment> Enrollments { get; set; } }
CodeFirstではモデルを作成する際、最初のintの名前にモデル名が入っていたら、規約に従いそのintを主キーにします。規約から外れた名前をキーに付けたい場合は、[Key]属性をそのintの上に付けることで主キーにできます。
最後の行のvirtualが関係を定義している部分です。ダイアグラムではこの部分はモデルの「ナビゲーションプロパティ」と呼ばれる下の部分に入ります。(ナビゲーションプロパティについては追って説明します)ICollectionなので、コレクションになっており、次の<>の中にモデル名、最後にこのモデル上での名前をつけます。規約にしたがい、モデル上の名前は、モデルを複数形にします。これで、Student(親)側の定義ができました。
次に子供のEnrollment側のコードです。
public enum Grade { A, B, C, D, F } public class Enrollment { public int EnrollmentId { get; set; } public int CourseId { get; set; } public int StudentId { get; set; } public Grade? Grade { get; set; } public virtual Course Course { get; set; } public virtual Student Student { get; set; } }
こちらには鍵になるintが複数定義されています。最初のintだけは自分自身のidでそれ以外は、外部キー(FK)になります。親を持つモデルには、必ず親となるモデルのキーが定義されていなければなりません。
virtualの部分も異なります。こちらはコレクションになっていません。親は一つなので当然です。この場合virtualの後には、モデル名、このモデル上の名前を書きます。規約に従いモデル上の名前には、親のモデル名を単数形で書いてください。
以上で、One-to-Manyの関係が作れます。
Many-to-Manyリレーションシップ
次にわかりやすいのは、N:Nです。
N:Nの定義の方法は2種類あります。2つのテーブルをリンクするテーブルを自分で定義して作るか、Code Firstに作らせるかです。前者の方は、N:Nのリンクテーブルの中にもプロパティを持たせたいときに使います。後者の方は、純粋なN:N用のリンクテーブルを作るときに使います。
まず前者のケースから。これはチュートリアルのEnrollmentモデルが参考になります。親の鍵を2つ持ち、virtualのナビゲーションプロパティも2つ持ちます。(上のEnrollmentを参考)このテーブルはGradeという、このモデルで使うプロパティを持つので、このように作っています。
次は純粋なN:Nリンクテーブルです。こちらはモデルを定義せずに、DALファイルの中にテーブル間の関係を定義するコードを書きます。
先にこの関係に使うモデルを定義しましょう。Instructorモデルを作ります。(namespaceに注意してください)
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace MvcApplication1.Models { public class Instructor { public int InstructorId { get; set; } [Required] [Display(Name = "Last Name")] [StringLength(50)] public string LastName { get; set; } [Required] [Column("FirstName")] [Display(Name = "First Name")] [StringLength(50)] public string FirstMidName { get; set; } [DataType(DataType.Date)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] [Display(Name = "Hire Date")] public DateTime HireDate { get; set; } public string FullName { get { return LastName + ", " + FirstMidName; } } public virtual ICollection<Course> Courses { get; set; } public virtual OfficeAssignment OfficeAssignment { get; set; } } }
次にCourseモデルを作ります。
using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace MvcApplication1.Models { public class Course { [DatabaseGenerated(DatabaseGeneratedOption.None)] [Display(Name = "Number")] public int CourseId { get; set; } [StringLength(50, MinimumLength = 3)] public string Title { get; set; } [Range(0, 5)] public int Credits { get; set; } [Display(Name = "Department")] public int DepartmentId { get; set; } public virtual Department Department { get; set; } public virtual ICollection<Enrollment> Enrollments { get; set; } public virtual ICollection<Instructor> Instructors { get; set; } }
最後にDALを修正します。
public class SchoolContext : DbContext { : base("DefaultConnection") { } public DbSet<Course> Courses { get; set; } public DbSet<Department> Departments { get; set; } public DbSet<Enrollment> Enrollments { get; set; } public DbSet<Instructor> Instructors { get; set; } public DbSet<Student> Students { get; set; } public DbSet<OfficeAssignment> OfficeAssignments { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Course>() .HasMany(c => c.Instructors).WithMany(i => i.Courses) .Map(t => t.MapLeftKey("CourseId") .MapRightKey("InstructorId") .ToTable("CourseInstructor")); } }
DALに記述するコードはこんな風になります。DALを作った時にしたようにDbSetにCourseとInstructorモデルの定義を追加したら、その下のOnModelCreatingの中で、Entityの定義をしてやります。ここでは、Courseから見てInstructorモデルを.HasManyでつなぎ、Instructorから見たCourseを.WithManyでつないでいます。.Map以下でそれぞれが持つFKと出来上がったテーブルの名前を定義します。
N:Nの定義はこんな感じです。
One-to-Zero or Oneリレーションシップ
最後に1:1です。1:1の定義方法も2種類ですが、普通のOne-to-Oneはまたこんど説明します。説明が多くなるので、よく使う簡単なZero or Oneの方を先に説明します。
チュートリアルでは、InstructorとOfficeAssignmentモデルがこれにあたります。先に作ったInstructorの中にOfficeAssignmentのナビゲーションプロパティがあったと思います。ここでは、OfficeAssignmentを作りましょう。
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace MvcApplication1.Models { public class OfficeAssignment { [Key] [ForeignKey("Instructor")] public int InstructorId { get; set; } [StringLength(50)] [Display(Name = "Office Location")] public string Location { get; set; } public virtual Instructor Instructor { get; set; } } }
1:1の子テーブルの特徴は、自身のモデル名がついたIdを持たないことです。代わりに親のIdを主キーとして持ちます。InstructorIdに2つの属性[Key]と[ForeignKey("Instructor")]がついていますね。つまり、このモデルはInstructorIdを主キーであり、親のInstructorモデルへのFKでもあるということです。ナビゲーションプロパティは、1:Nの時と同様に子モデルなので、1つの親を単数形で定義します。1:1の簡単な方の定義は以上です。
ここまでできたらチュートリアルを参考にシードデータも更新しましょう。(チュートリアルでは、Departmentという1:Nのモデルも定義しているので、こちらを作っておくのをお忘れなく。コピペしたデータにこのモデル用のデータも入っているのでエラーが出ますよ)