ASP.NET Core 3.1 RazorPagesで工数管理アプリを作ってみる③-Identity編

# ASP.NET Core 3.1 RazorPagesでユーザ管理をする
今回はユーザ管理機能を作成します。テーマは次の通りとします。

・認証及び承認に.NET Core Identity を利用する。
・特定のロールを持ったユーザしかアクセスできない画面を作る。

ちなみに認証とは「ユーザが誰であるかを特定する機能」、承認とは「ユーザがどんな権限を持っているのか特定する機能」を指します。

# 権限付きWEBアプリの作成
せっかく前回まででモデルを定義してマスタ管理できるようになったのですが、権限付きのWEBアプリを作る際は一旦初めからプロジェクトを作りなおした方が早いので、公式ドキュメントの[権限付きWEBアプリの作成](https://docs.microsoft.com/ja-jp/aspnet/core/security/authorization/secure-data?view=aspnetcore-3.1)を参考に作っていきたいと思います。

# WEBアプリの作成
vscodeから下記コマンドを実行して新しいWEBアプリを作成します。

```console:cmd
dotnet new webapp -o KosuM --auth Individual -uld 
```

必要なパッケージとかを追加します。

```console:cmd
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
```

DBの設定をします。今回からは一旦ローカルSQLServerを利用します。

```json:appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=KosuM_DB;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}
```

次に/Data/ApplicationDbContext.csを編集します。

```c#:/Data/ApplicationDbContext.cs
public class ApplicationDbContext : IdentityDbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        //IdentityのDB設定を行う
        base.OnModelCreating(modelBuilder);

        //複合主キーの設定
        modelBuilder.Entity<AnswerDetail>()
            .HasKey(c => new { c.AnswerID, c.AnswerDetailID });
        modelBuilder.Entity<QuestionDetail>()
            .HasKey(c => new { c.QuestionID, c.QuestionDetailID });
     }

    //部署
    public DbSet<Division> Division { get; set; }
    //グループ(課)
    public DbSet<Group> Group { get; set; }
    //グループメンバー(課員)
    public DbSet<GroupUser> GroupUser { get; set; }        
    //プロジェクト
    public DbSet<Project> Project { get; set; }
    //プロジェクト内作業
    public DbSet<ProjectTask> ProjectTask { get; set; }
    //プロジェクトメンバー―
    public DbSet<ProjectTask> ProjectUser { get; set; }
    }
```

各モデルの内容は割愛します。

次に.NET core Identity のアカウント登録/ログイン/ログアウトの画面をスキャフォールディングします。

```console:cmd
dotnet aspnet-codegenerator identity -dc KosuM.Data.ApplicationDbContext --files="Account.Register;Account.Login;Account.Logout"
```

合わせて、部署/グループ/プロジェクト/プロジェクト内作業のCRUD画面をスキャフォールディングします。

```console:cmd
dotnet aspnet-codegenerator razorpage -m Division -dc KosuM.Data.ApplicationDbContext -udl -outDir Pages\Divisions --referenceScriptLibraries
```

```console:cmd
dotnet aspnet-codegenerator razorpage -m Group -dc KosuM.Data.ApplicationDbContext -udl -outDir Pages\Groups --referenceScriptLibraries
```

```console:cmd
dotnet aspnet-codegenerator razorpage -m GroupUser -dc KosuM.Data.ApplicationDbContext -udl -outDir Pages\GroupUsers --referenceScriptLibraries
```

```console:cmd
dotnet aspnet-codegenerator razorpage -m Project -dc KosuM.Data.ApplicationDbContext -udl -outDir Pages\Projects --referenceScriptLibraries
```

```console:cmd
dotnet aspnet-codegenerator razorpage -m ProjectTask -dc KosuM.Data.ApplicationDbContext -udl -outDir Pages\ProjectTasks --referenceScriptLibraries
```

```console:cmd
dotnet aspnet-codegenerator razorpage -m ProjectUser -dc KosuM.Data.ApplicationDbContext -udl -outDir Pages\ProjectUsers --referenceScriptLibraries
```

最後にDBマイグレーションを行って、アップデートします。

```console:cmd
dotnet ef migrations add 1st_migration
```

```console:cmd
dotnet ef database update
```

一応、ここまで来ると上記でスキャフォールディングした画面で各マスタの更新はできるようになっているかと思います。今回のアプリにおいてはスキャフォールディングしたものをそのまま利用することはできないので、一部ソースを変更していますが、その部分については割愛します。

# 認証の追加
この段階では各マスタの更新はアクセスした誰でも行う事ができるようになっています。そのままでは運用上いろいろマズイと思いますので、認証を追加して登録したユーザのみがマスタ更新できるように変更したいと思います。
また、ここから先はどうしても公式ドキュメントの通りにできず・・・一部私なりの解釈を入れて実装しております。おかしな点等あればコメント等でご指摘いただけますと幸いです。

## シードデータの登録
まず、下記のようなロジッククラスにシードユーザを登録する処理を追加します。

```c#:/Logics/IdentityLogic.cs
public static async Task<IdentityResult> SeedUsers (UserManager<IdentityUser> userManager,RoleManager<IdentityRole> roleManager)
{
    IdentityResult IR = null;

    if (userManager.FindByEmailAsync("admin@localhost").Result == null)
    {
        IdentityUser user = new IdentityUser();
        user.UserName = "admin@localhost";
        user.Email = "admin@localhost";
        user.EmailConfirmed = true;

        IR = userManager.CreateAsync(user, "P@ssw0rd1!").Result;

        if (IR.Succeeded)
        {
            await AddRoleToUser(userManager,roleManager,user.UserName,"admin");
        }
    }

    return IR;
}

public static async Task<IdentityResult> AddRoleToUser(UserManager<IdentityUser> userManager,
                                                        RoleManager<IdentityRole> roleManager,
                                                        string uname,
                                                        string role)
{
    IdentityResult IR = null;

    if (roleManager == null)
    {
        throw new Exception("roleManager null");
    }

    if (!await roleManager.RoleExistsAsync(role))
    {
        IR = await roleManager.CreateAsync(new IdentityRole(role));
    }

    var user = await userManager.FindByNameAsync(uname);
    
    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}
```

画面の初期表示時に上記処理を呼ぶ様にします。
本来、公式ドキュメント上ではこの様な処理をProgram.csで呼ぶのですが、どうしてもここで生成したRoleMnagerを使ってRoleを作ることができませんでした。
なので仮にHome画面の処理で呼ぶようにします。恒久対処については別途見ていきたいと思います。

```c#:/Pages/index.cshtml.cs
public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
    private readonly UserManager<IdentityUser> _userManager;
    private readonly RoleManager<IdentityRole> _roleManager;

    public IndexModel(UserManager<IdentityUser> userManager ,RoleManager<IdentityRole> roleManager ,ILogger<IndexModel> logger)
    {
        _userManager = userManager;
        _roleManager = roleManager;
        _logger = logger;
    }

    public void OnGet()
    {      
        IdentityLogic.SeedUsers(_userManager,_roleManager);
    }
}
```

これをすればとりあえず、シードユーザでログインできることが確認できます。また、DBを確認すればユーザにRoleが追加されていることもわかると思います。ただ、このままでは作ったユーザログインしていても、ログインしていなくても**できることは同じ**です。

## 認証の追加
ログイン済みのユーザのみがページを参照できる様にするためにStartup.csを変更します。

```c#:Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddControllers(config =>
    {
        // using Microsoft.AspNetCore.Authorization;
        var policy = new AuthorizationPolicyBuilder()
                        .RequireAuthenticatedUser()
                        .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });
}
```

これでログインしないとどのページもアクセスできない様になります。ちなみに、だれでもアクセスできるページを作りたい場合は下記の様にコントローラに[AllowAnonymous]属性をつけるとログインしていなくてもアクセスできます。

```c#:/Pages/Index.cshtml.cs
[AllowAnonymous]
public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
    private readonly UserManager<IdentityUser> _userManager;
    private readonly RoleManager<IdentityRole> _roleManager;

    public IndexModel(UserManager<IdentityUser> userManager ,RoleManager<IdentityRole> roleManager ,ILogger<IndexModel> logger)
    {
        _userManager = userManager;
        _roleManager = roleManager;
        _logger = logger;
    }

    public void OnGet()
    {      
        IdentityLogic.SeedUsers(_userManager,_roleManager);
    }
}
```

ただ、このままでの状態ではログインしていればどのユーザでも**すべての画面にアクセス**できてしまいます。

## 承認の追加
特定のRoleを持ったユーザのみがアクセスできる様にします。一番単純なやり方はコントローラクラスに[Authorize(Roles = "{ロール名}")]を追加するだけです。例えば、上記でスキャフォールディングした画面に承認の機能を付ける場合は下記の通りに行います。

```c#:/Pages/Divisions/Create.cshtml.cs
[Authorize(Roles = "admin")]
public class CreateModel : PageModel
{
    private readonly KosuM.Data.ApplicationDbContext _context;

    public CreateModel(KosuM.Data.ApplicationDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Division Division { get; set; }

    // To protect from overposting attacks, enable the specific properties you want to bind to, for
    // more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Division.Add(Division);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
```


Roleに"admin"が追加されていないユーザがこのページにアクセスしようとしても、
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489752/5dece3b0-6288-d591-2a35-eed55bb54eb1.png)
の画面が表示される様になります。

#まとめ
ASP.NET core Identity を利用することで結構簡単にユーザやロール管理ができることがわかりました。それぞれカスタマイズもできるので、今後の要件に合わせて活用していきたいと思います。