【C#入門】初学者がASP.NETでWebアプリを作る：第4回

#今回やること
アカウント認証を実装します。

#初めに
既存のプロジェクトにASP.NET Identityを導入する方法はないようです。。。
ダミーで認証ありのプロジェクトを作って、そこから必要なものをコピーして編集して…みたいな方法しかなかったです。
さすがに面倒くさすぎるので、最初から作り直すことにしました。

#認証機能付きプロジェクトを作る
認証を「個別のユーザアカウント」で作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/6030f477-ed09-22bd-6046-75ae69ca939a.png)

#差分を取り込む
NuGetでNpgsqlをインストールします。
上記で作成したプロジェクトともともとのプロジェクトの差分を取り込みます。（[第2回](https://qiita.com/yuukieiu/items/6e8e0b483fdbfcae9415)参照）

appsettings.jsonのDefaultConnectionをPostgresに戻します。

```json:appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Port=5432;Database=SMSDB;User ID=postgres;Password=password;Enlist=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}
```

Startup.csのConfigureServicesのUseSqlServerをUseNpgsqlに戻します。

```C#:Startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                //options.UseSqlServer(
                options.UseNpgsql(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDatabaseDeveloperPageExceptionFilter();

            services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddControllersWithViews();
        }
```

自分で作ったファイルたちをコピーします。
コントローラ、モデル、ビュー等。（WinMergeで差分を見つけていきました）

次に自前のDbContextからApplicationDbContextに一部記述を移植して、自前のDbContextを削除、自前のDbContextを参照している個所をApplicationDbContextに書き換えます。

```C#:ApplicationDbContext.cs
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;

namespace SalaryManagementSystem.Data
{
    public class ApplicationDbContext : IdentityDbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<SalaryManagementSystem.Models.Salary> Salary { get; set; } // 移植箇所
    }
}

```

最後に、2つあるMigrationsフォルダを丸ごと削除します。
Data配下とプロジェクト直下にあります。

パッケージマネージャコンソールから、Add-MigrationとUpdate-DataBaseコマンドを実行します。
認証用のテーブルを作るためのマイグレーションが必要なのですが、上記コマンドを実行するためには一度既存のマイグレーションによる生成ファイルは削除する必要がありました。
そのため、Migrationsフォルダを削除しています。
また、DbContextは1つに統合できたので統合しました。

マイグレーションのコマンドで、モデルに対応するテーブル（Salaryテーブル）は既にあるので警告が出ます。
このテーブルだけでなく全部のテーブル作成がロールバックされてしまうので、Salaryテーブルは削除して、実行します。
実行できると、認証機能用のテーブルができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/380de4e0-5105-2c43-16a8-1b47134cc439.png)

#ひとまず起動してみる
右上にRegisterとLoginが表示されるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/61e315d1-78e5-e5a3-fd67-fdfe6d3dcaac.png)

例によって日本語化しておきます。ログイン周りの定義は_Layout.cshtmlから呼び出している_LoginPartical.cshtmlに定義されています。

```html:_LoginPartical.cshtml
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a  class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">ようこそ @User.Identity.Name!</a>
    </li>
    <li class="nav-item">
        <form  class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })">
            <button  type="submit" class="nav-link btn btn-link text-dark">ログアウト</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">アカウント登録</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">ログイン</a>
    </li>
}
</ul>
```

#デフォルトの動きを確認
まずアカウント登録の画面に遷移すると下記のような画面になります。
メールアドレスを入力するようになっていますが、面倒なのでユーザIDにしたいです。
あと右側の文言は「外部サービス連携は使えませんよ」的なメッセージなので消したいです。
適当に入力しRegisterを押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/27ca3dc3-b930-b7b2-2e75-26f1eabc675f.png)

…先に言ってほしいやつですね。この辺を右側に書きましょうか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/43e6683b-3a1f-a7a5-9e2a-55f258040f52.png)

登録が完了しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/e9a892e1-755e-c044-75fa-ec826d126d49.png)

ログインしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/90aa09c2-dbd2-a85f-d994-ff8bc688f40e.png)

ログイン失敗しました。どうやらメール認証しないとダメみたいですね。
メール認証…いったんオフにしたいですね…
今回はテーブルデータを更新して、認証したことにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/a16df04a-6c68-a3df-1608-40fd91f46fbc.png)

AspNetUsersテーブルのEmailConfirmedをtrueにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/0db4e3e6-c1d5-b34d-9be3-a9437c6f0a3b.png)

もう一度ログイン。ログインできました。メールアドレス＝ユーザIDはちょっと気に入らないですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/8a2a8572-feda-8be2-15a9-6164bd2604de.png)

#変更したいところ
・メールアドレスとパスワードではなく、IDとパスワードにしたい。（あと表示名）
　→つまり独自のユーザー情報モデルを作る
・登録画面の右のペインはパスワードのルールに書き換える

#独自のユーザー情報モデルを作る
こちらを参考にします。
https://kiyokura.hateblo.jp/entry/2014/06/23/010749

ユーザ情報モデルはSMSUserというクラスにします。ASP.NET CoreだとIUserではなくIdentityUserというインターフェースになります。

