MySQL で ASP.NET MVC のユーザ認証

前々回の投稿で MySQL を用いた Entity framework マイグレーション の利用について書きましたが、その続きとして MySQL を利用した ASP.NET MVC のユーザ認証/承認について書いてみます。

マイグレーションを有効にして、初期のテーブル作成部分を修正することで、ユーザー認証/承認を行うことができるようになります。 🙂

例として、前回同様、本の情報を扱うアプリケーションを作成していきます 😉

最初にプロジェクトの作成。プロジェクト名は「MysqlCodeFirstWithAuth02」とししています。なお、認証で「個人ユーザー アカウント」を選択しておきます。

プロジェクトが作成できたら、NuGet から MySql.ConnectorNET.Entity version 6.8.3.2 を導入します(Entity Framework はプロジェクト生成時に バージョン 6.1.1 が導入されています)。

まずはモデルの作成。
Models フォルダに Book クラスを作成します。

using System.ComponentModel.DataAnnotations;

namespace MysqlCodeFirstWithAuth02.Models
{
    public class Book
    {
        public int Id { get; set; }
        [Required]
        [MaxLength(50)]
        [Display(Name="タイトル")]
        public string Title { get; set; }
    }
}

次に、DbContext に作成したモデルの記述を追加するとともに MySQL 用の設定を行います。
Models フォルダの IdentityModels.cs ファイルを修正します。

using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Migrations.History;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;

using MySql.Data.Entity;
using MySql.Data.MySqlClient;

namespace MysqlCodeFirstWithAuth02.Models
{
    // ApplicationUser クラスにプロパティを追加することでユーザーのプロファイル データを追加できます。詳細については、http://go.microsoft.com/fwlink/?LinkID=317594 を参照してください。
    public class ApplicationUser : IdentityUser
    {
        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
        {
            // authenticationType が CookieAuthenticationOptions.AuthenticationType で定義されているものと一致している必要があります
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            // ここにカスタム ユーザー クレームを追加します
            return userIdentity;
        }
    }

    [DbConfigurationType(typeof(MysqlConfiguration))]
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
        }

        public DbSet<Book> Books { get; set; }

        public static ApplicationDbContext Create()
        {
            var context = new ApplicationDbContext();
            context.Database.Log = System.Console.Write;
            return context;
        }
    }

    public class MysqlConfiguration : DbConfiguration
    {
        public MysqlConfiguration()
        {
            AddDependencyResolver(new MySqlDependencyResolver());
            SetProviderFactory(MySqlProviderInvariantName.ProviderName, new MySqlClientFactory());
            SetDefaultConnectionFactory(new MySqlConnectionFactory());
            SetMigrationSqlGenerator(MySqlProviderInvariantName.ProviderName, () => new MySqlMigrationSqlGenerator());
            SetProviderServices(MySqlProviderInvariantName.ProviderName, new MySqlProviderServices());
            SetProviderFactoryResolver(new MySqlProviderFactoryResolver());
            SetManifestTokenResolver(new MySqlManifestTokenResolver());

            // __migrationHistory テーブルのデフォルト設定の変更
            SetHistoryContext("MySql.Data.MySqlClient", (connection, defaultSchema) => new MyHistoryContext(connection, defaultSchema));
        }
    }

    public class MyHistoryContext : HistoryContext
    {
        public MyHistoryContext(DbConnection dbConnection, string defaultSchema)
            : base(dbConnection, defaultSchema)
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // 複合キー(MigrationId, ContextKey)の長さがデフォルトでは大きすぎるので設定を変更する
            // 基底クラスの OnModelCreating(modelBuilder) でデフォルト設定を行っているので、base.OnModelCreating() の後に行うこと
            modelBuilder.Entity<HistoryRow>().Property(h => h.MigrationId).HasMaxLength(100).IsRequired();
            modelBuilder.Entity<HistoryRow>().Property(h => h.ContextKey).HasMaxLength(200).IsRequired();
        }
    }
}

