しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET Identity から ASP.NET Core Identity へ移行してみた

ASP.NET MVC 5 から Core MVC 2.1 への移行作業を行っていますが、地味に Identity 周りの移行ではまった部分が多かったのでメモとして残します。作業としては DB のマイグレーションがメインです。

移行に関してはドキュメントがありますが、正直これは役に立たないですね。

喜ばしいことに ASP.NET Core Identity になってもパスワードのハッシュ化形式は変更されていないので、ASP.NET Identity のデータをそのままでログインすることは出来ます。

しかし、テーブルのスキーマが変更されているので、マイグレーションを行う必要があります。パッと調べた感じではマイグレーションの方法は公式に用意されていないみたいなので、手動で対応しないといけません。

Identity 設定を移行する

とりあえず ASP.NET Identity と ASP.NET Core Identity のプロジェクトを用意して、DB の差分を確認することから始めます。以下はテンプレートそのままのプロジェクトです。

f:id:shiba-yan:20180709233528p:plain

ASP.NET Identity 側にある ApplicationUserManager.Create で設定している情報は、ASP.NET Core Identity では Startup クラスの中で IdentityOptions への設定として行います。

具体的にコードで例を出してみます。以下は ASP.NET Identity でのデフォルト設定です。

public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
{
    var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
    // ユーザー名の検証ロジックを設定します
    manager.UserValidator = new UserValidator<ApplicationUser>(manager)
    {
        AllowOnlyAlphanumericUserNames = false,
        RequireUniqueEmail = true
    };

    // パスワードの検証ロジックを設定します
    manager.PasswordValidator = new PasswordValidator
    {
        RequiredLength = 6,
        RequireNonLetterOrDigit = true,
        RequireDigit = true,
        RequireLowercase = true,
        RequireUppercase = true,
    };

    // ユーザー ロックアウトの既定値を設定します。
    manager.UserLockoutEnabledByDefault = true;
    manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
    manager.MaxFailedAccessAttemptsBeforeLockout = 5;

    return manager;
}

上の設定を ASP.NET Core Identity 向けに変更したら、以下のようになります。

大体はデフォルト値のままで問題ないですが、何故か RequireUniqueEmail の値だけは異なっていました。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<IdentityOptions>(options =>
    {
        options.User.RequireUniqueEmail = true;

        options.Password.RequiredLength = 6;
        options.Password.RequireNonAlphanumeric = true;
        options.Password.RequireDigit = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireUppercase = true;

        options.Lockout.AllowedForNewUsers = true;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
        options.Lockout.MaxFailedAccessAttempts = 5;
    });
}

カスタマイズしている場合には、この設定を合わせておかないと不整合が発生して、下手したらログイン出来なくなるパターンもあるので注意したいですね。

Identity DB を移行する

両方とも実行してユーザーを作成すると、いい感じに LocalDB にデータベースが作られます。

f:id:shiba-yan:20180709234158p:plain

Identity と Core Identity でテーブル名は変更されていないのですが、新しくテーブルが追加されたりしているので、この差分を何とかしないと正しく実行できません。

なので Visual Studio の SQL Server オブジェクト エクスプローラーから差分をチェックします。

これはソース DB とターゲット DB を指定すると、分かりやすく差分を表示してくれる上に変更用スクリプトも生成してくれる優れものです。手動で SQL を書く手間が省けました。

f:id:shiba-yan:20180709233535p:plain

__MigrationHistory というテーブルは無視して良いですが、何だかんだで全テーブル構造が異なっているという結果になりました。これは結構厳しいです。

当然ながら、この状態で Core Identity 側で Identity が作成した DB を使うようにするとエラーになります。

f:id:shiba-yan:20180710000514p:plain

先ほどチェックした差分から変更用スクリプトを作成して実行すれば、データを保持したまま構造を変えてくれますが、今回のケースでは LockoutEndDateUtc というカラムが LockoutEnd にリネームされているので、その部分だけはデータを捨てるなりスクリプトを変更するなりで対応します。

今回はデータを捨てて対応したので、マイグレーション自体はあっさり終わりましたが、これでもログインはまだ行えません。試すとログインエラーになってしまいます。

f:id:shiba-yan:20180710001806p:plain

DB のマイグレーションは問題ないのですが、最新の Core Identity は新しく追加された以下のカラムを利用してログイン処理を行うため、データを詰めてあげないと絶対にログインが失敗します。

  • NormalizedUserName
  • NormalizedEmail

このカラムには UserNameEmail を大文字化した値を入れる必要があります。

マイグレーションのスクリプト内で UPPER を行っても良いですし、私みたいに後からこの仕様変更に気が付いた場合は UPDATE を流せば良いです。

UPDATE [AspNetUsers] SET [NormalizedUserName] = UPPER([UserName]), [NormalizedEmail] = UPPER([Email])

この対応後、ようやく Core Identity でのログインが正しく行えるようになりました。長かったですね。

一応は公式で Identity と Core Identity の互換性レイヤーが提供されていますが、今回は完全なる Core Identity への移行が目的だったので使いませんでした。

しかし、このレイヤーのコードを読むことで互換性がない部分を理解できたので助かりました。