Visual

15 用户管理.mp4 (117.94MB) 注:

  • 从这一讲开始的源码,视频作者杨旭大神都放在了自己的 GitHub
  • 我也将我学习的代码放到了 GitHub

    一句话的事儿

    • 如何管理网站用户(CRUD:创建,删除,更新,查询)
    • 如何验证用户(前端UI对用户的验证,通过DataAnnotation)

准备工作

  1. GitHub 下载源码
  2. 打开 15 start 项目
  3. 打开程序包管理控制台,更新数据库
    1. Update-Database -Context HeavyContext
    2. Update-Database -Context ``ApplicationDbContext

使用 Identity—用户管理(IdentityManager)

创建项目

创建项目使用 ASP.NET Core MVC 模板 + 身份认证选择 个人。

自动生成的 Startup 里面的部分代码就启用了 Identity:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. ...
  4. services.AddDefaultIdentity<IdentityUser>()
  5. .AddDefaultUI(UIFramework.Bootstrap4)
  6. .AddEntityFrameworkStores<ApplicationDbContext>();
  7. ...
  8. }
  9. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  10. {
  11. ...
  12. app.UseCookiePolicy();
  13. app.UseAuthentication();
  14. ...
  15. }

UserController

通过依赖注入的 UserManager 实现对 User 的操控。

  1. using System.Threading.Tasks;
  2. using Heavy.Web.Models;
  3. using Heavy.Web.ViewModels;
  4. using Microsoft.AspNetCore.Authorization;
  5. using Microsoft.AspNetCore.Identity;
  6. using Microsoft.AspNetCore.Mvc;
  7. using Microsoft.EntityFrameworkCore;
  8. namespace Heavy.Web.Controllers
  9. {
  10. [Authorize]
  11. public class UserController : Controller
  12. {
  13. private readonly UserManager<ApplicationUser> _userManager;
  14. public UserController(UserManager<ApplicationUser> userManager)
  15. {
  16. _userManager = userManager;
  17. }
  18. /// <summary>
  19. /// 获取所有用户
  20. /// </summary>
  21. /// <returns></returns>
  22. public async Task<IActionResult> Index()
  23. {
  24. var users = await _userManager.Users.ToListAsync();
  25. return View(users);
  26. }
  27. /// <summary>
  28. /// 增加一个用户[HttpGet],其目的是防止刷新后反复添加用户,从而用于Post返回
  29. /// </summary>
  30. /// <returns></returns>
  31. public IActionResult AddUser()
  32. {
  33. return View();
  34. }
  35. /// <summary>
  36. /// 增加一个用户[HttpPost]
  37. /// </summary>
  38. /// <param name="userAddViewModel">创建用户的UI</param>
  39. /// <returns></returns>
  40. [HttpPost]
  41. public async Task<IActionResult> AddUser(UserCreateViewModel userAddViewModel)
  42. {
  43. //Model验证,若失败返回到Action,即AddUser
  44. if (!ModelState.IsValid)
  45. {
  46. return View(userAddViewModel);
  47. }
  48. //创建用户并赋值相关属性,其中密码使用_userManager.CreateAsync()
  49. var user = new ApplicationUser
  50. {
  51. UserName = userAddViewModel.UserName,
  52. Email = userAddViewModel.Email,
  53. IdCardNo = userAddViewModel.IdCardNo,
  54. BirthDate = userAddViewModel.BirthDate
  55. };
  56. var result = await _userManager.CreateAsync(user, userAddViewModel.Password);
  57. //验证创建是否成功
  58. if (result.Succeeded)
  59. {
  60. return RedirectToAction("Index");
  61. }
  62. //将错误藐视输出
  63. foreach (IdentityError error in result.Errors)
  64. {
  65. ModelState.AddModelError(string.Empty, error.Description);
  66. }
  67. return View(userAddViewModel);
  68. }
  69. /// <summary>
  70. /// 编辑用户[HttpGet]
  71. /// </summary>
  72. /// <param name="id">用户Id</param>
  73. /// <returns></returns>
  74. public async Task<IActionResult> EditUser(string id)
  75. {
  76. var user = await _userManager.FindByIdAsync(id);
  77. if (user == null)
  78. {
  79. return RedirectToAction("Index");
  80. }
  81. return View(user);
  82. }
  83. /// <summary>
  84. /// 编辑用户[HttpPost]
  85. /// </summary>
  86. /// <param name="id">用户Id</param>
  87. /// <param name="userEditViewModel">编辑用户的UI</param>
  88. /// <returns></returns>
  89. [HttpPost]
  90. public async Task<IActionResult> EditUser(string id, UserEditViewModel userEditViewModel)
  91. {
  92. var user = await _userManager.FindByIdAsync(id);
  93. if (user == null)
  94. {
  95. return RedirectToAction("Index");
  96. }
  97. user.UserName = userEditViewModel.UserName;
  98. user.Email = userEditViewModel.Email;
  99. user.IdCardNo = userEditViewModel.IdCardNo;
  100. user.BirthDate = userEditViewModel.BirthDate;
  101. var result = await _userManager.UpdateAsync(user);
  102. if (result.Succeeded)
  103. {
  104. return RedirectToAction("Index");
  105. }
  106. ModelState.AddModelError(string.Empty, "更新用户信息时发生错误");
  107. return View(user);
  108. }
  109. /// <summary>
  110. /// 删除用户[HttpPost]
  111. /// </summary>
  112. /// <param name="id"></param>
  113. /// <returns></returns>
  114. [HttpPost]
  115. public async Task<IActionResult> DeleteUser(string id)
  116. {
  117. var user = await _userManager.FindByIdAsync(id);
  118. if (user != null)
  119. {
  120. var result = await _userManager.DeleteAsync(user);
  121. if (result.Succeeded)
  122. {
  123. return RedirectToAction("Index");
  124. }
  125. ModelState.AddModelError(string.Empty, "删除用户时发生错误");
  126. }
  127. else
  128. {
  129. ModelState.AddModelError(string.Empty, "用户找不到");
  130. }
  131. return View("Index", await _userManager.Users.ToListAsync());
  132. }
  133. }
  134. }

