ASP.NET Core Razor Pages in Action 9 使用身份验证管理用户
本章涵盖
• 实施基本身份验证
• 安装和配置 ASP.NET Core Identity
• 搭建基架和自定义身份 UI
本章和下一章将介绍如何在 Razor Pages 应用程序中管理用户。他们着眼于两个密切相关的主题:身份验证和授权。身份验证是识别用户身份的过程。授权是保护应用程序过程的一部分,它将用户的访问权限限制为仅允许他们访问的应用程序部分。
本章重点介绍用户的性质以及如何验证其身份,或确保他们是他们声称的身份。身份验证很难正确。从实际角度来看,您需要提供机制来捕获和存储用户的个人信息,包括只有他们知道的信息,例如密码。然后,您需要能够在后续访问中记住该用户。您的应用程序可能还要求您使用辅助身份验证机制,即双因素身份验证 (2FA),例如通过 SMS 发送的代码。您可能需要为用户提供重置密码(如果忘记密码)或管理其个人资料信息的功能。如何验证电子邮件地址?或者在反复错误登录尝试的情况下管理帐户锁定?您需要以加密安全的方式完成所有这些作。
要真正安全,您需要跟上不断变化的加密标准,并深入了解允许不良行为者(黑客)劫持或克隆用户身份的潜在攻击媒介。因此,除非您是该领域的专家,否则您永远不应该尝试实施自己的身份验证解决方案。如果他们的个人数据最终出现在 Pastebin 上供全世界查看,您的用户将不会钦佩您的技术努力。
相反,我们将探索由专家编写和测试的现成 ASP.NET Core Identity 库,该库解决了安全管理用户的问题。我们将了解它的默认实现以及它提供的自定义机会。然后,您将了解 Identity UI 包,其中包括涵盖各种用户管理场景的现成页面。您将使用基架生成其中一些页面的版本,以便根据您的要求对其进行自定义,并且您将为新注册实施电子邮件确认服务。
在本章结束时,您将了解身份验证基础知识,并使用 ASP.NET Core Identity 扩展现有数据库以处理用户信息存储。您将了解如何自定义 Identity 框架的各个方面以满足您的业务需求,并且已安装 Identity UI 包并学习如何根据您的应用程序要求对其进行修改。
9.1 身份验证基础知识
虽然本章的大部分内容集中在 ASP.NET Core Identity 库,但您首先要了解 ASP.NET Core 应用程序中身份验证背后的基础知识。您将了解如何向 Razor Pages 应用程序添加身份验证服务、启用它们,并使用它们为用户分配身份,以便它们对您不再匿名。
9.1.1 身份验证的工作原理
如果您曾经参加过贸易会议,您很可能在门口被要求表明自己的身份,可能是通过填写某种纸质或电子表格。您让自己登录并获得一个徽章,可以在会议厅周围佩戴,这样您就不必一次又一次地重新表明自己的身份。徽章将包含您编码为条形码或 QR 码的信息,因此需要知道您身份的人可以使用解码器(扫描仪)快速访问它,而无需询问您。徽章将在某个时候过期 - 可能在一天结束时或大会上。
Web 应用程序中的身份验证遵循类似的过程。Razor Pages 应用程序访问者需要通过填写某种登录表单来提供其身份信息。登录后,身份验证服务会将有关用户的信息序列化为加密的 Cookie,这相当于您在约定中佩戴的徽章。对于所有后续请求,该 Cookie 将在客户端和服务器之间传递,直到 Cookie 过期。身份验证中间件从此 Cookie 中读取值,并使用它们来合成 HttpContext 的 User 属性。此后,任何需要有关当前用户的信息的服务都可以检查 User 属性,该属性是 ClaimsPrincipal 类型。在 Razor Pages 应用程序中,等效于贸易协定访问者由 ClaimsPrincipal 的此实例表示。
9.1.2 添加简单身份验证
本节介绍向 CityBreaks 应用程序添加基于 Cookie 的身份验证所需的最少步骤。您将向服务容器添加身份验证服务,并指定有关身份验证服务应如何工作的一些默认信息。然后,您将添加启用身份验证功能的身份验证中间件。最后,您将添加一个简单的登录表单来获取用户的凭证并对其进行身份验证。
首先,您需要向 Program.cs 添加 using 指令,以使 Microsoft.AspNetCore .Authentication.Cookies 可用于您的代码。然后,使用 AddAuthentication 方法添加身份验证服务。
清单 9.1 向服务容器添加身份验证服务
builder.Services.AddAuthentication(CookieAuthenticationDefaults
.AuthenticationScheme)
.AddCookie();
就是这样。您添加了非常基本的 Cookie 身份验证。您必须指定默认方案的名称,该名称表示已注册的身份验证处理程序及其选项。在本例中,您使用了 CookieAuthenticationDefaults.AuthenticationSchxme 来表示方案的名称。它是一个解析为 Cookie 的常量。
下一阶段是添加身份验证中间件。您将通过调用 app 来执行此作。UseAuthentication() 请求管道中。但是,此调用的位置对于身份验证正常工作至关重要。它必须在添加路由之后但在 MapRazorPages 调用添加终结点之前放置。默认模板包括对 UseAuthorization 的调用,该调用添加了授权中间件。在此之前,您还必须添加身份验证;最终结果如下面的清单所示。
清单 9.2 在路由之后和授权之前添加身份验证中间件
app.UseRouting();
app.UseAuthentication(); ❶
app.UseAuthorization();
app.MapRazorPages();
❶ 在路由之后和授权和端点中间件之前添加身份验证。
我们将在下一章更详细地探讨授权,但目前,授权中间件需要有当前用户(由身份验证中间件产生)和当前端点(由路由中间件选择)的信息,才能知道如果当前用户无权访问所选端点,是否要短路管道,或者允许请求流向端点中间件。 ,以便可以执行选定的端点。接下来,您将在 Pages 文件夹中创建一个名为 Login 的新 Razor 页面。您将使用以下清单中的代码添加一个非常简单的表单,该表单仅捕获名称。
清单 9.3 简单的登录表单
@page
@model CityBreaks.Pages.LoginModel
@{
}
<div class="col-4">
<form method="post">
<div class="mb-3">
<label class="form-label" asp-for="UserName"></label>
<input class="form-control" asp-for="UserName" />
</div>
<div class="mb-3">
<button class="btn btn-outline-primary">Sign in</button>
</div>
</form>
</div>
现在,转到 PageModel,您需要添加下一个清单中所示的 using 指令。
清单 9.4 LoginModel 类所需的 using 指令
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using System.ComponentModel.DataAnnotations;
PageModel 类本身 (LoginModel) 具有 UserName 的绑定属性和异步 OnPost 处理程序,您可以在其中使用提供的用户名登录用户。
列表 9.5 LoginModel 类
public class LoginModel : PageModel
{
[BindProperty, Display(Name="User name")] ❶
public string UserName { get; set; } ❶
public async Task OnPostAsync()
{
var claims = new List<Claim> ❷
{ ❷
new Claim(ClaimTypes.Name, UserName) ❷
}; ❷
var identity = new ClaimsIdentity(claims,
➥ CookieAuthenticationDefaults.AuthenticationScheme); ❸
var principal = new ClaimsPrincipal(identity); ❹
await HttpContext.SignInAsync(principal); ❺
}
}
❶ 将 UserName 属性分配为绑定目标。
❷ 将用户名分配给索赔。
❸ 根据声明创建身份。
❹ 使用标识创建 ClaimsPrincipal。
❺ 签到委托人。
专注于清单 9.5 中的 OnPostAsync 处理程序,您首先创建一个声明。声明是一致存储的有关用户的信息片段:声明的名称和值(可选)。这是基于声明的身份验证的基础,这也是 ASP.NET Core 使用的功能。例如,我声称自己有 Mike Brind 这个名字。我也声称自己是一名作家(尽管是偶然的)。而且我声称会开车......比刘易斯·汉密尔顿更好。好的,所以我的一些主张可能经不起推敲,但我们稍后会看看。
您将使用该声明创建身份。标识是支持声明的特定类型:ClaimsIdentity。正如您从用于构造此标识的声明是集合的一部分这一事实中推断出的那样,一个标识可以支持多个声明。此标识用于构造 ClaimsPrincipal,表示登录的用户。ClaimsPrincipal 能够支持多个身份,这也是有道理的,因为在现实生活中,我们实际上有多种形式的身份。例如,我有驾照和护照。两种形式的身份具有相似的声明:我的名字和地址。我的驾驶执照还包括有关我被允许驾驶的车辆类别的索赔。我有一张季票可以去观看我最喜欢的足球队。这是另一种形式的身份证明,包括我被允许进入体育场的入口、我被分配的座位以及通行证有效的赛季。图 9.1 说明了这个概念。
图 9.1 ClaimsPrincipal 支持多个标识,每个标识都支持多个声明。
创建主体后,使用 HttpContext 上的 SignInAsync 扩展方法将其登录。此方法使用已注册的身份验证处理程序创建 Cookie,从而确定用户已经过身份验证。
接下来,您将向布局页面添加一些代码,这些代码将显示经过身份验证的用户的名称,如果用户未经过身份验证,则显示指向登录页面的链接。将清单 9.6 中的代码块放在包含 ul 元素的 div 之后,该元素在 nav 元素中包含导航链接。
清单 9.6 显示用户名或登录链接
<div class="align-content-end">
@if (User.Identity.IsAuthenticated) ❶
{
<p>@User.Identity.Name, you are logged in</p> ❷
}
else
{
<a asp-page="/login">Sign in</a>
}
</div>
❶ 检查用户是否经过身份验证。
❷ 使用 User.Identity.Name 属性访问用户名。
ClaimsPrincipal 通过页面的 User 属性进行访问。其 Identity 属性提供对用户的主要声明标识的访问。它公开了两个有用的属性:IsAuthenticated(用于确定当前请求是来自经过身份验证的用户还是匿名用户)和 Name(用于获取分配给 Name 声明的值)。
还有两个步骤需要采取。第一种是强制用户登录。您将通过发出质询来执行此作,使用我在第 3 章:ChallengeResult 中没有介绍的作结果。与 Cookie 一起使用时,质询会将匿名用户重定向到配置的登录路径,默认情况下为 /Account /Login。您正在使用不同的路径,因此您需要先配置该路径。导航到 Program.cs,然后修改身份验证服务注册以包含 Cookie 选项,如下所示。
示例 9.7 配置默认登录路径
builder.Services.AddAuthentication(CookieAuthenticationDefaults
➥ .AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/login";
});
最后,在主页 IndexModel 中修改 OnGetAsync 处理程序,以便在用户未通过身份验证时返回 ChallengeResult。还需要为 Microsoft.AspNetCore.Mvc 添加 using 指令。
列表 9.8 向 OnGetAsync 处理程序添加 ChallengeResult
public async Task<IActionResult> OnGetAsync() ❶
{
if (User.Identity.IsAuthenticated) ❷
{
Cities = await _cityService.GetAllAsync();
return Page();
}
return Challenge(); ❸
}
❶ 更改返回 Task<IActionResult>
的方法。
❷ 在显示内容之前检查用户是否经过身份验证。
❸ 如果没有,请发出质询。
除了 Razor 页面之外,PageModel 类还通过 User 属性公开当前用户。您将访问该作以确定当前用户是否经过身份验证,如果未通过身份验证,您将使用 Challenge 帮助程序方法返回新的 ChallengeResult。
现在一切都已准备就绪,您可以运行应用程序。当应用程序启动时,您应该会自动重定向到配置的登录路径,但请注意,浏览器地址栏中的 URL 还包含一个查询字符串:/login?ReturnUrl=%2F 的 ReturnUrl=%2F 中。ReturnUrl 查询字符串值表示发出质询的页面,在本例中为主页。%2F 是 URL 编码的正斜杠。通过输入用户名并提交登录表单进行身份验证后,SignInAsync 方法会将您重定向回该位置。尝试输入任何你喜欢的名字,并注意你已经通过身份验证了(图 9.2)。
图 9.2 您已成功通过身份验证,导航栏中的消息确认了这一点。
在网站上移动,请注意您保持登录状态。您只能在关闭浏览器时注销自己,而不仅仅是选项卡。这不是很安全。登录时可以假装成任何人的事实也不是。
我以地球上最富有的人之一(埃隆·马斯克 (Elon Musk))的身份登录,只是因为我能做到。目前,应用程序中没有代码来验证我声称自己是马斯克先生。想象一下,如果你能走到 Elon 的银行,说你就是他。当然,银行会要求您通过提供其他信息来验证您的索赔,例如只有马斯克先生知道的秘密。在大多数情况下,这至少采用密码的形式。
为了使您的登录更安全,您需要一种方法来安全地捕获和存储用户的密码。您还需要将提交的凭证与您存储的凭证进行比较,以确保登录者是他们所说的身份。理想情况下,您还需要为用户提供一种方法,以便在他们忘记密码或认为密码已泄露时注册帐户、注销和重置密码。这是从头开始进行的大量工作,尤其是当 ASP.NET Core Identity 为您完成大部分工作时。
9.2 ASP.NET 核心身份
ASP.NET Core Identity (Identity) 是一个支持身份验证 (确定访客是谁) 和授权 (确定允许他们执行哪些作) 的框架。默认实现使用 EF Core 将用户详细信息(包括其凭据)存储在数据库中。它还为许多常见场景提供了可自定义的 UI,包括注册用户;让他们登录;重置密码;管理他们的个人资料;生成帐户验证令牌;与外部身份验证提供商合作,例如 Google、Microsoft、Facebook、Twitter;和更多。
标识以 NuGet 包的形式提供。当您第一次使用 Visual Studio 中的 new project 向导创建 Web 应用程序时,您可以通过指定 Individual Accounts 作为身份验证类型来将其配置为在一开始就包含 Identity(图 9.3)。
图 9.3 选择 Individual Accounts 以在项目中包含 Identity。
如果您使用 CLI 创建应用程序,则可以使用 --auth 或 -au 开关来指定新项目使用个人账户:
dotnet new webapp --auth Individual
但是,您已经有一个项目。您需要采取某些步骤才能将其设置为与 Identity 一起使用。您需要添加所需的包,添加一个类来表示您的用户,配置现有数据库以使其与 Identity 一起使用,然后配置 Identity 所依赖的服务。完成这些步骤后,您可以添加新的迁移,该迁移将更新您的现有数据库以充当身份存储。
存储与 Identity 本身是分开的。您可以自由选择自己的存储机制,并创建自定义提供程序(如果尚不存在)以使用它。身份存储的默认实现依赖于 Microsoft.AspNetCore 。Identity.EntityFrameworkCore NuGet 包,适用于关系数据库。使用 dotnet add package(如果使用 CLI)将其添加到项目中,或者使用 install-package(对于包管理器控制台),通过直接添加对项目文件的引用或使用 Visual Studio 工具在解决方案中管理 NuGet 包。标识 UI 位于名为 Microsoft.AspNetCore 的包中。Identity.UI,因此也需要添加它。添加这两个包后,请确保执行 dotnet restore。
9.2.1 创建用户
下一步要求您创建一个类来表示您的用户。此类的一个关键功能是,如果要使用 Identity 框架,它必须从 IdentityUser 派生。IdentityUser 类型已定义多个属性,包括 UserName、Email、PhoneNumber 和一些特定于身份验证工作流程的属性,例如 LockoutEnabled。这表示是否启用了锁定功能,因此在指定次数的无效登录尝试后,帐户会自动锁定。您可以启用此功能来防止暴力攻击 — 通常是黑客系统地尝试所有可能的密码组合来自动尝试登录。
IdentityUser 还包括一个名为 HashedPassword 的属性。这提供了一个线索,表明 Identity 在存储密码之前使用加密安全算法对密码进行哈希处理。密码永远不应存储为纯文本。 用户很懒惰,倾向于重复使用密码。他们很可能在您的应用程序中使用与他们用于银行相同的应用程序。如果黑客能够获取用户数据的副本,则您可能犯了向他们提供对应用程序外部各种资源的访问权限的罪行。您将调用您的用户类 CityBreaksUser。将以下类添加到 Models 文件夹中。
清单 9.9 CityBreaksUser 类
using Microsoft.AspNetCore.Identity;
namespace CityBreaks.Models
{
public class CityBreaksUser : IdentityUser ❶
{
}
}
❶ 您的用户类派生自 IdentityUser。
在此阶段,我们将仅使用 IdentityUser 的默认属性。我们稍后会看看如何自定义这个类。
9.2.2 配置 DbContext
下一阶段涉及调整现有的 CityBreaksContext 以使用 Identity。为此需要进行两项修改(清单 9.10)。第一种是从 IdentityDbContext<TUser>
而不是 DbContext 派生上下文类,其中 TUser 表示您刚刚创建的用户类。IdentityDbContext 包括一些 DbSet 属性,表示 Identity 用于存储用户数据各个方面的数据库表。您还需要包含 Microsoft.AspNetCore.Identity 的 using 指令。EntityFrameworkCore 的 EntityFrameworkCore 中。
示例 9.10 调整 DbContext 以使用 Identity
using CityBreaks.Data.Configuration;
using CityBreaks.Models;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; ❶
using Microsoft.EntityFrameworkCore;
namespace CityBreaks.Data
{
public class CityBreaksContext :
➥ IdentityDbContext<CityBreaksUser> ❷
{
public CityBreaksContext(DbContextOptions options) :
➥ base(options)
{
...
❶ 添加相关的 using 指令以将 Identity 引入范围。
❷ 从 IdentityDbContext
您需要确保调用 base。OnModelCreating 在重写的 OnModelCreating 方法中;否则,将不会进行 IdentityDbContext 的模型配置,从而导致在尝试创建迁移时出现错误。这在以下清单中的粗体行中显示。
清单 9.11 调用基。OnModelCreating 在重写的 OnModelCreating 中
protected override void OnModelCreating(ModelBuilder builder)
{
builder
.ApplyConfiguration(new CityConfiguration())
.ApplyConfiguration(new CountryConfiguration())
.ApplyConfiguration(new PropertyConfiguration());
base.OnModelCreating(builder);
}
配置 Identity 服务
您需要配置应用程序以包含 Identity 服务,并使用 CityBreaksContext 作为存储。将对 AddAuthentication 的现有调用替换为以下对 AddDefaultIdentity
清单 9.12 向应用程序添加 Identity 服务
builder.Services.AddDefaultIdentity<CityBreaksUser>()
.AddEntityFrameworkStores<CityBreaksContext>();
此服务注册使用其默认设置配置 Identity。稍后您将看到这些设置是什么以及如何自定义它们。
9.2.3 添加迁移
最后,您处于可以添加迁移的阶段,该迁移将搭建数据库中所需的表的基架。与上一章一样,您可以使用 add-migration 命令通过包管理器控制台或通过 CLI 的 dotnet ef migrations add 创建迁移。您将此迁移命名为 AddedIdentity:
[PMC]
add-migration AddedIdentity
[CLI]
dotnet ef migrations add AddedIdentity
执行此命令后,请花点时间检查迁移的 Up 方法中的代码。如果一切按计划进行,您应该有代码来创建 7 个表,每个表表示 IdentityDbContext 类中的一个 DbSet。通过 CLI 使用 update-database 命令或 dotnet ef database update 应用迁移,然后查看数据库的修订架构。果然,数据库中已经添加了 7 个表(图 9.4),每个表的名称都以 AspNet 为前缀。
图 9.4 Identity 添加了 7 个前缀为 AspNet 的表。
主表是 AspNetUsers 表。这是存储用户配置文件数据的位置。其他表是可选的。其中 3 个与 roles management 有关,这是一种管理不同级别授权的机制;我们将在下一章中探讨这些。AspNetUserClaims 表用于将有关用户的其他信息存储为声明集合,以支持授权方案。AspNetUserLogins 表用于存储有关用户的外部登录名(如 Google 或 Facebook)的信息(如果您选择实现第三方身份验证)。AspNetUserTokens 表是保存外部登录授权令牌的位置。我在本书中不介绍第三方身份验证,但您可以参考官方文档,以获取有关与最流行的社交登录服务集成的指导:http://mng.bz/p6QK。
在运行应用程序之前,您只需要做最后一件事。您需要将上一节中添加到布局中的代码替换为部分,因为默认 Identity UI 需要一个名称:_LoginPartial。因此,将新的 Razor 视图 > Empty 添加到名为 _LoginPartial.cshtml 的 Pages\Shared 文件夹。从布局中剪切以下代码段,并将其粘贴到新的 partial 文件中。添加 Sign out anchor tag 帮助程序,并修改 Sign in anchor tag 帮助程序。
清单 9.13 从布局中提取并粘贴到 _LoginPartial.cshtml 中
<div class="align-content-end">
@if (User.Identity.IsAuthenticated)
{
<p>@User.Identity.Name, you are logged in
<a asp-area="Identity" asp-page="/Account/Logout">Sign out</a> ❶
</p>
}
else
{
<a asp-area="Identity" asp-page="/Account/Login">Sign in</a> ❷
}
</div>
❶ 添加用于注销的锚标签助手。
❷ 更改锚点标签助手以指定 Identity 区域和 Account/Login 页面。
在从布局中剪切代码的位置,将其替换为
<partial name="_LoginPartial" />
完成此作后,您可以启动应用程序。这一次,IndexModel OnGet 方法中的质询应该将您重定向到 Identity 的默认登录路径 /Identity/ Account/Login,如图 9.5 所示。
图 9.5 默认身份登录 UI
如果您在此时期待一个屡获殊荣的主题,您可能会感到有点失望。好消息是默认 UI 可以正常工作;但它看起来并不好看,就像您在上一章中创建的脚手架 CRUD 页面一样,它需要一些自定义,然后才能将其部署为工作应用程序的一部分。您的用户不会对右侧的内容有太多用处,该内容链接到有关配置外部身份验证提供程序(如 Facebook 或 Google)的文章。
目前,您将注册一个新帐户并使用它登录。单击 Register as a New User 链接,然后输入电子邮件地址和密码。默认密码要求是它应包含以下内容:
• 最少 6 个字符
• 至少一个非字母数字字符(例如 *!)
• 至少一个小写字符
• 至少一个大写字符
• 至少一位数字
提交注册表后,您应该会发现您已自动登录到应用程序(图 9.6)。您还应该看到 AspNetUsers 表中只有一个条目(图 9.7)。
图 9.6 使用 Identity 登录
图 9.7 AspNetUsers 表中添加了一个新条目。
在查看 AspNetUsers 表中的数据时,可以快速查看表的架构,该架构反映了 IdentityUser 类属性。一些字段与帐户状态相关,而其他字段则与用户的用户档案相关。配置文件字段仅限于账户管理所需的字段(姓名、电子邮件和电话号码),因此可以通过电子邮件或短信发送令牌和确认。如果您运营的是电子商务网站,您还需要存储用户的送货地址,并且可能还需要存储不同的账单地址。您可能希望存储他们的出生日期,以便向他们发送卡片或限制有年龄限制的产品和服务的销售。在本章的其余部分,您将了解如何自定义 IdentityUser 以启用此功能,以及其他与 Identity 相关的自定义。在执行此作之前,请单击 Sign Out 链接,然后按照说明注销应用程序以确认注销功能是否有效。
9.3 自定义身份
现在,您已经有了 Identity 及其 UI 工作,我们可以查看一些选项,用于自定义各个方面以满足您自己的应用程序要求。您已经看到了默认密码选项;现在我们将探索如何控制这些选项以及与 Identity 工作原理相关的其他选项。然后,我们将研究如何自定义 IdentityUser 的实现,以便您可以存储比基类型所需的更多信息。最后,我们将更仔细地研究默认 UI,并了解如何控制其外观和行为。
9.3.1 自定义身份选项
IdentityOptions 类表示可用于配置 Identity 系统的所有选项。它具有许多属性,提供对与 Identity 的特定区域相关的选项的访问。表 9.1 显示了您最有可能使用的属性。
表 9.1 选择 IdentityOptions 属性
The options for managing your application’s policy for locking accounts in the event of failed login attempts |
其他属性允许您为电子邮件确认、密码重置等配置令牌生成,以及与密钥值和个人数据的存储相关的几个选项。对于大多数应用程序,您不太可能触及这些内容。
您将通过 AddDefaultIdentity <TUser>
的重载来配置 IdentityOptions,该重载将 Action<IdentityOptions>
作为参数。下面的清单通过显示如何配置 User 和 Password 属性的特定选项来说明这一点。
示例 9.14 设置身份选项
builder.Services.AddDefaultIdentity<CityBreaksUser>(options => {
options.User.RequireUniqueEmail = false; ❶
options.Password.RequiredLength = 8; ❷
})
❶ 通过 User 属性设置用户选项。
❷ 通过 Password 属性设置密码选项。
配置用户选项
表 9.2 显示了 UserOptions 类的两个属性,它们表示可配置的用户选项。默认 Identity UI 包使用电子邮件地址作为用户名,该用户名必须是唯一的。请记住这一点,如果您要进一步限制允许的字符并删除符号,用户将无法注册,因为电子邮件地址至少需要两个符号。这些选项仅在您更改用户的注册方式时生效 - 这相对容易做到,您稍后将看到。
表 9.2 UserOptions 属性
Specifies the range of characters permitted in a user’s name. Defaults to a-z, A-Z, 0-9, and the symbols -._@+. |
|
配置密码选项
密码正在成为一个热门话题。一段时间以来,Microsoft 一直在从密码转向生物识别技术(例如指纹、面部和语音模式识别)以及通过 SMS 发送给用户的安全代码。尽管如此,密码仍然是 Identity 默认实现的核心。如前所述,通过 PasswordOptions 对象(表 9.3)有许多配置选项。
表 9.3 PasswordOptions 属性
在 Identity 中,密码在存储之前会进行哈希处理。哈希过程还包括盐,即随机生成的值,该值被添加到密码中,以确保两个相同密码的结果哈希值是唯一的。然后,盐和哈希密码一起存储在 AspNetUsers 表的 PasswordHash 列中。
当用户在登录过程中提交密码时,将检索用户名的 PasswordHash 值,并提取盐并用于对提交的密码进行哈希处理。哈希是确定性的,因为给定相同的输入,哈希将始终导致相同的输出。因此,假设提交的密码正确,则提交的密码的加盐和哈希版本应与数据库中存储的密码相匹配。与加密不同,哈希也是单向的。您无法撤消哈希值以检索原始值。
这听起来可能很复杂,而负责处理密码以进行存储的算法肯定很复杂,这强调了一点,除非您知道自己在做什么,否则您不应尝试替换现有实现。
在您的应用程序中,您将删除大部分限制,因此您可以使用简单的密码进行测试。应用以下选项,无需使用特殊字符。
清单 9.15 配置口令选项
builder.Services.AddDefaultIdentity<CityBreaksUser>(options =>{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
}).AddEntityFrameworkStores<CityBreaksContext>();
注意 如果更改 RequiredLength 值,则还需要更新已应用于 Identity UI 注册表中使用的 RegisterModel 中的 UserName 属性的 StringLength 验证。稍后,当您搭建基架并自定义 UI 本身时,您将了解到这一点。
配置 SignInOptions
SignInOptions 支持配置各种帐户确认要求,详见表 9.4。提供 IUserConfirmation
表 9.4 SignInOptions 属性
默认情况下,Identity UI 包不支持电话号码确认。因此,如果将此项设置为 true,则会将所有用户锁定在应用程序之外。如果要启用密码重置或其他依赖于电子邮件地址的功能,最好启用 RequireConfirmedEmail。稍后,我们将了解如何在本地启用和测试此功能,而无需访问电子邮件提供商。
配置 LockoutOptions
主要是为了防御暴力攻击,您可以在多次尝试登录失败时启用帐户锁定。Table 9.5 中详细介绍了可配置选项。
表 9.5 LockoutOptions 属性
Determines the value applied to the LockoutEnabled column for newly created users |
||
Specifies the maximum number of failed attempts to sign in before the account is locked out |
表 9.5 中的选项配置帐户锁定(如果使用)。在本章后面的章节中,当您自定义 UI 时,您将看到如何管理此流程。在此之前,我们将研究自定义用户。
9.3.2 自定义用户
您的用户类 CityBreaksUser 派生自 IdentityUser。正如我所讨论的,这与用户的个人资料相关的属性数量有限。如果您想允许用户预订住宿地点,您至少需要他们提供一些其他信息,例如他们的姓名和地址。请记住,Identity 背后的默认数据访问技术是 EF Core,因此您可以将新属性添加到用户实体,然后使用迁移将这些更改传播到数据库,而不是手动修改数据库架构来容纳这些数据。首先,您将修改 CityBreaksUser 以包含三个属性。
列表 9.16 自定义 IdentityUser
public class CityBreaksUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
}
然后,您将创建一个新的迁移:
[Powershell]
add-migration CustomizeIdentityUser
[CLI]
dotnet ef migrations add CustomizeIdentityUser
应用迁移(update-database 或 dotnet ef database update)后,您可以检查 AspNetUsers 表的架构,以确认已成功添加新列,如图 9.8 所示。
图 9.8 迁移后修改后的 AspNetUser 表
现在,您可以容纳额外的用户数据,您需要在注册时捕获这些数据。因此,在下一节中,您将看到它是如何完成的。
9.3.3 基架和自定义 UI
标识 UI 是作为 Razor 类库 (RCL) 开发的,使其能够部署为 NuGet 包并插入到想要使用它的应用程序中。RCL 可以包含 Razor 页面、静态资产,甚至 MVC 控制器和视图。开发 RCL 是本书未涵盖的高级主题,但您可以参考官方文档了解更多详细信息:http://mng.bz/O6Vw。只要您在 RCL 中复制文件结构,RCL 的内容就可以被覆盖。磁盘上的物理文件优先于 RCL 的内容。
Identity UI 包括对基架的支持,以生成部分(如果不是全部)Identity 页面的物理副本。基架页面会复制 RCL 中的文件结构,这意味着它们会自动覆盖默认 UI。身份基架取决于您在第 8 章:Microsoft.VisualStudio.Web.CodeGeneration.Design 中生成 CRUD 页面时安装的同一软件包。如果您使用的是 Visual Studio,则可以通过 Add...New Scaffolded Item 对话框(图 9.9)。
图 9.9 New Scaffolded Item 对话框
当你点击 Add 按钮时,你会看到一些要搭建脚手架的文件(图 9.10)。
图 9.10 Identity UI 基架选择对话框
此对话框列出了身份 UI 中的每个页面。他们中的大多数的目的从他们的名字中相对容易弄清楚。但是,如果您想查看它们的作用,您始终可以勾选 覆盖所有文件 选项来生成每个文件并检查其内容。我建议在一个单独的项目中执行此作,您可以将其用作参考。构建页面后,您负责管理其代码。
如果您使用的是 VS Code,则基架对话框对您不可用。相反,您可以使用 CLI 为页面搭建基架。首先,您可以使用 -lf 或 --listFiles 选项列出所有可用文件,就像从项目文件夹中执行的以下命令一样:
dotnet aspnet-codegenerator identity --listFiles
请注意,listFiles 选项区分大小写,并且工具名称必须包含连字符: aspnet-codegenerator。在搭建 CRUD 页面的基架时,您可能还记得您不需要包含连字符。输出使用点表示法代替您在 Visual Studio 版本的文件列表中看到的路径分隔符来显示文件名(图 9.11)。
图 9.11 使用 CLI 进行基架列出的身份文件
根据您用于搭建基架的工具,在对话框中选择 Account\Login 和 Account\ Register 文件,然后选择 CityBreaksContext 作为数据上下文类,或使用 CLI 中的以下命令:
dotnet aspnet-codegenerator identity -dc CityBreaks.Data.CityBreaksContext
➥ -sqlite -fi "Account.Login;Account.Register"
要完全了解您在此命令中设置的选项及其较长的替代版本,您可以随时从 CLI 执行 dotnet aspnet-codegenerator identity --help。
单击对话框中的 Add,或执行命令。完成后,代码生成器应该已经创建了一个名为 Areas 的新文件夹,其中包含一系列嵌套的文件夹和文件(图 9.12)。
图 9.12 基架文件夹和文件
Razor 类库使用名为 areas 的功能,我在讨论定位点标记帮助程序上的 asp-area 属性时简要介绍了该功能。如果您还记得,在本章的前面部分,您必须在 login 标记帮助程序中包含 asp-area 属性,该属性指向名为 Identity 的区域。任何子文件夹都按照约定定义 Areas 文件夹中的区域。所以你有一个 Identity 区域;在该文件夹中,将 Razor 页面存储在各个区域自己的 Pages 文件夹中。除了 Login 和 Register 页面之外,基架还生成了 ViewStart、ViewImports 和用于管理验证脚本的部件。所有这些都会覆盖 RCL 中的匹配内容。这是您现在的代码。您拥有它。在开始使用代码之前,您将快速查看其内容,以更好地了解两个主要的身份参与者:UserManager 和 SignInManager。
UserManager 和 SignInManager
Identity UI 是可选的。无论您是否使用它,如果您基于 Identity 本身构建应用程序,您都会发现您需要或多或少地自定义您的身份验证工作流程。执行此作时,您必须使用 UserManager 和 SignInManager。
SignInManager 类提供了一个用于管理用户登录的 API。Table 9.6 总结了使用此类时最有可能使用的方法。
表 9.6 常用的 SignInManager 方法
此外,SignInManager 还提供了一系列方法,可帮助您使用外部登录提供程序、双重身份验证、锁定等。在 Register 和 Login 页面中查看 PageModel 代码时,您可以看到 SignInManager 作为服务注入到构造函数中,仅用于登录用户以及获取可能已注册的外部登录提供程序(例如 Twitter 和 Google)的列表。SignInManager 类在 http://mng.bz/m2z4 中完整记录。
UserManager 提供了一个 API,用于使用数据库或其他已注册的持久性存储来管理用户。因此,它包括保存和检索用户数据的广泛方法,包括表 9.7 中的方法。
表 9.7 选择 UserManager 方法
Gets the user corresponding to the ClaimsPrincipal passed in to the method |
|
此外,还有一些方法可用于管理密码、确认令牌、双重身份验证、锁定等。表中的最后两种方法更有可能用作授权工作流的一部分,我们将在下一章中更详细地介绍它们。与 SignInManager 一样,UserManager 作为服务注入到需要的任何位置。有关 UserManager 类的大量其他方法和属性的详细信息,您可以参考文档:http://mng.bz/5mYa。
返回自定义 UI,首先您将修改 Login 页面,使其不再提供有关连接外部身份验证服务的指导。打开基架 Login.cshtml 文件,并找到以以下代码开头的 div 元素:
<div class="col-md-6 col-md-offset-2">
<section>
<h3>Use another service to log in.</h3>
注释掉整个 div,或将其根除。然后运行应用程序。这一次,登录页面应该没有之前占据页面右侧的内容(图 9.13)。接下来,您将更改 Registration 页面以删除有关外部登录的相同内容,并捕获您添加到 user 类的其他信息。
图 9.13 修改后的 Login 页面
修改 Registration 页面
位于 Areas\Identity\Pages\Account\Register.cshtml.cs 中的 RegisterModel 类使用输入模型模式来封装用户名和电子邮件的绑定目标。您需要为名字、姓氏和地址添加属性,因此请将以下代码行添加到 RegisterModel 中声明的 InputModel 类中。
清单 9.17 为要捕获的其他注册数据添加属性
[Required]
[Display(Name ="First Name")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
public string Address { get; set; }
在此阶段,如果在配置密码选项时更改了 RequiredLength 值,则应相应地调整已应用于 InputModel 的 Password 属性的 StringLength 属性的 MinimumLength 属性值。
接下来,您需要将新属性的绑定值分配给用户。这发生在 OnPostAsync 方法中,其中包含大量代码。但是,在此阶段,如果 ModelState 有效,您应该只对创建用户的位感兴趣。
列表 9.18 如果 ModelState 有效,则创建用户
if (ModelState.IsValid)
{
var user = CreateUser(); ❶
await _userStore.SetUserNameAsync(user, Input.Email, ❷
➥ CancellationToken.None); ❷
await _emailStore.SetEmailAsync(user, Input.Email, ❸
➥ CancellationToken.None); ❸
var result = await _userManager.CreateAsync(user, ❹
➥ Input.Password); ❹
❶ 用户的实例由文件底部的私有 CreateUser 方法创建。
❷ 用户名已分配给用户。
❸ 电子邮件已分配给用户。
❹ 用户由 UserManager 保存到数据存储中。
在以前版本的 Identity UI 中,用户是在一行代码中使用其电子邮件和用户名创建的:
var user = new CityBreaksUser { UserName = Input.Email, Email = Input.Email };
作为 .NET 6 的一部分发布的更新版本要详细得多。创建被委托给一个名为 CreateUser 的私有方法,该方法使用 Activator.CreateInstance,对基架开发人员无法预见的某些边缘情况进行错误处理。你不必坚持下去。您可以改为将代码替换为更简单的版本。毕竟,这是您现在的代码。
与以前的版本还有其他差异。username 和 email 属性分别通过 IUserStore.SetUserNameAsync 和 IUserEmailStore.SetEmailAsync 方法分配。这些 API 提供了一种一致的方法来获取和设置用户的用户名和电子邮件。还有一个用于管理电话号码的类似 API:IUserPhoneNumberStore 接口。在本书中,我不会详细介绍这些接口。只需知道默认实施只需将指定值 (Input.Email) 分配给用户的 UserName 和 Email 属性就足够了。目前,您需要做的就是将传入值分配给用户的新属性。
清单 9.19 为 Identity 用户的自定义属性赋值
if (ModelState.IsValid)
{
var user = CreateUser();
user.FirstName = Input.FirstName; ❶
user.LastName = Input.LastName; ❶
user.Address = Input.Address; ❶
await _userStore.SetUserNameAsync(user, Input.Email,
➥ CancellationToken.None);
await _emailStore.SetEmailAsync(user, Input.Email,
➥ CancellationToken.None);
var result = await _userManager.CreateAsync(user, Input.Password);
❶ 在这里,您将绑定值分配给您添加到 IdentityUser 实现中的自定义属性。
您对 Razor 页面进行了两项修改:第一次注释掉或删除您在 Login 页面中处理的有关外部登录的相同代码块,第二次在表单开头添加以下表单字段,就在验证摘要标记帮助程序下方。
清单 9.20 其他 IdentityUser 属性的 forms 字段
<div class="form-floating">
<input asp-for="Input.FirstName" class="form-control" />
<label asp-for="Input.FirstName"></label>
<span asp-validation-for="Input.FirstName" class="text-danger"></span>
</div>
<div class="form-floating">
<input asp-for="Input.LastName" class="form-control" />
<label asp-for="Input.LastName"></label>
<span asp-validation-for="Input.LastName" class="text-danger"></span>
</div>
<div class="form-floating">
<textarea asp-for="Input.Address" class="form-control"></textarea>
<label asp-for="Input.Address"></label>
<span asp-validation-for="Input.Address" class="text-danger"></span>
</div>
现在您可以运行应用程序并单击 Login 页面上的 Register as a New User 链接,这应该会带您到修改后的表单,如图 9.14 所示。完成它(使用不同的电子邮件地址),然后提交。您应该发现自己已登录,并且还应该看到其他字段已填充到 AspNetUsers 表中。
图 9.14 修订后的登记表
9.3.4 启用电子邮件确认
Identity 支持一些使用电子邮件的方案。例如,您可以要求用户确认他们能够控制用于注册的电子邮件地址,方法是向他们发送一封电子邮件,其中包含一个链接,该链接必须单击以验证其注册。密码重置功能也依赖于电子邮件。Identity UI 包含由 IEmailSender 接口表示的电子邮件服务。它有一个方法 SendEmailAsync,在默认实现中,该方法根本不执行任何作。
清单 9.21 默认的 EmailSender 服务
internal class EmailSender : IEmailSender
{
public Task SendEmailAsync(string email, string subject, string htmlMessage)
{
return Task.CompletedTask;
}
}
您不必使用 IEmailSender 接口。您可以使用任何您喜欢的内容,但在此示例中,您将提供自己的实现,该实现使用名为 MailKit (http://mng.bz/69GA) 的开源电子邮件管理库生成电子邮件。Internet 上(以及我的站点上)的无数示例演示了如何使用 System.Net.Mail 类中的类从 ASP.NET 应用程序发送电子邮件,但这种方法现在已经过时了。相反,Microsoft 建议您使用更现代的库,MailKit 在其文档中特别提到作为示例。所以你需要做的第一件事是安装包:
[CLI]
dotnet add package MailKit
[Package Manager Console]
install-package MailKit
接下来,将一个名为 TempMail 的新文件夹添加到项目的根目录下。您将模拟过时的 System 提供的 SpecifiedPickupDirectory 交付方法。Net.Mail 类。这使您可以指定邮件库生成的电子邮件在磁盘上放置的位置。这对于测试和调试非常有用,因为这意味着您不必依赖网络可用性。MailKit 不支持开箱即用的 SpecifiedPickupDirectory,因此您将根据项目常见问题解答 (http://mng.bz/YK8z) 中的代码提供自己的实现。
将新的类文件添加到 Services 文件夹,并将其命名为 EmailService.cs。将任何现有内容替换为以下清单中的代码。
清单 9.22 EmailService 类
using MailKit.Net.Smtp;
using Microsoft.AspNetCore.Identity.UI.Services;
using MimeKit;
using MimeKit.IO;
namespace CityBreaks.Services
{
public class EmailService : IEmailSender ❶
{
private readonly IHostEnvironment _environment; ❷
public EmailService(IHostEnvironment environment) ❷
{ ❷
_environment = environment; ❷
} ❷
public async Task SendEmailAsync(string email, string subject, ❷
➥ string htmlMessage)
{
var pickupDirectory = Path.Combine(_environment.ContentRootPath,❸
➥ "TempMail"); ❸
var message = new MimeMessage(); ❹
message.From.Add(MailboxAddress.Parse( ❹
➥ "test@test.com")); ❹
message.To.Add(MailboxAddress.Parse(email)); ❹
message.Subject = subject; ❹
message.Body = new TextPart("html") ❹
{ ❹
Text = htmlMessage ❹
}; ❹
Await SaveToPickupDirectory(message, pickupDirectory); ❺
await Task.CompletedTask;
}
}
// SaveToPickupDirectory method here
}
❶ 该类实现 IEmailSender 接口。
❷ 注入 IHostEnvironment 接口,以便您可以使用它来生成电子邮件文件夹的路径。
❸ 生成文件夹路径。
❹ 从发送到 SendEmailAsync 方法的参数构造消息。
❺ “发送” 电子邮件。
从 MailKit 项目的 FAQ 或本节的下载 (http://mng.bz/G18D) 中获取 SaveToPickupDirectory 方法的代码,并将其插入到清单 9.22 中注释中指示的点。
注意此实现是硬编码的,用于将电子邮件转储到磁盘上的文件夹中。虽然这在本地运行应用程序时很方便,但当应用程序在生产环境中运行时,它就没有多大用处了。在最后一章中,我们将介绍管理它的方法,因此邮件服务的行为取决于应用程序运行的环境。
现在,您已经拥有了 IEmailSender 服务的实现,您需要向服务容器注册您的实现:
builder.Services.AddTransient<IEmailSender, EmailService>();
请记住,默认情况下,Identity 不要求用户确认其账户。仅当需要帐户确认时,才会使用电子邮件服务,因此您需要在 Program.cs 中更改 SignIn 选项。添加以下清单中所示的粗体行。
清单 9.23 要求进行账户确认
builder.Services.AddDefaultIdentity<CityBreaksUser>(options => {
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.SignIn.RequireConfirmedAccount = true;
}).AddEntityFrameworkStores<CityBreaksContext>();
运行应用程序,然后再次完成注册过程。这一次,您在填写表单时不会自动登录。您应该会收到一条消息来检查您的电子邮件(图 9.15)。
图 9.15 Register Confirmation 页面
在此阶段,如果您没有正确配置电子邮件服务,您将在页面中看到一条消息,其中包含一个链接,您应该单击该链接以确认您的帐户。
但是,希望你做对了,现在你的 TempMail 文件夹中有一个 EML 文件(图 9.16)。
图 9.16 电子邮件已生成并放置在指定的取件目录中。
使用合适的应用程序(电子邮件客户端)打开 EML 文件,然后单击链接(图 9.17)。
图 9.17 确认邮件
您应该被引导回应用程序,并收到一条消息,表明您的电子邮件已确认。现在,您可以登录到应用程序。您可以访问所有 Identity UI,甚至是提供您不打算实施的工作流程的部分。例如,identity/account/manage 端点上的多个页面允许用户更改其个人数据、获取数据副本、删除数据、管理 2FA 等。我们将在下一节中介绍管理这些的方法。
在我们继续之前,您应该记住关于此功能如何工作的一件事。生成的电子邮件中的链接在查询字符串中包含验证令牌。令牌使用 Base64 URL 编码 (http://mng.bz/09YJ) 进行编码。结果值包括大小写混合字符。如果您在路由选项中将 LowercaseQueryStrings 设置为 true,则编码值将更改为全小写,从而破坏令牌的完整性。在这种情况下,令牌将始终无法通过验证。
9.3.5 禁用 UI 功能
当您包含 UI 包时,您可以选择加入所有 UI 包。毫无疑问,会有一些你不想实现的部分。您可以选择如何禁用这些功能。您可以使用授权来防止未经授权的访问,同时保持页面的功能,我们将在下一章中介绍。
当然,可以删除指向您不希望用户访问的任何页面的链接。然而,这并不是一个万无一失的解决方案。其他精明的开发人员在访问或使用您的应用程序时可能会很好地识别出 Identity UI URL 方案,并且可能会想四处闲逛。相反,您可以搭建不想实现的页面的基架,并更改其代码以禁用它们。
在此示例中,您将搭建 LoginWith2Fa 页面的基架并禁用它。如果您使用的是 Visual Studio,请使用 Add...用于生成 Account\ LoginWith2fa 的新基架项选项。或者,使用 CLI 执行以下命令:
dotnet aspnet-codegenerator identity -dc CityBreaks.Data.CityBreaksContext
-sqlite -fi Account.LoginWith2fa
搭建页面基架后,打开 PageModel 文件,并注释掉现有的 OnGetAsync 和 OnPostAsync 方法。完成此作后,请将它们替换为以下实现:
public IActionResult OnGet() => NotFound();
public IActionResult OnPost() => NotFound();
如果用户尝试通过 GET 或 POST 请求访问此终端节点,我选择返回 404 Not Found 消息。如果您愿意,可以提供替代响应。您可能希望将用户重定向到其他位置;例如,您可以使用 RedirectToPage 方法。重要的是替换 OnGet 和 OnPost 处理程序。当禁用其他只有 OnGet 处理程序的页面时,实现 OnPost 处理程序仍然是明智的。
您已经完成了确定用户身份的任务,因此他们不再是匿名的。有了这些信息,您现在可以确定他们在网站上被授权做什么。目前,您有一个向所有人开放的管理区域。例如,您不希望任何人都能访问物业管理页面。他们可以设定假期价格,但这永远行不通。在下一章中,您将了解如何使用授权来根据您对选定访客的了解来限制对他们的访问。
总结
身份验证是识别站点用户的过程,因此他们不再是匿名的。
您需要添加身份验证服务和中间件才能在应用程序中启用身份验证。
基于浏览器的 Web 应用程序中的身份验证主要依赖于以加密格式保存当前用户身份的 Cookie。
ASP.NET Core Identity 是一个用于管理身份验证和用户的框架。它使用 EF Core 将用户数据存储在数据库中。
您可以通过迁移自定义 IdentityUser。
Identity UI 是一个包,它为许多身份验证方案提供页面。
您可以使用基架生成 Identity UI 页面的版本,并根据您的需要对其进行自定义。