前回の記事では、BooksContext を作成しましたが、今回は、認証で「個人ユーザー アカウント」を選択したことで自動作成された ApplicationDbContext クラスへ手を入れています。

次に、MySQL のデータベース ユーザへ作成するデータベースを扱う権限を与えておきます(DB 名: efcfSample02, DB ユーザー名: efcf01。前回の記事のユーザーを作成していない場合はパスワード:******** とします)。
(ユーザーが存在する場合)
MySQL Command Line Client の例:grant all on efcfSample02.* to ‘efcf01’@’localhost’;
(ユーザーが存在しない場合)
MySQL Command Line Client の例:grant all on efcfSample02.* to ‘efcf01’@’localhost’ identified by ‘********’;

次に、プロジェクト ルートの Web.config を修正します。

...
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=自動生成された値" requirePermission="false" />
  </configSections>
  <connectionStrings>
    <add name="DefaultConnection" connectionString="server=localhost;database=efcfSample03;uid=efcf01;password=********;charset=utf8" providerName="MySql.Data.MySqlClient" />
  </connectionStrings>
  <appSettings>
...
  </runtime>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
      <provider invariantName="MySql.Data.MySqlClient" type="MySql.Data.MySqlClient.MySqlProviderServices, MySql.Data.Entity.EF6" />
    </providers>
  </entityFramework>
<system.data>
    <DbProviderFactories>
      <remove invariant="MySql.Data.MySqlClient" />
      <add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient" description=".Net Framework Data Provider for MySQL" type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.8.3.0, Culture=neutral, PublicKeyToken=自動生成された値" />
    </DbProviderFactories>
  </system.data>
</configuration>

次のことを行っています。

  • connectionStrings の設定
  • entityFramework の providers への MySql.Data.MySqlClient の追加(MySQL の Chapter 10 EF 6 Support を参照)
  • system.data の DbProviderFactories の remove にある name=”MySQL Data Provider” の削除(自動追加されるんですが、文法違反になっているという。。。バグですね。。。そのうち修正されるでしょう)

これでマイグレーションの準備ができました。
パッケージマネージャ コンソールで次のコマンドを入力します。
Enable-Migrations

生成された Configuration.cs を修正します(User ロールを初期データとして持つようにする)。

namespace MysqlCodeFirstWithAuth02.Migrations
{
    using System.Data.Entity.Migrations;

    using Microsoft.AspNet.Identity;
    using Microsoft.AspNet.Identity.EntityFramework;

    using MysqlCodeFirstWithAuth02.Models;

    internal sealed class Configuration : DbMigrationsConfiguration<ApplicationDbContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(ApplicationDbContext context)
        {
            AddRole(context);
        }

        private bool AddRole(ApplicationDbContext context)
        {
            IdentityResult ir;
            var rm = new RoleManager<IdentityRole>
                (new RoleStore<IdentityRole>(context));
            ir = rm.Create(new IdentityRole("User"));

            return ir.Succeeded;
        }
    }
}

パッケージマネージャ コンソールで次のコマンドを入力します。
Add-Migration Initial

生成された Initial クラスを修正します。

...
            CreateTable(
                "dbo.AspNetRoles",
                c => new
                    {
                        Id = c.String(nullable: false, maxLength: 128, storeType: "nvarchar"),
                        //Name = c.String(nullable: false, maxLength: 256, storeType: "nvarchar"),
                        Name = c.String(nullable: false, maxLength: 255, storeType: "nvarchar"),
                    })
                .PrimaryKey(t => t.Id)
                .Index(t => t.Name, unique: true, name: "RoleNameIndex");
