前面說過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 介面管理並新增或移除 User 底下的 Claim,所以今天來實作 User 介面。
首先一樣需要 ViewModel 和資料存取層,因為做的事情一樣,就不多說明了。
User 的 ViewModel。
using System.Collections.Generic;
namespace BlazorServer.ViewModels
{
public class CustomUserViewModel
{
public CustomUserViewModel()
{
Claims = new();
}
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
using System.Collections.Generic;
namespace BlazorServer.ViewModels
{
public class CustomUserClaimsViewModel
{
public CustomUserClaimsViewModel()
{
Cliams = new();
}
public string UserId { get; set; }
public List<CustomUserClaimViewModel> Cliams { get; set; }
}
}
因為 Claim 不像 User 本來就註冊了,也不像 Role 會讓使用者自己定義,所以這邊先建立好幾組跟 User 權限有關的 Claim。
using System.Collections.Generic;
using System.Security.Claims;
namespace BlazorServer.Models
{
public static class ClaimsStore
{
public static List<Claim> AllClaims = new List<Claim>()
{
new Claim("ManageUser", string.Empty),
new Claim("CreateUser", string.Empty),
new Claim("EditUser", string.Empty),
new Claim("DeleteUser", string.Empty)
};
}
}
介面IUserRepository
using BlazorServer.ViewModels;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BlazorServer.Repositories
{
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. EditUsersInRoleAsync
Post 方法的話,當時是用兩個變數分開接RoleId
及List<CustomUserRoleViewModel> model
,這邊編輯 User 下 Claim 的 Post 方法跟 Role 不同,是再用一個 ViewModel CustomUserClaimsViewModel 去裝載資料,本質上並無差別。
using BlazorServer.Models;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Identity;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace BlazorServer.Repositories.Implement
{
public class UserRepository : IUserRepository
{
private readonly UserManager<IdentityUser> _userManager;
public UserRepository(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
public async Task<List<CustomUserViewModel>> GetUsersAsync()
{
var users = _userManager.Users.ToList();
var customUsers = new List<CustomUserViewModel>();
foreach (var user in users)
{
customUsers.Add(new CustomUserViewModel { UserId = user.Id, UserName = user.UserName, Email = user.Email });
}
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)
{
CustomUserClaimViewModel userClaim = new CustomUserClaimViewModel
{
ClaimType = claim.Type
};
if (claims.Any(c => c.Type == claim.Type && c.Value == "true"))
{
userClaim.IsSelected = true;
}
model.Cliams.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.Cliams.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
};
}
}
}
再去Startup.cs
註冊
public void ConfigureServices(IServiceCollection services)
{
…
services.AddScoped<IUserRepository, UserRepository>();
}
然後就是前端畫面呈現。UserManagement.razor.cs
using BlazorServer.Repositories;
using BlazorServer.Shared;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
namespace BlazorServer.Pages.UserManagement
{
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(js);
}
private async Task loadData()
{
Users = await UserRepository.GetUsersAsync();
}
private async Task editUser(string userId)
{
NavigationManager.NavigateTo($"UserManagement/EditUser/{userId}");
}
private async Task deleteUser(string userId)
{
SweetConfirmViewModel sweetConfirm = new SweetConfirmViewModel()
{
RequestTitle = $"是否確定刪除使用者{userId}?",
RequestText = "這個動作不可復原",
ResponseTitle = "刪除成功",
ResponseText = "使用者被刪除了",
};
string jsonString = JsonSerializer.Serialize(sweetConfirm);
bool 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">
User 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.Repositories;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
using System.Threading.Tasks;
namespace BlazorServer.Pages.UserManagement
{
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.Repositories;
using BlazorServer.Shared;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.Threading.Tasks;
namespace BlazorServer.Pages.UserManagement
{
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(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.Cliams)
{
<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 介面了。
Ref:Manage user claims in asp net core
Ref:Claim type and claim value in claims policy based authorization in asp net core