前面说过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
。
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">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.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 重构,可点击原文链接与重构后代码比较学习,谢谢阅读,支持原作者