iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0
自我挑戰組

ASP.NET Core & Blazor系列 第 27

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


上一篇
Day26 指派角色給使用者
下一篇
Day28 Policy-based authorization
系列文
ASP.NET Core & Blazor30

尚未有邦友留言

立即登入留言