快速掌握 ASP.NET 身份认证框架 Identity - 用户注册

这是 ASP.NET Core Identity 系列的第二篇文章,上一篇文章介绍了 Identity 框架的集成,以及一些基础知识

这篇文章讲一讲如何在 ASP.NET Core Identity 中实现用户注册。

点击上方或后方蓝字,阅读 ASP.NET Core Identity 系列合集。.

本篇文章的示例项目:https://github.com/zilor-net/IdentitySample/tree/main/Sample02

快速掌握 ASP.NET 身份认证框架 Identity - 用户注册

准备工作

ASP.NET Core Identity 提供了很多不同的身份选项,帮助我们实现用户注册。

首先,让我们在 「Models」 文件夹中,创建一个用户注册模型:

public class UserRegistrationModel
{
    [Display(Name = "姓氏")]
    public string FirstName { get; set; }

    [Display(Name = "名字")]
    public string LastName { get; set; }

    [Display(Name = "电子邮箱")]
    [Required(ErrorMessage = "电子邮箱不能为空")]
    [EmailAddress]
    public string Email { get; set; }

    [Display(Name = "密码")]
    [Required(ErrorMessage = "密码不能为空")]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [Display(Name = "确认密码")]
    [DataType(DataType.Password)]
    [Compare("Password", ErrorMessage = "密码与确认密码不匹配。")]
    public string ConfirmPassword { get; set; }
}

这个类中的属性,需要用户在注册表单中填写。

其中,Email 和 Password 属性是必需的,ConfirmPassword 属性的值必须与 Password 属性的值匹配。

有了这个模型,我们再来创建一个 「Account」 控制器:

public class AccountController : Controller
{
    [HttpGet]
    public IActionResult Register()
    {
        return View();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Register(UserRegistrationModel userModel)
    {
        return View();
    }
}

它有两个 Register 操作方法,用来提供用户注册功能。

第一个 Register 方法接受 Get 请求,用来显示注册表单的视图;

第二个 Register 方法接受 Post 请求,用来处理用户注册逻辑,并显示注册结果的视图。

还需要为 GET Register 操作,创建一个视图,这里我们可以直接使用 Create 模板,模型类选择 UserRegistrationModel 类即可。

快速掌握 ASP.NET 身份认证框架 Identity - 用户注册

现在这个视图中的表单,已经可以为用户注册模型,提供所有的输入字段了。

需要注意的是,我们最好修改表单中 asp-validation-summary 的值为 All

因为,默认情况下,它只能显示模型验证产生的错误信息,而不会显示我们自己设置的验证错误。

视图中的 Create 按钮,会跳转到 POST 请求的 Register 操作方法,并且填充用户注册模型。

在 Web API 中使用来自用户的数据,最好采用数据传输对象的方式,但是在 MVC 中不一定如此。

这是因为 MVC 有模型的存在,在有视图的情况下,模型在一定程度上,替代了数据传输对象职责。

由于我们的数据通过视图传递,因此该类模型也被称为视图模型。

「UserRegistrationModel」 就是一个视图模型,如果我们想要真正的在数据库中存储它,必须要把它映射到 User 实体类。

我们需要安装 AutoMapper :

Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection

然后,在 Program 类中注册它:

builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());

接着,在布局页的导航菜单上,添加一个注册按钮。

因为后面还要添加登录按钮,所以为了代码的可维护性,我们可以创建一个分部视图:

// Shared\_LoginPartial.cshtml

<ul class="navbar-nav">
    <li class="nav-item">
        <a class="nav-link text-dark" asp-controller="Account" 
               asp-action="Register">注册</a>    
    </li>
</ul>

最后,修改 「_Layout.cshtml」 布局视图,在导航菜单上添加登录分布视图:

<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
    <partial name="_LoginPartial" /><!-- 添加 -->
    <ul class="navbar-nav flex-grow-1"> 

注册操作

准备工作都已经完成了。

现在,让我们从用户注册逻辑开始。

首先,必须在 「Account」 控制器中,注入 AutoMapper 和 UserManager 类:

private readonly IMapper _mapper;
private readonly UserManager<User> _userManager;
public AccountController(IMapper mapper, UserManager<User> userManager)
{
    _mapper = mapper;
    _userManager = userManager;
}

「UserManager」 由 Identity 框架提供,它负责帮助我们管理应用程序中的用户。

