前に述べたように、ASP.NET Core Identity は Claim に基づく認証であり、Role は型が Role の Claim にすぎません。ASP.NET Framework Identity の時代には Role 認証しかありませんでしたが、Claim は ASP.NET Core Identity で初めて登場したもので、Facebook、Twitter などの外部プログラムのサードパーティ認可を取得するためのものです。これにより、ユーザーは異なるプラットフォームで同じアカウントを登録する必要がなくなります。
そして Claim は実際には単なる ClaimType と ClaimValue の文字列の組み合わせであり、通常は Role のように管理ページでユーザーに割り当てるのではなく、User ページで管理し、User 配下の Claim を追加・削除します。そこで今日は User ページを実装します。
まず同様に ViewModel とデータアクセス層が必要です。やっていることは同じなので、詳細は割愛します。
User の ViewModel。
namespace BlazorServer.ViewModels;
public class CustomUserViewModel
{
public CustomUserViewModel()
{
Claims = new List<string>();
}
public string? UserId { get; set; }
public string? UserName { get; set; }
public string? Email { get; set; }
public List<string>? Claims { get; set; }
}
単一の Claim を保持する ViewModel
namespace BlazorServer.ViewModels;
public class CustomUserClaimViewModel
{
public string? ClaimType { get; set; }
public bool IsSelected { get; set; }
}
User 配下の Claim を保持する ViewModel
namespace BlazorServer.ViewModels;
public class CustomUserClaimsViewModel
{
public CustomUserClaimsViewModel()
{
Claims = new List<CustomUserClaimViewModel>();
}
public string? UserId { get; set; }
public List<CustomUserClaimViewModel> Claims { get; set; }
}
Claim は User のように最初から登録されているわけでもなく、Role のようにユーザーが自由に定義するものでもないため、ここではあらかじめ User 権限に関連するいくつかの Claim を用意します。
using System.Security.Claims;
namespace BlazorServer.Models;
public static class ClaimsStore
{
public static List<Claim> AllClaims = new()
{
new Claim("ManageUser", string.Empty),
new Claim("CreateUser", string.Empty),
new Claim("EditUser", string.Empty),
new Claim("DeleteUser", string.Empty)
};
}
ページ IUserRepository
using BlazorServer.Models;
using BlazorServer.ViewModels;
namespace BlazorServer.Repository;
public interface IUserRepository
{
Task<ResultViewModel> DeleteUserAsync(string userId);
Task<ResultViewModel> EditUserAsync(CustomUserViewModel model);
Task<CustomUserViewModel> GetUserAsync(string userId);
Task<List<CustomUserViewModel>> GetUsersAsync();
Task<CustomUserClaimsViewModel> EditClaimsInUserAsync(string userId);
Task<ResultViewModel> EditClaimsInUserAsync(CustomUserClaimsViewModel model);
}
UserRepository の実装です。RoleRepository.EditUsersInRoleAsyncPost メソッドを覚えていれば、当時は Role.Id と List<CustomUserRoleViewModel> model を別々の変数に格納していました。ここでの User 配下の Claim を編集する Post メソッドは Role とは異なり、別途 ViewModel CustomUserClaimsViewModel を使用してデータを保持しますが、本質的には違いはありません。
using System.Security.Claims;
using BlazorServer.Models;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Identity;
namespace BlazorServer.Repository.Implement;
public class UserRepository : IUserRepository
{
private readonly UserManager<IdentityUser> _userManager;
public UserRepository(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
public async Task<List<CustomUserViewModel>> GetUsersAsync()
{
var customUsers = _userManager.Users.Select(user => new CustomUserViewModel
{ UserId = user.Id, UserName = user.UserName, Email = user.Email }).ToList();
return await Task.Run(() => customUsers);
}
public async Task<CustomUserViewModel> GetUserAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
var userClaims = await _userManager.GetClaimsAsync(user);
var result = new CustomUserViewModel
{
UserId = user.Id,
UserName = user.UserName,
Email = user.Email,
Claims = userClaims.Select(x => $"{x.Type} : {x.Value}").ToList()
};
return result;
}
public async Task<ResultViewModel> EditUserAsync(CustomUserViewModel model)
{
var user = await _userManager.FindByIdAsync(model.UserId);
if (user == null)
{
return new ResultViewModel
{
Message = $"ID {model.UserId} のユーザーが見つかりません",
IsSuccess = false
};
}
user.UserName = model.UserName;
user.Email = model.Email;
var result = await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
return new ResultViewModel
{
Message = "ユーザーを更新しました!",
IsSuccess = true
};
}
return new ResultViewModel
{
Message = "ユーザーの更新に失敗しました!",
IsSuccess = false
};
}
public async Task<ResultViewModel> DeleteUserAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return new ResultViewModel
{
Message = $"ID {userId} のユーザーが見つかりません",
IsSuccess = false
};
}
var result = await _userManager.DeleteAsync(user);
if (result.Succeeded)
{
return new ResultViewModel
{
Message = "ユーザーを削除しました!",
IsSuccess = true
};
}
return new ResultViewModel
{
Message = "ユーザーの削除に失敗しました!",
IsSuccess = false
};
}
public async Task<CustomUserClaimsViewModel> EditClaimsInUserAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
var claims = await _userManager.GetClaimsAsync(user);
var model = new CustomUserClaimsViewModel
{
UserId = userId
};
foreach (var claim in ClaimsStore.AllClaims)
{
var userClaim = new CustomUserClaimViewModel
{
ClaimType = claim.Type
};
if (claims.Any(c => c.Type == claim.Type && c.Value == "true"))
{
userClaim.IsSelected = true;
}
model.Claims.Add(userClaim);
}
return model;
}
public async Task<ResultViewModel> EditClaimsInUserAsync(CustomUserClaimsViewModel model)
{
var user = await _userManager.FindByIdAsync(model.UserId);
var claims = await _userManager.GetClaimsAsync(user);
var result = await _userManager.RemoveClaimsAsync(user, claims);
if (!result.Succeeded)
{
return new ResultViewModel
{
Message = "ユーザーの Claim を削除できません!",
IsSuccess = false
};
}
result = await _userManager.AddClaimsAsync(user,
model.Claims.Select(c => new Claim(c.ClaimType!, c.IsSelected ? "true" : "false")));
if (!result.Succeeded)
{
return new ResultViewModel
{
Message = "指定された Claim をユーザーに割り当てられません!",
IsSuccess = false
};
}
return new ResultViewModel
{
Message = "Claim の割り当てに成功しました",
IsSuccess = true
};
}
}
Program.cs で登録します。
builder.Services.AddScoped<IUserRepository, UserRepository>();
次にフロントエンドのページを表示します。
UserManagement.razor.cs
using BlazorServer.Repository;
using BlazorServer.Shared;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace BlazorServer.Pages.RolesManagement;
public partial class UserManagement
{
[Inject] protected IUserRepository? UserRepository { get; set; }
[Inject] protected NavigationManager? NavigationManager { get; set; }
[Inject] protected IJSRuntime? Js { get; set; }
private JsInteropClasses? _jsClass;
public List<CustomUserViewModel> Users { get; set; } = new();
protected override async Task OnInitializedAsync()
{
await LoadData();
_jsClass = new JsInteropClasses(Js!);
}
private async Task LoadData()
{
Users = await UserRepository!.GetUsersAsync();
}
private async Task EditUser(string userId)
{
NavigationManager!.NavigateTo($"UserManagement/EditUser/{userId}");
await Task.CompletedTask;
}
private async Task DeleteUser(string userId)
{
var sweetConfirm = new SweetConfirmViewModel()
{
RequestTitle = $"ユーザー {userId} を削除してもよろしいですか?",
RequestText = "この操作は元に戻せません",
ResponseTitle = "削除成功",
ResponseText = "ユーザーが削除されました",
};
var jsonString = JsonSerializer.Serialize(sweetConfirm);
var result = await _jsClass!.Confirm(jsonString);
if (result)
{
var deleted = await UserRepository!.DeleteUserAsync(userId);
if (deleted.IsSuccess)
{
await LoadData();
}
else
{
await _jsClass!.Alert(deleted.Message!);
}
}
}
}
UserManagement.razor
@page "/UserManagement/UserList"
<h1>すべてのユーザー</h1>
@if (Users.Any()) {
<NavLink
class="btn btn-primary mb-3"
href="Identity/Account/Register"
Match="NavLinkMatch.All"
>
新規ユーザー追加
</NavLink>
foreach (var user in Users) {
<div class="card mb-3 w-25">
<div class="card-header">ユーザーID : @user.UserId</div>
<div class="card-body">
<h5 class="card-title">@user.UserName</h5>
</div>
<div class="card-footer">
<button
type="button"
class="btn btn-primary"
@onclick="() => EditUser(user.UserId)"
>
ユーザー編集
</button>
<button
type="button"
class="btn btn-danger"
@onclick="() => DeleteUser(user.UserId)"
>
ユーザー削除
</button>
</div>
</div>
} } else {
<div class="card w-25">
<div class="card-header">まだユーザーがいません</div>
<div class="card-body">
<h5 class="card-title">下のボタンをクリックしてユーザーを追加</h5>
<NavLink
class="btn btn-primary"
href="Identity/Account/Register"
Match="NavLinkMatch.All"
>
新規ユーザー追加
</NavLink>
</div>
</div>
}
EditUser.razor.cs
using BlazorServer.Repository;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
namespace BlazorServer.Pages.RolesManagement;
public partial class EditUser
{
[Inject] protected IUserRepository? UserRepository { get; set; }
[Inject] protected NavigationManager? NavigationManager { get; set; }
public CustomUserViewModel User { get; set; } = new();
[Parameter] public string? UserId { get; set; }
protected override async Task OnInitializedAsync()
{
var result = await UserRepository!.GetUserAsync(UserId!);
User = new CustomUserViewModel
{
UserId = result.UserId,
UserName = result.UserName,
Claims = result.Claims
};
}
private async Task EditRole()
{
await UserRepository!.EditUserAsync(User);
NavigationManager!.NavigateTo("/UserManagement/UserList");
}
public void EditUsersInRole()
{
NavigationManager!.NavigateTo($"/UserManagement/EditClaimsInUser/{UserId}");
}
public void Cancel()
{
NavigationManager!.NavigateTo($"/UserManagement/UserList");
}
}
EditUser.razor
@page "/UserManagement/EditUser/{UserId}"
<EditForm class="mt-3" Model="User" OnValidSubmit="EditRole">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group row">
<label for="RoleName" class="col-sm-1 col-form-label">ユーザー名</label>
<div class="col-sm-3">
<InputText
@bind-Value="User.UserName"
id="RoleName"
class="form-control"
placeholder="ユーザー名"
></InputText>
</div>
</div>
<div class="card mb-3 w-50">
<div class="card-header">
<h3>ユーザー配下の Claim</h3>
</div>
<div class="card-body">
@if (User.Claims.Any()) { foreach (var claim in User.Claims) {
<h5 class="card-title">@claim</h5>
} } else {
<h5 class="card-title">現在このユーザーに Claim はありません</h5>
}
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">ユーザー更新</button>
<button type="button" class="btn btn-info" @onclick="EditUsersInRole">
このユーザーの Claim を追加/削除
</button>
<button type="button" class="btn btn-danger" @onclick="Cancel">
キャンセル
</button>
</div>
</div>
</EditForm>
EditClaimsInUser.razor.cs
using BlazorServer.Repository;
using BlazorServer.Shared;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace BlazorServer.Pages.RolesManagement;
public partial class EditClaimsInUser
{
[Inject] protected IUserRepository? UserRepository { get; set; }
[Inject] protected NavigationManager? NavigationManager { get; set; }
[Inject] protected IJSRuntime? Js { get; set; }
private JsInteropClasses? _jsClass;
[Parameter] public string? UserId { get; set; }
public CustomUserClaimsViewModel UserClaimViewModel { get; set; } = new CustomUserClaimsViewModel();
protected override async Task OnInitializedAsync()
{
await LoadData();
_jsClass = new JsInteropClasses(Js!);
}
private async Task LoadData()
{
UserClaimViewModel = (await UserRepository!.EditClaimsInUserAsync(UserId!));
}
public async Task HandleValidSubmit()
{
var result = await UserRepository!.EditClaimsInUserAsync(UserClaimViewModel);
if (result.IsSuccess)
{
NavigationManager!.NavigateTo($"/UserManagement/EditUser/{UserId}");
}
else
{
await _jsClass!.Alert(result.Message!);
}
}
public void Cancel()
{
NavigationManager!.NavigateTo($"/UserManagement/EditUser/{UserId}");
}
}
EditClaimsInUser.razor
@page "/UserManagement/EditClaimsInUser/{UserId}"
<EditForm Model="UserClaimViewModel" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="card">
<div class="card-header">
<h2>ユーザーから Claim を追加または削除</h2>
</div>
<div class="card-body">
@foreach (var claim in UserClaimViewModel.Claims) {
<div class="form-check m-1">
<label class="form-check-label">
<InputCheckbox @bind-Value="@claim.IsSelected"></InputCheckbox>
@claim.ClaimType
</label>
</div>
}
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">更新</button>
<button type="button" class="btn btn-danger" @onclick="@Cancel">
キャンセル
</button>
</div>
</div>
</EditForm>
最後に NavMenu.razor に NavLink を追加します。
<li class="nav-item px-3">
<NavLink
class="nav-link"
href="UserManagement/UserList"
Match="NavLinkMatch.All"
>
<span class="bi bi-people h4 p-2 mb-0" aria-hidden="true"></span> Users
</NavLink>
</li>
これで、User と Claim の簡単な CRUD ページができました。
参照:
- Manage user claims in asp net core
- Claim type and claim value in claims policy based authorization in asp net core
注:本記事のコードは .NET 6 + Visual Studio 2022 でリファクタリングされています。原文のリンクとリファクタリング後のコードを比較しながら学習してください。お読みいただきありがとうございます。原作者をサポートしてください。