UserXxViewModel

User 的各种视图模型,通过特性标注实现基本的验证和显式设置:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel.DataAnnotations;
  4. using System.Linq;
  5. using System.Threading.Tasks;
  6. namespace Heavy.Web.ViewModels
  7. {
  8. /// <summary>
  9. /// 创建User的前端UI
  10. /// </summary>
  11. public class UserCreateViewModel
  12. {
  13. [Required]
  14. [Display(Name = "用户名")]
  15. public string UserName { get; set; }
  16. //[Required]
  17. [DataType(DataType.EmailAddress)]
  18. [RegularExpression(@"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|""(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*"")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])", ErrorMessage = "Email的格式不正确")]
  19. public string Email { get; set; }
  20. [Required]
  21. [DataType(DataType.Password)]
  22. public string Password { get; set; }
  23. [Required]
  24. [Display(Name = "身份证号")]
  25. [StringLength(18, MinimumLength = 18, ErrorMessage = "{0}的长度是{1}")]
  26. public string IdCardNo { get; set; }
  27. [Required]
  28. [Display(Name = "出生日期")]
  29. [DataType(DataType.Date)]
  30. public DateTime BirthDate { get; set; }
  31. }
  32. }

扩展 IdentityUser

查看 IdentityUser 的源码,不难发现它的属性并不多。我们可以通过继承它来创建属性更丰富的 IdentityUser。
Identity源码

  1. #region Assembly Microsoft.Extensions.Identity.Stores, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  2. // C:\Users\Felix\.nuget\packages\microsoft.extensions.identity.stores\2.2.0\lib\netstandard2.0\Microsoft.Extensions.Identity.Stores.dll
  3. #endregion
  4. using System;
  5. namespace Microsoft.AspNetCore.Identity
  6. {
  7. //
  8. // Summary:
  9. // Represents a user in the identity system
  10. //
  11. // Type parameters:
  12. // TKey:
  13. // The type used for the primary key for the user.
  14. public class IdentityUser<TKey> where TKey : IEquatable<TKey>
  15. {
  16. //
  17. // Summary:
  18. // Initializes a new instance of Microsoft.AspNetCore.Identity.IdentityUser`1.
  19. public IdentityUser();
  20. //
  21. // Summary:
  22. // Initializes a new instance of Microsoft.AspNetCore.Identity.IdentityUser`1.
  23. //
  24. // Parameters:
  25. // userName:
  26. // The user name.
  27. public IdentityUser(string userName);
  28. //
  29. // Summary:
  30. // Gets or sets the date and time, in UTC, when any user lockout ends.
  31. //
  32. // Remarks:
  33. // A value in the past means the user is not locked out.
  34. public virtual DateTimeOffset? LockoutEnd { get; set; }
  35. //
  36. // Summary:
  37. // Gets or sets a flag indicating if two factor authentication is enabled for this
  38. // user.
  39. //
  40. // Value:
  41. // True if 2fa is enabled, otherwise false.
  42. [PersonalData]
  43. public virtual bool TwoFactorEnabled { get; set; }
  44. //
  45. // Summary:
  46. // Gets or sets a flag indicating if a user has confirmed their telephone address.
  47. //
  48. // Value:
  49. // True if the telephone number has been confirmed, otherwise false.
  50. [PersonalData]
  51. public virtual bool PhoneNumberConfirmed { get; set; }
  52. //
  53. // Summary:
  54. // Gets or sets a telephone number for the user.
  55. [ProtectedPersonalData]
  56. public virtual string PhoneNumber { get; set; }
  57. //
  58. // Summary:
  59. // A random value that must change whenever a user is persisted to the store
  60. public virtual string ConcurrencyStamp { get; set; }
  61. //
  62. // Summary:
  63. // A random value that must change whenever a users credentials change (password
  64. // changed, login removed)
  65. public virtual string SecurityStamp { get; set; }
  66. //
  67. // Summary:
  68. // Gets or sets a salted and hashed representation of the password for this user.
  69. public virtual string PasswordHash { get; set; }
  70. //
  71. // Summary:
  72. // Gets or sets a flag indicating if a user has confirmed their email address.
  73. //
  74. // Value:
  75. // True if the email address has been confirmed, otherwise false.
  76. [PersonalData]
  77. public virtual bool EmailConfirmed { get; set; }
  78. //
  79. // Summary:
  80. // Gets or sets the normalized email address for this user.
  81. public virtual string NormalizedEmail { get; set; }
  82. //
  83. // Summary:
  84. // Gets or sets the email address for this user.
  85. [ProtectedPersonalData]
  86. public virtual string Email { get; set; }
  87. //
  88. // Summary:
  89. // Gets or sets the normalized user name for this user.
  90. public virtual string NormalizedUserName { get; set; }
  91. //
  92. // Summary:
  93. // Gets or sets the user name for this user.
  94. [ProtectedPersonalData]
  95. public virtual string UserName { get; set; }
  96. //
  97. // Summary:
  98. // Gets or sets the primary key for this user.
  99. [PersonalData]
  100. public virtual TKey Id { get; set; }
  101. //
  102. // Summary:
  103. // Gets or sets a flag indicating if the user could be locked out.
  104. //
  105. // Value:
  106. // True if the user could be locked out, otherwise false.
  107. public virtual bool LockoutEnabled { get; set; }
  108. //
  109. // Summary:
  110. // Gets or sets the number of failed login attempts for the current user.
  111. public virtual int AccessFailedCount { get; set; }
  112. //
  113. // Summary:
  114. // Returns the username for this user.
  115. public override string ToString();
  116. }
  117. }