用户注册逻辑在 Post 请求的 Register 方法中实现:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(UserRegistrationModel userModel)
{
    if(!ModelState.IsValid)
    {
        return View(userModel);
    }

    var user = _mapper.Map<User>(userModel);

    var result = await _userManager.CreateAsync(user, userModel.Password);
    if(!result.Succeeded)
    {
        foreach (var error in result.Errors)
        {
            ModelState.TryAddModelError(error.Code, error.Description);
        }

        return View(userModel);
    }

    await _userManager.AddToRoleAsync(user, "Guest");

    return RedirectToAction(nameof(HomeController.Index), "Home");
}

这里需要注意的是,我们把这个方法改成了异步的,因为 ·UserManager· 的助手方法也是异步的。

在方法内部,先检查模型的有效性,如果它是无效的,就返回带有无效模型的相同视图。

如果检查通过,就将 userModel 射到 user 实体。

使用 CreateAsync 方法用来注册用户,它会对密码进行哈希后保存。

之后,检查它返回的创建结果。

如果创建成功,我们只需在 UserManager 的帮助下,使用 AddToRoleAsync 方法,给用户添加一个默认的角色,并将用户重定向到 Index 页面。

但是如果注册失败,就遍历所有的错误,并将它们添加到 ModelState 中。

前面我们已经将视图的验证信息摘要改为了 All,否则这里添加的错误,不会显示出来。

最后,不要忘记创建 AutoMapper 的映射配置类:

// MappingProfile.cs

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<UserRegistrationModel, User>()
            .ForMember(
            user => user.UserName, 
            expression => expression.MapFrom(userModel => userModel.Email));
    }
}

这里我们把电子邮件映射到用户名,因为我们在注册表单中没有提供用户名的字段。

现在启动应用,测试一下用户注册。

我可以尝试各种错误的注册内容,观察验证结果。

需要注意的是,默认的密码验证规则,非常的复杂,它不但要求有大小写字母,还必须要求你有一个特殊符号。

这个密码规则很不错,但是有时候我可能并不需要强规则。

而且我们使用了电子邮件作为用户名,但是默认情况下,没有要求电子邮件是唯一的。

所以,我们可以改变这些规则,这需要在注册 Identity 的地方,通过配置选项修改:

services.AddIdentity<User, IdentityRole>(options =>
  {
    options.Password.RequiredLength = 6;
    options.Password.RequireDigit = false;
    options.Password.RequireUppercase = false;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireLowercase = false;
    options.User.RequireUniqueEmail = true;
  })
.AddEntityFrameworkStores<ApplicationContext>();

比如,我们这里取消了数字、大写字母和特殊字符的验证,最小长度改为 6 位,同时设置电子邮件的唯一验证。

现在我们再来启动应用,测试一下注册过程,比如错误的密码格式、重复的电子邮箱。

大家通过一些测试可以发现,有些错误信息是中文的、有些则是英文的。

因为这个错误分为两种类型,一种是我们自定义的模型验证错误,我们已经设置了错误的信息,所以这种类型是中文的。

另一种是 ASP.NET Core Identity 内置的验证错误,它是我们通过在操作方法中,自己读取并添加进模型状态的。

因为 ASP.NET Core Identity 是英文版的,所以我们获取到的是英文信息,

不过,我们也可以自行定义中文信息,这需要我们创建一个自定义的错误描述类:

public class CustomIdentityErrorDescriber : IdentityErrorDescriber
{
    public override IdentityError PasswordTooShort(int length)
    {
        return new()
        {
            Code = nameof(PasswordTooShort),
            Description = $"密码至少需要 {length} 位"
        };
    }

    public override IdentityError DuplicateEmail(string email)
    {
        return new()
        {
            Code = nameof(DuplicateUserName), 
            Description = $"电子邮箱 {email} 已存在。"
        };
    }

    public override IdentityError DuplicateUserName(string username)
    {
        return new()
        {
            Code = nameof(DuplicateUserName), 
            Description = $"用户名 {username} 已存在。"
        };
    }

自定义的错误信息,需要通过覆写 「IdentityErrorDescriber」 基类的错误方法,设置你想要的错误描述。

需要的话,你可以通过阅读源码,找到所有的错误方法。

我们还需要把它配置到注册 Identity 服务的方法中:

AddErrorDescriber<CustomIdentityErrorDescriber>()

小结

现在,我们已经实现了用户注册,具体的代码可以参看示例项目,下篇文章将会继续讲解用户登陆以及身份认证。