(27/30) Everyone learn Blazor together: Add users and Claim functions

(27/30) Everyone learn Blazor together: Add users and Claim functions

As mentioned earlier,`ASP.NET Core Identity` is a verification based on `Claim`, and `Role` is a `Claim` of type `Role`

最后更新 12/25/2021 11:08 AM
StrayaWorker
预计阅读 10 分钟
分类
Blazor
专题
Learn the Blazor series together
标签
.NET C# ASP.NET Core Blazor

前面说过ASP.NET Core Identity 是基于Claim 的验证,而Role 就是类型为RoleClaimASP.NET Framework Identity 时代只有Role 验证,ClaimASP.NET Core Identity 才出现的,目的是为了取得外部程序如FacebookTwitter 等等第三方的授权,如此一来用户就不用在不同平台注册重复账号。

Claim 其实就只是一组ClaimTypeClaimValue 的字符串组合,通常不会像Role 用一个页面去管理并指派给User,而是以User 页面管理并新增或移除User 底下的Claim,所以今天来实现User 页面。

首先一样需要ViewModel 和数据存取层,因为做的事情一样,就不多说明了。

UserViewModel

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; }
}

承载单一ClaimViewModel

namespace BlazorServer.ViewModels;

public class CustomUserClaimViewModel
{
	public string? ClaimType { get; set; }
	public bool IsSelected { get; set; }
}

承载UserClaimViewModel

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.IdList<CustomUserRoleViewModel> model,这边编辑UserClaimPost 方法跟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>();

Then there is front-end page presentation.

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>

这样就有简单的UserClaimCRUD 页面了。

引用:

  1. Manage user claims in asp net core
  2. Claim type and claim value in claims policy based authorization in asp net core

** Note: The code in this article is refactored through. NET 6 + Visual Studio 2022. You can click on the original link to compare and learn the refactored code. Thank you for reading and support the original author **

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 12/25/2021

(29/30) Everyone learn Blazor together: Blazor unit testing

Probably the most boring process of developing a system is to solve bugs, especially the error of trying to value null objects (`Object reference not set to an instance of an object.`). This should be the most common problem that most people encounter when they first step into the programming field. In order to relieve themselves from the boring process of solving bugs, this article introduces 'unit testing'.

继续阅读
同分类 / 同标签 12/25/2021

(28/30) Everyone learns Blazor together: Policy-based authorization

It was mentioned before that 'ASP.NET Core Identity' uses 'Claim' based authentication. In fact,'ASP.NET Core Identity' has different types of authorization methods, the simplest are 'login authorization','role authorization', and 'Claim authorization', but all of the above are implemented in one way: 'Policy-based authorization'.

继续阅读