添加了身份证号和出生日期的 ApplicationUser:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel.DataAnnotations;
  4. using System.Linq;
  5. using System.Threading.Tasks;
  6. using Microsoft.AspNetCore.Identity;
  7. namespace Heavy.Web.Models
  8. {
  9. public class ApplicationUser: IdentityUser
  10. {
  11. //添加身份证号码,并通过DataAnnotations验证
  12. [MaxLength(18)]
  13. public string IdCardNo { get; set; }
  14. //添加出生日期,并通过DataAnnotations验证
  15. [DataType(DataType.Date)]
  16. public DateTime BirthDate { get; set; }
  17. }
  18. }

然后将 Configure Services 里面的代码:services.AddDefaultIdentity<IdentityUser> 修改为 services.AddDefaultIdentity<ApplicationUser>

然后修改 ApplicationDbContext 指明 IdentityUser 的实现类为 ApplicationUser:
此处的原因是IdentityUser的继承类ApplicationUser是来自于IdentityDbContext,与此同时也能够让ApplicationUser能够被EF Core下的DbContext追踪。

  1. public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
  2. {
  3. ...
  4. }

然后 Add-Migration + Update-Database 更新数据库。

最后将程序中所有原来使用 IdentityUser 的地方替换为 ApplicationUser。

自定义密码规则