...
            CreateTable(
                "dbo.AspNetUsers",
                c => new
                    {
                        Id = c.String(nullable: false, maxLength: 128, storeType: "nvarchar"),
                        Email = c.String(maxLength: 256, storeType: "nvarchar"),
                        EmailConfirmed = c.Boolean(nullable: false),
                        PasswordHash = c.String(unicode: false),
                        SecurityStamp = c.String(unicode: false),
                        PhoneNumber = c.String(unicode: false),
                        PhoneNumberConfirmed = c.Boolean(nullable: false),
                        TwoFactorEnabled = c.Boolean(nullable: false),
                        LockoutEndDateUtc = c.DateTime(precision: 0),
                        LockoutEnabled = c.Boolean(nullable: false),
                        AccessFailedCount = c.Int(nullable: false),
                        //UserName = c.String(nullable: false, maxLength: 256, storeType: "nvarchar"),
                        UserName = c.String(nullable: false, maxLength: 255, storeType: "nvarchar"),
                    })
                .PrimaryKey(t => t.Id)
                .Index(t => t.UserName, unique: true, name: "UserNameIndex");
...

修正前の行をコメントで残しています。
この修正は、max key length is 767 bytes の壁にインデックスが引っかかるのを回避するためのものです。

パッケージマネージャ コンソールで次のコマンドを入力します。
Update-Database

これで、MySQL のデータベースが作成され、初期データも設定されます。

次に Controllers フォルダの AccountController クラスを修正します。
修正内容は、次のものです。

  • ユーザーの登録時に当該ユーザーを User ロールに所属させる
...
        //
        // POST: /Account/Register
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Register(RegisterViewModel model)
        {
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
                var result = await UserManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    // ユーザをロールへ所属させる
                    await UserManager.AddToRoleAsync(user.Id, "User");

                    await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
                    
                    // アカウント確認とパスワード リセットを有効にする方法の詳細については、http://go.microsoft.com/fwlink/?LinkID=320771 を参照してください
                    // このリンクを含む電子メールを送信します
                    // string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
                    // var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
                    // await UserManager.SendEmailAsync(user.Id, "アカウントの確認", "このリンクをクリックすることによってアカウントを確認してください <a href=\"" + callbackUrl + "\">こちら</a>");

                    return RedirectToAction("Index", "Home");
                }
                AddErrors(result);
            }

            // ここで問題が発生した場合はフォームを再表示します
            return View(model);
        }
...
        //
        // POST: /Account/ExternalLoginConfirmation
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl)
        {
            if (User.Identity.IsAuthenticated)
            {
                return RedirectToAction("Index", "Manage");
            }

            if (ModelState.IsValid)
            {
                // 外部ログイン プロバイダーからユーザーに関する情報を取得します
                var info = await AuthenticationManager.GetExternalLoginInfoAsync();
                if (info == null)
                {
                    return View("ExternalLoginFailure");
                }
                var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
                var result = await UserManager.CreateAsync(user);
                if (result.Succeeded)
                {
                    result = await UserManager.AddLoginAsync(user.Id, info.Login);
                    if (result.Succeeded)
                    {
                        // ユーザをロールへ所属させる
                        await UserManager.AddToRoleAsync(user.Id, "User");

                        await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
                        return RedirectToLocal(returnUrl);
                    }
                }
                AddErrors(result);
            }

            ViewBag.ReturnUrl = returnUrl;
            return View(model);
        }
...

次に、リポジトリ関係です。
プロジェクトに DAL フォルダを作成します。
作成した DAL フォルダに IBooksRepository インターフェイスを作成します。

using System;
using System.Linq;
using System.Threading.Tasks;

using MysqlCodeFirstWithAuth02.Models;

namespace MysqlCodeFirstWithAuth02.DAL
{
    public interface IBooksRepository : IDisposable
    {
        IQueryable<Book> FindBooks();
        Task<Book> GetBookAsync(int id);
        Book AddBook(Book book);
        void UpdateBook(Book book);
        Task DeleteBook(int id);

        Task SaveAsync();
    }
}

次に DAL フォルダに BooksRepository クラスを作成します。

using System;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;

using MysqlCodeFirstWithAuth02.Models;

namespace MysqlCodeFirstWithAuth02.DAL
{
    public class BooksRepository : IBooksRepository
    {
        private ApplicationDbContext _context;

        public BooksRepository()
        {
            _context = new ApplicationDbContext();
        }

        #region IBooksRepository メンバー

