Day27 建立使用者及Claim功能

前面说过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 方法的话,当时是用两个变数分开接RoleIdList<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


<<:  Day12. UX/UI 设计流程之二: Flow Chart (以 Axure RP 实作)

>>:  27. 移转 Aras PLM大小事 - 额外编码取号

[Java Day19] 4.7. 静态化

教材网址 https://coding104.blogspot.com/2021/06/java-s...

Day19有比较有伤害

plotly.js 既然前两天介绍的PivotTable.js是支援plotly.js,那麽我们就...

每日挑战,从Javascript面试题目了解一些你可能忽略的概念 - Day10

tags: ItIron2021 Javascript 前言 终於迈入第10天啦! 我们昨天讲完资料...

DAY25 - 自学就像瞎子摸象,在未知的情况下试图拼出原貌

前言 记得以前在学校的时候修过资料结构与演算法,考试也都会考时间复杂度等相关的题目 毕业後,在公司上...

网路是怎样连接的(十一) 初探IP协议

思考重点 封包是如何找到下一个端节点的 IP地址在封包转发中扮演的角色 MAC地址在封包转发中扮演的...