Identity 默认要求用户设置复杂的强密码,我们可以通过 IdentityOptions 自定义密码规则。

  1. services.AddDefaultIdentity<ApplicationUser>(options =>
  2. {
  3. options.Password.RequireNonAlphanumeric = false;
  4. options.Password.RequireLowercase = false;
  5. options.Password.RequireUppercase = false;
  6. options.Password.RequiredLength = 6;
  7. })
  8. .AddDefaultUI(UIFramework.Bootstrap4)
  9. .AddEntityFrameworkStores<ApplicationDbContext>();

补充官网源码

https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/identity-configuration?view=aspnetcore-3.1

锁定—Lockout

Identity 选项。锁定将指定LockoutOptions ,其中包含表中所示的属性。
表 2

属性 说明 默认
AllowedForNewUsers 确定新用户是否可以锁定。 true
DefaultLockoutTimeSpan 锁定发生时用户被锁定的时间长度。 5 分钟
MaxFailedAccessAttempts 如果启用了锁定,则在用户被锁定之前失败的访问尝试次数。 5

密码—Password

Identity Options. Password指定PasswordOptions ,其中包含表中所示的属性。
表 3

属性 说明 默认
RequireDigit 要求密码中的数字介于0-9 之间。 true
RequiredLength 密码的最小长度。 6
RequireLowercase 密码中需要小写字符。 true
RequireNonAlphanumeric 密码中需要一个非字母数字字符。 true
RequiredUniqueChars 仅适用于 ASP.NET Core 2.0 或更高版本。
需要密码中的非重复字符数。
1
RequireUppercase 密码中需要大写字符。 true

登录—SignIn

Identity 登录“ 指定SignInOptions ,其中包含表中所示的属性。
登录

属性 说明 默认
RequireConfirmedEmail 需要确认电子邮件登录。 false
RequireConfirmedPhoneNumber 需要确认电话号码才能登录。 false

令牌—Token

Identity 选项。标记指定TokenOptions ,其中包含表中所示的属性。
令牌牌

属性 说明
AuthenticatorTokenProvider 获取或设置 AuthenticatorTokenProvider 用于使用验证器验证双重登录的。
ChangeEmailTokenProvider 获取或设置 ChangeEmailTokenProvider 用于生成电子邮件更改确认电子邮件中使用的令牌的。
ChangePhoneNumberTokenProvider 获取或设置 ChangePhoneNumberTokenProvider 用于生成更改电话号码时使用的令牌的。
EmailConfirmationTokenProvider 获取或设置用于生成帐户确认电子邮件中使用的令牌的令牌提供程序。
PasswordResetTokenProvider 获取或设置用于生成密码重置电子邮件中使用的令牌的IUserTwoFactorTokenProvider
ProviderMap 用于使用用作提供程序名称的密钥构造 用户令牌提供程序

用户—User

Identity Options。 User指定UserOptions ,其中包含表中所示的属性。
用户

属性 说明 默认
AllowedUserNameCharacters 用户名中允许使用的字符。 abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
-._@+
RequireUniqueEmail 要求每个用户都有唯一的电子邮件。 false

Cookie 设置

  1. services.ConfigureApplicationCookie(options =>
  2. {
  3. options.AccessDeniedPath = "/Identity/Account/AccessDenied";
  4. options.Cookie.Name = "YourAppCookieName";
  5. options.Cookie.HttpOnly = true;
  6. options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
  7. options.LoginPath = "/Identity/Account/Login";
  8. // ReturnUrlParameter requires
  9. //using Microsoft.AspNetCore.Authentication.Cookies;
  10. options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
  11. options.SlidingExpiration = true;
  12. });

Password Hasher 选项

PasswordHasherOptions 获取和设置用于密码哈希的选项。
PASSWORD HASHER 选项

选项 说明
CompatibilityMode 对新密码进行哈希处理时使用的兼容性模式。 默认为 IdentityV3。 哈希密码的第一个字节称为 格式标记,它指定用于对密码进行哈希处理的哈希算法的版本。 针对哈希验证密码时,该方法会根据 VerifyHashedPassword 第一个字节选择正确的算法。 无论使用哪个版本的算法对密码进行哈希处理,客户端都可以进行身份验证。 设置兼容性模式会影响 新密码的哈希。
IterationCount 使用 PBKDF2 对密码进行哈希处理时使用的迭代次数。 仅当设置为时,才使用此值 CompatibilityMode IdentityV3 。 该值必须是正整数并且默认值为 10000