        public IQueryable<Book> FindBooks()
        {
            IQueryable<Book> query = _context.Books.AsNoTracking().OrderBy(k => k.Id);
            return query;
        }

        public async Task<Book> GetBookAsync(int id)
        {
            return await getBook(id);
        }

        public Book AddBook(Book book)
        {
            return _context.Books.Add(book);
        }

        public void UpdateBook(Book book)
        {
            _context.Entry(book).State = EntityState.Modified;
        }

        public async Task DeleteBook(int id)
        {
            var book = await getBook(id);
            if (book != null)
            {
                _context.Books.Remove(book);
            }
        }

        public async Task SaveAsync()
        {
            await _context.SaveChangesAsync();
        }

        private async Task<Book> getBook(int id)
        {
            return await _context.Books.FindAsync(id);
        }

        #endregion

        #region IDisposable メンバー

        private bool _disposed = false;

        /// <summary>
        /// リソースの開放を行います。
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// リソースの開放を行います。
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (_disposed) return;

            _disposed = true;

            if (disposing)
            {
                // マネージ リソースの解放処理
            }
            // アンマネージ リソースの解放処理
            _context.Dispose();
        }

        #endregion

        /// <summary>
        /// デストラクタ
        /// </summary>
        ~BooksRepository()
        {
            Dispose(false);
        }
    }
}

次に、FilterConfig を修正して、全体に Authorize 属性を設定します。
App_Start フォルダの FilterConfig クラスを修正します。

using System.Web;
using System.Web.Mvc;

namespace MysqlCodeFirstWithAuth02
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new AuthorizeAttribute());
        }
    }
}

次に、HomeController です。
Controllers フォルダの HomeController クラスを修正します。
修正内容は、次のものです。

  • List, Create アクションを実装(例示なので、Detail, Update, Delete の各アクションは実装しません)
  • Index, About, Contact アクションに AllowAnonymous 属性を設定
using System.Threading.Tasks;
using System.Web.Mvc;

using MysqlCodeFirstWithAuth02.DAL;
using MysqlCodeFirstWithAuth02.Models;

namespace MysqlCodeFirstWithAuth02.Controllers
{
    public class HomeController : Controller
    {
        private readonly IBooksRepository _repository;

        public HomeController() : this(new BooksRepository()) { }
        public HomeController(IBooksRepository repository)
        {
            _repository = repository;
        }

        [AllowAnonymous]
        public ActionResult Index()
        {
            return View();
        }

        [AllowAnonymous]
        public ActionResult List()
        {
            return View(_repository.FindBooks());
        }

        // GET: Cm/Create
        [Authorize(Roles = "User")]
        public ActionResult Create()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        [Authorize(Roles = "User")]
        public async Task<ActionResult> Create([Bind(Include = "Id,Title")] Book book)
        {
            if (ModelState.IsValid)
            {
                _repository.AddBook(book);
                await _repository.SaveAsync();
                return RedirectToAction("List");
            }

            return View(book);
        }

        [AllowAnonymous]
        public ActionResult About()
        {
            ViewBag.Message = "Your application description page.";

            return View();
        }

        [AllowAnonymous]
        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";

            return View();
        }
    }
}

最後にビューです。
Index を修正します。

@{
    ViewBag.Title = "Home Page";
}

<div class="jumbotron">
    <h1>MySQL DB の利用テスト</h1>
    <p class="lead">MySQL を利用する 認証機能とアプリケーション動作のテスト。</p>
    <p>@Html.ActionLink("本の一覧", "List", null, new { @class = "btn btn-primary btn-lg" })</p>
</div>

次に、List, Create の View を実装します(こちらは生成されるもので足りるのでコードは掲載しません)。

これでアプリケーションが動きます。
本の一覧まではログイン指定なくても参照できますが、本の情報を登録しようとすると、ログインを求められます。新規ユーザー登録で電子メールアドレスとパスワード
を登録すると本の情報を追加することができます(Create アクションはロールによるアクセス制限が掛かっています)。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です