```C#:SMSUser.cs
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;

namespace SalaryManagementSystem.Models
{
    [Table("SMSUser")]
    public class SMSUser : IdentityUser
    {
        [Key]
        [Column("UserId")]
        [Display(Name = "ユーザーID")]
        public string SMSUserId { get; set; }

        [Column("UserName")]
        [Display(Name = "ユーザー名")]
        public string SMSUserName { get; set; }

        [Display(Name = "パスワード")]
        public string Password { get; set; }

        public bool IsLocked { get; set; }

        public int LoginFailCount { get; set; }

        public DateTime CreateDate { get; set; }

        public DateTime UpdateDate { get; set; }

        [NotMapped] public override string Id { get; set; }
        [NotMapped] public override string NormalizedUserName { get; set; }
        [NotMapped] public override string Email { get; set; }
        [NotMapped] public override string NormalizedEmail { get; set; }
        [NotMapped] public override bool EmailConfirmed { get; set; }
        [NotMapped] public override string PasswordHash { get; set; }
        [NotMapped] public override string SecurityStamp { get; set; }
        [NotMapped] public override string ConcurrencyStamp { get; set; }
        [NotMapped] public override string PhoneNumber { get; set; }
        [NotMapped] public override bool PhoneNumberConfirmed { get; set; }
        [NotMapped] public override bool TwoFactorEnabled { get; set; }
        [NotMapped] public override DateTimeOffset? LockoutEnd { get; set; }
        [NotMapped] public override bool LockoutEnabled { get; set; }
        [NotMapped] public override int AccessFailedCount { get; set; }

    }
}

```

ユーザストアクラスを作ります。雰囲気で実装しました。よくわかっていません…。

```C#SMSUserStore.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SalaryManagementSystem.Data;

namespace SalaryManagementSystem.Models
{
    public class SMSUserStore : IUserPasswordStore<SMSUser>
    {
        private ApplicationDbContext _context;
        private static List<SMSUser> Users = new List<SMSUser>();

        public SMSUserStore(ApplicationDbContext context)
        {
            _context = context;
        }
        public Task<IdentityResult> CreateAsync(SMSUser user, CancellationToken cancellationToken)
        {
            _context.SMSUsers.Add(user);
            _context.SaveChangesAsync();
            return (Task<IdentityResult>)Task.Delay(0);
        }

        public Task<IdentityResult> DeleteAsync(SMSUser user, CancellationToken cancellationToken)
        {
            _context.SMSUsers.Remove(user);
            _context.SaveChangesAsync();
            return (Task<IdentityResult>)Task.Delay(0);
        }

        public void Dispose()
        {
            // 何もしない
        }

        public Task<SMSUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
        {
            return _context.SMSUsers.FirstOrDefaultAsync(u => u.SMSUserId == userId, cancellationToken);
        }

        public Task<SMSUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
        {
            return _context.SMSUsers.FirstOrDefaultAsync(u => u.SMSUserName.ToUpper() == normalizedUserName, cancellationToken);
        }

        public Task<string> GetNormalizedUserNameAsync(SMSUser user, CancellationToken cancellationToken)
        {
            return Task.Run(() => user.SMSUserName.ToUpper(), cancellationToken);
        }

        public Task<string> GetPasswordHashAsync(SMSUser user, CancellationToken cancellationToken)
        {
            return Task.Run(() => user.Password, cancellationToken);
        }

        public Task<string> GetUserIdAsync(SMSUser user, CancellationToken cancellationToken)
        {
            return Task.Run(() => user.SMSUserId, cancellationToken);
        }

        public Task<string> GetUserNameAsync(SMSUser user, CancellationToken cancellationToken)
        {
            return Task.Run(() => user.SMSUserName, cancellationToken);
        }

        public Task<bool> HasPasswordAsync(SMSUser user, CancellationToken cancellationToken)
        {
            return Task.Run(() => string.IsNullOrEmpty(user?.Password), cancellationToken);
        }

        public Task SetNormalizedUserNameAsync(SMSUser user, string normalizedName, CancellationToken cancellationToken)
        {
            throw new NotSupportedException("NormalizedUserName is not supported.");
        }

        public Task SetPasswordHashAsync(SMSUser user, string passwordHash, CancellationToken cancellationToken)
        {
            user.Password = passwordHash;
            return (Task<IdentityResult>)Task.Delay(0);
        }

        public Task SetUserNameAsync(SMSUser user, string userName, CancellationToken cancellationToken)
        {
            user.SMSUserName = userName;
            return (Task<IdentityResult>)Task.Delay(0);
        }

        public Task<IdentityResult> UpdateAsync(SMSUser user, CancellationToken cancellationToken)
        {
            _context.SMSUsers.Update(user);
            _context.SaveChangesAsync();
            return (Task<IdentityResult>)Task.Delay(0);
        }
    }
}
```

これに伴い、DBContextも修正しています。
継承クラスをIdentityDbContext→DbContextに変更し、
SMSUsersを追加しました。

```C#:ApplicationDbContext.cs
public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<SalaryManagementSystem.Models.Salary> Salary { get; set; }
        public DbSet<SalaryManagementSystem.Models.SMSUser> SMSUsers { get; set; }
    }
```

最後にStartUp.csを修正します。

```C#:Startup.cs
public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDatabaseDeveloperPageExceptionFilter();

            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            //    .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddIdentity<SMSUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();
            services.AddControllersWithViews();
        }
```

テーブル（SMSUser）がないので、作ります。
なぜかスキャフォールディングでモデルクラスとして選べなかったので、手動で作りました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/1ea59779-c6b8-53ad-9cb9-4972f77b59a9.png)


#次回予告
長くなってきたので次回に持ち越します。
「あっこいつ間違っとるな…」と思ったらコメントで教えていただけるととても助かります…。