本章包括
- ASP.NET Core 中 Web 应用程序的身份验证工作原理
- 使用 ASP.NET Core 标识系统创建项目
- 向现有 Web 应用添加用户功能
- 自定义默认 ASP.NET Core 标识 UI
像 ASP.NET Core 这样的 Web 框架的卖点之一是能够提供为个人用户定制的动态应用程序。许多应用程序都有一个“帐户”服务的概念,您可以“登录”到该服务并获得不同的体验。
根据服务的不同,帐户会为您提供不同的功能:在某些应用程序上,您可能需要登录才能访问额外的功能,在其他应用程序上您可能会看到建议的文章。在电子商务应用程序上,您可以下单并查看过去的订单;在 Stack Overflow 上,您可以发布问题和答案;而在新闻网站上,您可能会根据以前看过的文章获得定制体验。
当您考虑向应用程序添加用户时,通常需要考虑两个方面:
- Authentication —— 创建用户并让他们登录应用程序的过程
- Authorization —— 根据当前登录的用户自定义体验并控制用户可以做什么
在这一章中,我将讨论第一点,身份验证和成员资格,在下一章中我将讨论第二点,授权。在第 14.1 节中,我将讨论身份验证和授权之间的区别、身份验证在传统 ASP.NET Core Web 应用程序中的工作方式,以及如何构建系统以提供登录功能。
我还会提及传统 Web 应用程序和客户端或移动 Web 应用程序使用的 Web API 在身份验证方面的典型差异。本书重点介绍了用于身份验证的传统 Web 应用程序,但许多原则都适用于两者。
在第 14.2 节中,我介绍了一个名为 ASP.NET Core Identity(简称 Identity)的用户管理系统。Identity 与 EF Core 集成,并提供创建和管理用户、存储和验证密码以及用户登录和退出应用程序的服务。
在第 14.3 节中,您将使用包含 ASP.NET Core Identity 的默认模板创建一个应用程序。这将为您提供一个应用程序,让您了解 Identity 提供的功能以及它没有的所有功能。
创建一个应用程序非常有助于了解各个部分是如何结合在一起的,但您通常需要向现有应用程序添加用户和身份验证。在第 14.4 节中,您将看到将 ASP.NET Core Identity 添加到现有应用程序所需的步骤:第 12 章和第 13 章中的配方应用程序。
在第 14.5 节和第 14.6 节中,您将学习如何通过“脚手架”单个页面来替换默认 Identity UI 中的页面。在第 14.5 节中,您将了解如何自定义 Razor 模板以在用户注册页面上生成不同的 HTML,在第 14.6 节中,将了解如何定制与 Razor 页面相关的逻辑。您将看到如何存储有关用户的其他信息(例如,用户的姓名或出生日期),以及如何为他们提供权限,以便您以后可以使用这些权限自定义应用程序的行为(例如,如果用户是 VIP)。
在我们具体研究 ASP.NET Core 身份系统之前,让我们先看看 ASP.NET Core 中的身份验证和授权,当您登录网站时会发生什么,以及如何设计应用程序以提供此功能。
14.1 认证和授权简介
当您向应用程序添加登录功能并基于当前登录用户控制对某些功能的访问时,您使用的是两个不同的安全方面:
- 身份验证(Authentication)——确定您是谁的过程
- 授权(Authorization)——决定你被允许做什么的过程
通常,您需要知道用户是谁,然后才能确定允许他们做什么,所以身份验证总是首先进行,然后是授权。在本章中,我们只关注身份验证;我们将在第 15 章讨论授权。在本节中,我将首先讨论 ASP.NET Core 如何看待用户,并介绍一些对身份验证至关重要的术语和概念。我总是觉得这是第一次学习身份验证时最难掌握的部分,所以我会慢慢来。
接下来,我们将了解登录到传统 Web 应用程序意味着什么。毕竟,您只在一个页面上提供密码并登录应用程序,应用程序如何知道后续请求来自您呢?
最后,我们将研究当您需要支持客户端应用程序和调用 Web API 的移动应用程序以及传统 Web 应用程序时,身份验证是如何工作的。许多概念是相似的,但支持多种类型的用户、传统应用程序、客户端应用程序和移动应用程序的需求导致了不同的替代解决方案。
14.1.1 了解 ASP.NET Core 中的用户和声明
ASP.NET Core 中融入了用户的概念。在第 3 章中,您已经了解到 HTTP 服务器 Kestrel 为它接收的每个请求创建一个 HttpContext 对象。此对象负责存储与该请求相关的所有详细信息,例如请求 URL、发送的任何标头、请求主体等。
HttpContext 对象还将请求的当前主体公开为 User 属性。这是 ASP.NET Core 用户提出请求的视图。任何时候,你的应用程序需要知道当前用户是谁,或者他们可以做什么,这些都可以查看 HttpContext.user 主体。
定义:您可以将委托人视为应用程序的用户。
在 ASP.NET Core 中,主体被实现为 ClaimsPrincipals,它有一个与之相关联的声明集合,如图14.1所示。
图14.1 主体是当前用户,实现为 ClaimsPrincipal。它包含描述用户的声明集合。
您可以将声明视为当前用户的属性。例如,您可以对电子邮件、姓名或出生日期等内容提出验证。
定义:声明是关于委托人的单一信息;它由声明类型和可选值组成。
声明也可以与权限和授权间接相关,因此您可以有一个名为 HasAdminAccess 或 IsVipCustomer 的声明。这些将以与用户主体相关联的声明完全相同的方式存储。
注意:早期版本的 ASP.NET 使用基于角色的安全方法,而不是基于声明的方法。ASP.NET Core 中使用的 ClaimsPrincipal 由于传统原因与此方法兼容,但您应该对新应用程序使用基于声明的方法。
Kestrel 为到达应用程序的每个请求分配一个用户主体。最初,该主体是一个通用的、匿名的、未经身份验证的主体,没有声明。您如何登录,ASP.NET Core 如何知道您已登录后续请求?
在下一节中,我们将了解使用 ASP.NET Core 的传统 Web 应用程序中的身份验证工作原理,以及登录用户帐户的过程。
14.1.2 ASP.NET Core 中的身份验证:服务和中间件
将身份验证添加到任何 Web 应用程序都涉及许多移动部件。无论您是构建传统的 Web 应用程序还是客户端应用程序,相同的一般过程都适用,尽管在实现方面通常存在差异,正如我将在第 14.1.3 节中讨论的:
- 客户端向应用程序发送标识当前用户的标识符和密码。例如,您可以发送电子邮件地址(标识符)和密码(加密)。
- 应用程序验证标识符是否与应用程序已知的用户相对应,以及相应的密码是否正确。
- 如果标识符和密码有效,应用程序可以为当前请求设置主体,但也需要为后续请求存储这些详细信息的方法。对于传统的 Web 应用程序,这通常是通过在 cookie 中存储用户主体的加密版本来实现的。
这是大多数 Web 应用程序的典型流程,但在本节中,我将介绍它在 ASP.NET Core 中的工作方式。整个过程是相同的,但很高兴看到这种模式如何适合 ASP.NET Core 应用程序的服务、中间件和 MVC 方面。当您以用户身份登录时,我们将逐步了解典型应用程序中的各种功能,这意味着什么,以及您如何以该用户身份发出后续请求。
登录到 ASP.NET CORE 应用程序
当你第一次到达网站并登录到传统的 Web 应用程序时,该应用程序会将你发送到登录页面,并要求你输入用户名和密码。将表单提交到服务器后,应用程序会将您重定向到一个新页面,您就神奇地登录了!图 14.2 显示了当您提交表单时 ASP.NET Core 应用程序的幕后情况。
图14.2 登录 ASP.NET Core 应用程序。SignInManager 负责将 HttpContext.User 设置为新主体,并将主体序列化为加密的 cookie。
这显示了从您在 Razor 页面上提交登录表单到重定向返回到浏览器的一系列步骤。当请求第一次到达时,Kestrel 创建一个匿名用户主体,并将其分配给 HttpContext.user 属性。然后将请求路由到 Login.cshtml Razor 页面,该页面使用模型绑定从请求中读取电子邮件和密码。
重要的工作发生在 SignInManager 服务中。这负责从数据库中加载具有所提供用户名的用户实体,并验证他们提供的密码是否正确。
警告:切勿将密码直接存储在数据库中。应该使用强大的单向算法对它们进行散列。ASP.NET Core 身份系统为您提供了这一点,但重申这一点总是明智的!
如果密码正确,SignInManager 将从数据库中加载的用户实体创建一个新的 ClaimsPrincipal,并添加适当的声明,如电子邮件地址。然后,它用新的经过身份验证的主体替换旧的匿名 HttpContext.User 主体。
最后,SignInManager 序列化主体,对其进行加密,并将其存储为 cookie。cookie 是一小段文本,与每个请求一起在浏览器和应用程序之间来回发送,由名称和值组成。
此身份验证过程说明了如何在用户首次登录应用程序时为请求设置用户,但后续请求如何?您只在首次登录应用程序时发送密码,那么应用程序如何知道是同一用户发出请求?
为后续请求验证用户
在多个请求中持久化身份的关键在于图 14.2 的最后一步,其中在 cookie 中序列化了主体。浏览器会自动将此 cookie 与向您的应用程序发出的所有请求一起发送,因此您无需在每次请求时提供密码。
ASP.NET Core 使用随请求发送的身份验证 cookie 来重新定义 ClaimsPrincipal,并为请求设置 HttpContext.User 主体,如图 14.3 所示。需要注意的重要一点是,此过程何时在 AuthenticationMiddleware 中发生。
图14.3 登录应用程序后的后续请求。随请求发送的 cookie 包含用户主体,该主体被验证并用于验证请求。
当收到包含身份验证 cookie 的请求时,Kestrel 将创建默认的、未经身份验证的匿名主体,并将其分配给 HttpContext.User 主体。此时运行的任何中间件,在 AuthenticationMiddleware 之前,都会将请求视为未经身份验证,即使存在有效的 cookie。
提示:如果您的身份验证系统看起来不工作,请仔细检查中间件管道。只有在身份验证后运行的中间件才会将请求视为已验证。
AuthenticationMiddleware 负责设置请求的当前用户。中间件调用身份验证服务,后者从请求中读取 cookie,对其进行解密,并对其进行反序列化,以获得用户登录时创建的 ClaimsPrincipal。
AuthenticationMiddleware 将 HttpContext.User 主体设置为新的身份验证主体。所有后续中间件现在都将知道请求的用户主体,并可以相应地调整其行为(例如,在主页上显示用户名,或限制对应用程序某些区域的访问)。
注意:如果请求包含身份验证 cookie,AuthenticationMiddleware 仅负责验证传入请求并设置 ClaimsPrincipal。它不负责将未经验证的请求重定向到登录页面或拒绝由 AuthorizationMiddleware 处理的未经授权的请求,如第 15 章所示。
到目前为止所描述的过程,即用户登录时,一个应用程序对用户进行身份验证,并设置一个 cookie,该 cookie 在后续请求时读取,这在传统 Web 应用程序中很常见,但这不是唯一的可能。在下一节中,我们将了解客户端和移动应用程序使用的 Web API 应用程序的身份验证,以及身份验证系统如何针对这些场景进行更改。
14.1.3 API 和分布式应用程序的认证
到目前为止,我所概述的流程适用于传统的 Web 应用程序,其中只有一个端点完成所有工作。它负责验证和管理用户,以及提供应用程序数据,如图 14.4 所示。
图14.4 传统应用程序通常处理应用程序的所有功能:业务逻辑、生成UI、身份验证和用户管理。
除了这个传统的 Web 应用程序之外,通常使用 ASP.NET Core 作为 Web API 来为移动和客户端 SPA 提供数据。类似地,后端微服务的趋势意味着即使是使用 Razor 的传统 Web 应用程序也需要在后台调用 API,如图 14.5 所示。
图14.5 现代应用程序通常需要公开移动和客户端应用程序的Web API,以及可能在后端调用API。当所有这些服务都需要验证和管理用户时,这在逻辑上变得复杂。
在这种情况下,您有多个应用程序和 API,所有这些都需要了解同一用户正在跨所有应用程序和应用程序接口发出请求。如果你保持以前的方法,每个应用程序都管理自己的用户,事情很快就会变得难以管理!
您需要复制应用程序和 API 之间的所有登录逻辑,还需要一些保存用户详细信息的中央数据库。用户可能需要多次登录才能访问服务的不同部分。除此之外,对于某些移动客户端,特别是在您向多个域发出请求的情况下(因为 cookie 只属于一个域),使用 cookie 会产生问题。
您如何改进这一点?典型的方法是提取所有应用程序和 API 通用的代码,并将其移动到身份提供者,如图 14.6 所示。
而不是直接登录到应用程序,应用程序重定向到身份提供商应用程序。用户登录到此身份提供者,后者将承载令牌传递回客户端,以指示用户是谁以及允许他们访问什么。客户端和应用程序可以将这些令牌传递给 API,以提供有关登录用户的信息,而无需重新验证或直接管理用户。
图14.6 另一种架构涉及使用中央身份提供者来处理系统的所有身份验证和用户管理。令牌在身份提供者、应用程序和API之间来回传递。
从表面上看,这种架构显然更加复杂,因为您已经将一个全新的服务(身份提供者)加入到了这一组合中,但从长远来看,这有很多优点:
- 用户可以在多个服务之间共享其身份。当您登录到中央身份提供商时,您实际上已登录到使用该服务的所有应用程序。这为您提供了单一登录体验,您不必一直登录到多个服务。
- 减少重复。所有登录逻辑都封装在身份提供程序中,因此您不需要向所有应用程序添加登录屏幕。
- 可以轻松添加新的提供程序。无论您使用身份提供者方法还是传统方法,都可以使用外部服务来处理用户的身份验证。例如,你会在允许你“使用 Facebook 登录”或“使用谷歌登录”的应用程序上看到这一点。如果使用集中式身份提供程序,则可以在一个地方处理添加对其他提供程序的支持,而无需显式配置每个应用程序和 API。
开箱即用,ASP.NET Core 支持这样的架构,并支持使用已发行的承载令牌,但它不支持在核心框架中发行这些令牌。这意味着您需要为身份提供者使用其他库或服务。
身份提供商的一种选择是将所有身份验证责任委托给第三方身份提供商,如 Facebook、Okta、Auth0 或 Azure Active Directory B2C。它们为您管理用户,因此用户信息和密码存储在其数据库中,而不是您自己的数据库中。这种方法的最大优点是,您不必担心确保客户数据安全;你可以很肯定,第三方会保护它,因为这是他们的全部业务。
提示:在可能的情况下,我建议采用这种方法,因为它将安全责任委托给其他人。如果您从未拥有过用户的详细信息,您就不会丢失这些信息!
另一个常见的选择是构建自己的身份提供者。这听起来可能需要很多工作,但多亏了像 OpenIddict 这样的优秀库(https://github.com/openidict)和 IdentityServer(https://docs.identityserver.io/),完全可以编写自己的身份提供程序来提供将由应用程序使用的承载令牌。
开始使用 OpenIddict 和 IdentityServer 的人经常忽略的一个方面是,它们不是预制的解决方案。作为开发人员,您需要编写知道如何创建新用户(通常在数据库中)、如何加载用户详细信息以及如何验证密码的代码。在这方面,创建身份提供者的开发过程类似于我在 14.1.2 节中讨论的具有 cookie 认证的传统 Web 应用程序。
事实上,你几乎可以把身份提供者想象成一个只有账户管理页面的传统 Web 应用。它还可以为其他服务生成令牌,但不包含其他特定于应用程序的逻辑。这两种方法都需要管理数据库中的用户,并为用户提供登录界面,这是本章的重点。
注意:连接应用程序和 API 以使用身份提供者可能需要对应用程序和身份提供者进行大量繁琐的配置。为了简单起见,本书重点介绍了使用 14.1.2 节中概述的流程的传统 Web 应用程序。ASP.NET Core 包含一个用于使用 IdentityServer 和客户端 SPA 的帮助程序库。有关如何开始的详细信息,请参阅 Microsoft 的“SPA 验证和授权”文档,网址为 http://mng.bz/w9Mq 以及 IdentityServer 文档:https://docs.identityserver.io/.
ASP.NET Core Identity(以下简称 Identity)是一种系统,它可以使构建应用程序(或身份提供商应用程序)的用户管理方面更简单。它处理将用户保存和加载到数据库的所有模板,以及许多安全性最佳实践,例如用户锁定、密码散列和双因素身份验证。
定义:双因素身份验证(2FA)是指除密码外,还需要额外的信息才能登录。例如,这可能涉及通过短信向用户的手机发送代码,或使用移动应用程序生成代码。
在下一节中,我将讨论 ASP.NET Core Identity 系统,它解决的问题,何时你想使用它,何时你不想使用它。
14.2 什么是 ASP.NET Core 身份?
无论您是使用 Razor Pages 编写传统的 Web 应用程序,还是使用 IdentityServer 这样的库设置新的身份提供者,您都需要一种方法来保存用户的详细信息,例如用户名和密码。
这可能看起来是一个相对简单的要求,但是,考虑到这与安全性和人们的个人信息有关,您必须正确处理。除了存储每个用户的声明外,重要的是使用强大的哈希算法存储密码,以允许用户在可能的情况下使用2FA,并防止暴力攻击,这是众多要求中的几个。尽管手动编写所有代码并构建自己的身份验证和会员系统是完全可能的,但我强烈建议您不要这样做。
我已经提到了第三方身份提供商,如 Auth0 或 Azure Active Directory B2C。这些是软件即服务(SaaS)解决方案,为您解决应用程序的用户管理和身份验证方面的问题。如果您正在将应用程序迁移到云端,那么像这样的解决方案很有意义。
如果您不能或不想使用这些第三方解决方案,我建议您考虑使用 ASP.NET Core Identity 系统在数据库中存储和管理用户详细信息。ASP.NET Core Identity 负责与身份验证相关的大部分模板,但它仍然很灵活,如果需要,还可以让您控制用户的登录过程。
注意:ASP.NET Core Identity 是 ASPNET Identity 的一种改进,经过一些设计改进并转换为与 ASP.NET Core 一起使用。
默认情况下,ASP.NET Core Identity 使用 EF Core 在数据库中存储用户详细信息。如果您已经在项目中使用了 EF Core,这是一个完美的选择。或者,也可以编写自己的商店,以另一种方式加载和保存用户详细信息。
Identity 负责用户管理的低级部分,如表 14.1 所示。正如你从这个列表中看到的,身份给了你很多,但并不是所有的东西!
表14.1 哪些服务由 ASP.NET Core Identity 处理
管理 ASP.NET Core 的认证 |
需要开发人员实施 |
用于存储用户和声明的数据库架构。 在数据库中创建用户。密码验证和规则。 处理用户帐户锁定(以防止暴力攻击)。 管理和生成 2FA 代码。正在生成密码重置令牌。将其他索赔保存到数据库。 管理第三方身份提供商(例如,Facebook、Google、Twitter)。 |
用于登录、创建和管理用户(Razor Pages 或控制器)的 UI。这包含在一个可选包中,该包提供了默认 UI。 发送电子邮件。 自定义用户声明(添加新声明)。 正在配置第三方身份提供程序。 |
最大的缺失是,您需要为应用程序提供所有 UI,以及将所有单独的 Identity 服务绑定在一起,以创建一个正常运行的登录过程。这是一个相当大的缺失,但它使身份系统非常灵活。
幸运的是,ASP.NET Core 包含一个助手 NuGet 库 Microsoft.AspNet-Core.Identity.UI,它为您免费提供了整个 UI 样板。这是30多个 RazorPages,具有登录、注册用户、使用双因素身份验证和使用外部登录提供商等功能。如果需要,您仍然可以自定义这些页面,但让整个登录过程开箱即用,不需要代码,这是一个巨大的胜利。我们将在第 14.3 节和第 14.4 节中查看这个库以及您如何使用它。
因此,无论您是创建应用程序还是向现有应用程序添加用户管理,我强烈建议使用默认 UI 作为起点。但问题仍然存在:你什么时候应该使用 Identity,什么时候应该考虑使用自己的 Identity?
我是 Identity 的忠实粉丝,所以我倾向于在大多数情况下使用它,因为它为您处理了很多与安全相关的事情,这些事情很容易搞砸。我听到过几个反对它的论点,其中一些是有效的,另一些则不那么有效:
- 我的应用程序中已经有了用户身份验证 —— 太好了!在这种情况下,你可能是对的,身份可能不是必要的。但是您的定制实现是否使用2FA?你有帐户锁定吗?如果没有,您需要添加它们,那么考虑Identity可能是值得的。
- 我不想使用 EF Core —— 这是一个合理的立场。您可以使用 Dapper、其他 ORM,甚至文档数据库来访问数据库。幸运的是,Identity 中的数据库集成是可插拔的,因此您可以更换EFCore集成,改用自己的数据库集成库。
- 我的用例太复杂了—— 因为 Identity 为身份验证提供了较低级别的服务,所以您可以随心所欲地编写这些内容。它也是可扩展的,因此,如果您需要在创建主体之前转换声明,可以这样做。
- 我不喜欢默认的 Razor Pages UI —— Identity 的默认 UI 是完全可选的。您仍然可以使用 Identity 服务和用户管理,但提供自己的 UI 用于登录和注册用户。然而,请注意,尽管这样做给了您很大的灵活性,但在用户管理系统中引入安全漏洞也是非常容易的,这是您最不希望出现安全漏洞的地方!
- 我没有使用 Bootstrap 来设置应用程序的样式 —— 默认的 Identity UI 使用 Bootstrap 作为样式框架,与默认的 ASP.NET Core 模板相同。不幸的是,你不能轻易改变这一点,所以如果你使用不同的框架,或者你需要自定义生成的 HTML,你仍然可以使用 Identity,但你需要提供自己的 UI。
- 我不想建立自己的身份系统 —— 我很高兴听到这个消息。使用 Azure Active Directory B2C 或 Auth0 这样的外部身份提供商是一种很好的方式,可以将与将用户个人信息存储到第三方相关的责任和风险转移。
每当您考虑将用户管理添加到 ASP.NET Core 应用程序时,我都建议将 Identity 视为一个很好的选择。在下一节中,我将通过使用默认的 Identity UI 创建一个新的 Razor Pages 应用程序来演示 Identity 所提供的功能。在第 14.4 节中,我们将使用该模板并将其应用于现有应用程序,在第 14.5 节和第 14.6 节中,您将看到如何覆盖默认页面。
14.3 创建使用 ASP.NET Core Identity 的项目
我已经概括介绍了身份验证和身份验证,但最好的方法是查看一些有效的代码。在本节中,我们将查看 ASP.NET Core 模板使用 Identity 生成的默认代码、项目的工作方式以及 Identity 的适用范围。
14.3.1 根据模板创建项目
首先,您将使用 Visual Studio 模板生成一个简单的 Razor Pages 应用程序,该应用程序使用 Identity 将单个用户帐户存储在数据库中。
提示:您可以通过运行 dotnet new Webapp-au Individual-uld,使用 .NET CLI 创建一个等效的项目。
若要使用 Visual Studio 创建模板,必须使用 VS 2019 或更高版本并安装 .NET 5.0 SDK:
- 选择“文件”>“新建”>“项目”,或从启动屏幕中选择“创建新项目”。
- 从模板列表中,选择 ASP.NET Core Web 应用程序,确保选择 C# 语言模板。
- 在下一个屏幕上,输入项目名称、位置和解决方案名称,然后单击“创建”。
- 选择 Web 应用程序模板,然后单击 Authentication下的 Change 以打开 Authentication 对话框,如图 14.7 所示。
- 选择“个人用户帐户”以创建配置有 EF Core 和 ASP.NET Core 标识的应用程序。单击“确定”。
- 单击“创建”以创建应用程序。Visual Studio 将自动运行 dotnet 还原以还原项目所需的所有 NuGet 包。
- 运行应用程序以查看默认应用程序,如图 14.8 所示。
图14.7 在 VS 2019 中选择新 ASP.NET Core 应用程序模板的身份验证模式
图14.8 带有个人帐户身份验证的默认模板与无身份验证模板类似,在页面右上方添加了一个 Login 小部件。
这个模板看起来应该很熟悉,有一个转折点:您现在有了“注册”和“登录”按钮!可以随意使用模板——创建用户、登录和注销——来体验应用程序。一旦你高兴了,看看模板生成的代码和它省去你编写的样板。
14.3.2 在解决方案资源管理器中探索模板
由模板生成的项目(如图 14.9 所示)与默认的无身份验证模板非常相似。这在很大程度上是由于默认的 UI 库,它带来了大量的功能,而不会让您接触到具体的细节。
图14.9 默认模板的项目布局。根据您的 Visual Studio 版本,确切的文件可能略有不同。
最大的添加是项目根目录中的 Areas 文件夹,其中包含 Identity 子文件夹。Areas 有时用于组织功能部分。每个区域都可以包含自己的 Pages 文件夹,这类似于应用程序中的主 Pages 文件夹。
定义:Areas 用于将 Razor Pages 分组为单独的层次结构,以便于组织。我很少使用区域,而是更喜欢在“Pages”文件夹中创建子文件夹。唯一的例外是 Identity UI,它默认使用单独的 Identity 区域。有关区域的更多详细信息,请参阅 Microsoft 的“ASP.NET Core中 的领域”文档:http://mng.bz/7Vw9.
Microsoft.AspNetCore.Identity.UI 包在“Identity”区域中创建 Razor Pages。您可以通过在应用程序的 Areas/Identity/Pages 文件夹中创建相应的页面来覆盖此默认 UI 中的任何页面。例如,如图 14.9 所示,默认模板添加了一个_ViewStart.cshtml 文件,该文件覆盖作为默认 UI 一部分包含的版本。此文件包含以下代码,用于将默认的 Identity UI Razor Pages 设置为使用项目的默认 _Layout.cshtml 文件:
@{
Layout = "/Pages/Shared/_Layout.cshtml";
}
此时,一些明显的问题可能是“您如何知道默认 UI 中包含什么”,以及“您可以覆盖哪些文件”?您将在第 14.5 节中看到这两个问题的答案,但通常情况下,您应该尽可能避免覆盖文件。毕竟,默认 UI 的目标是减少必须编写的代码量!
新项目模板中的 Data 文件夹包含应用程序的 EF Core DbContext,称为 ApplicationDbContext,以及用于配置数据库模式以使用 Identity 的迁移。我将在 14.3.3 节中更详细地讨论这个模式。
与无身份验证版本相比,此模板中包含的最后一个附加文件是部分 Razor 视图 Pages/Shared/LoginPartial.cshtml。它提供了图 14.8 中所示的注册和登录链接,并以默认 Razor 布局 _layout.chtml 呈现。
如果您查看 _LoginPartial.cshtml 内部,您可以通过使用标记帮助程序将 Razor Page 路径与{area}路由参数组合起来,了解路由如何与区域一起工作。例如,Login 链接使用 asp 区域属性指定 Razor Page/Account/Login 位于 Identity 区域:
<a asp-area="Identity" asp-page="/Account/Login">Login</a>
提示:通过将区域路由值设置为 Identity,可以在 Identity区域中引用 Razor Pages。您可以在生成链接的标记帮助程序中使用 asp 区域属性。
除了 ASP.NET Core Identity 提供的新文件外,还值得打开 Startup.cs 并查看其中的更改。最明显的变化是 ConfigureServices 中的附加配置,它添加了 Identity 所需的所有服务。
清单 14.1 向 ConfigureServices 添加 ASP.NET Core 标识服务
public void ConfigureServices(IServiceCollection services)
{
//ASPNETCore Identity使用EF Core,因此它包括标准的EF Core配置。
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
//添加标识系统(包括默认UI),并将用户类型配置为IdentityUser
services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true) //要求用户在登录前确认其帐户(通常通过电子邮件)
.AddEntityFrameworkStores<ApplicationDbContext>(); //配置Identity以将其数据存储在EF Core中
services.AddRazorPages();
}
AddDefaultIdentity() 扩展方法执行以下操作:
- 添加核心 ASP.NET Core 标识服务。
- 将应用程序用户类型配置为 IdentityUser。这是存储在数据库中的实体模型,表示应用程序中的“用户”。如果需要,可以扩展此类型,但这并不总是必要的,如第 14.6 节所示。
- 添加用于注册、登录和管理用户的默认 UI Razor Pages。
- 配置用于生成 2FA 和电子邮件确认令牌的令牌提供程序。
在“启动”中,Configure 方法还有一个非常重要的变化:
app.UseAuthentication();
这将 AuthenticationMiddleware 添加到管道中,以便您可以对传入请求进行身份验证,如图 14.3 所示。这个中间件的位置非常重要。它应该放在 UseRouting() 之后、UseAuthorization() 和UseEndpoints() 之前,如下面的列表所示。
//放置在UseAuthentication之前的中间件将看到所有匿名请求。 app.UseRouting(); //路由中间件基于请求URL确定请求哪个页面。 app.UseAuthentication(); //UseAuthentication应放在UseRouting之后。 app.UseAuthorization(); //UseAuthorization应放在UseAuthentication之后,以便它可以访问用户主体。 //在设置用户主体并应用授权之后,UseEndpoints应该是最后一个。 app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); }
如果您不使用这种特定顺序的中间件,您可能会遇到奇怪的错误,用户身份验证不正确,或者授权策略应用不正确。这个顺序是在模板中自动为您配置的,但如果您正在升级现有应用程序或移动中间件,则需要小心。
重要提示:UseAuthentication() 和UseAuthorization() 必须放在UseRouting() 和UseEndpoints() 之间。此外,UseAuthorization() 必须放在UseAuthentication() 之后。您可以在每个调用之间添加额外的中间件,只要保持整个中间件顺序即可。
现在您已经了解了 Identity 所做的添加,我们将更详细地了解数据库模式以及 Identity 如何在数据库中存储用户。
14.3.3 ASP.NET Core 身份数据模型
开箱即用,在默认模板中,Identity 使用 EF Core 存储用户帐户。它提供了一个可以从中继承的基本 DbContext,称为 IdentityDbContext ,它使用 IdentityUser 作为应用程序的用户实体。
在模板中,应用程序的 DbContext 称为 ApplicationDbContext。如果你打开这个文件,你会发现它非常稀疏;它继承了我前面描述的 IdentityDbContext 基类,就是这样。这个基类给你什么?最简单的方法是用迁移更新数据库并查看。
应用迁移的过程与第 12 章相同。确保连接字符串指向要创建数据库的位置,在项目文件夹中打开命令提示符,然后运行此命令以使用迁移更新数据库:
dotnet ef database update
如果数据库还不存在,CLI 将创建它。图 14.10 显示了默认模板的数据库外观。
图 14.10 ASP.NET Core Identity 使用的数据库模式
提示:如果在运行 dotnet-ef 命令后看到错误,请按照 12.3.1 节中的说明安装 .NET 工具。还要确保从项目文件夹而不是解决方案文件夹运行该命令。
那是很多桌子!您不需要直接与这些表交互 —— Identity 为您处理这些表 —— 但对它们的用途有一个基本的了解也无妨:
- EFMigrationsHistory —— 标准的 EF Core 迁移表,用于记录已应用的迁移。
- AspNetUsers —— 用户配置文件表本身。这是 IdentityUser 被序列化到的位置。我们稍后将仔细查看此表。
- AspNetUserClaims —— 与给定用户关联的声明。一个用户可以有许多声明,因此它被建模为多对一关系。
- AspNetUserLogins和AspNetUser令牌 —— 这些与第三方登录相关。配置后,用户可以使用 Google 或 Facebook 帐户登录(例如),而不是在应用程序上创建密码。
- AspNetUserRoles、AspNetRoles和AspNetRoleClaims —— 这些表有点像是 .NET 4.5 天前基于角色的旧权限模型遗留下来的,而不是基于声明的权限模型。这些表允许您定义多个用户可以属于的角色。每个角色都可以分配多个声明。当用户主体被分配该角色时,这些声明将被有效继承。
您可以自己探索这些表,但其中最有趣的是 AspNetUsers 表,如图 14.11 所示。
图14.11 AspNetUsers 表用于存储验证用户所需的所有详细信息。
AspNetUsers 表中的大多数列都与安全相关-用户的电子邮件、密码哈希、是否确认了电子邮件、是否启用了2FA等等。默认情况下,没有其他信息(如用户名)的列。
注意:从图 14.11 中可以看到,主键 Id 存储为字符串列。默认情况下,Identity 使用 Guid 作为标识符。要自定义数据类型,请参阅 Microsoft“ASP.NET Core 中的身份模型自定义”文档的“更改主键类型”部分:http://mng.bz/5jdB。
用户的任何其他属性都作为声明存储在与该用户关联的 AspNetUserClaims 表中。这允许您添加任意的附加信息,而无需更改数据库架构以适应它。要存储用户的出生日期吗?您可以向该用户添加声明,而无需更改数据库模式。当您向每个新用户添加 Name 声明时,您将在第 14.6 节中看到这一点。
注意:添加声明通常是扩展默认IdentityUser 的最简单方法,但您也可以直接向 IdentityUser 添加其他属性。这需要更改数据库,但在许多情况下仍然有用。您可以在此处阅读如何使用此方法添加自定义数据:http://mng.bz/Xd61.
了解 IdentityUser 实体(存储在 AspNetUsers 表中)和 ClaimsPrincipal(在 HttpContext.User 上公开)之间的区别很重要。当用户首次登录时,将从数据库中加载 IdentityUser。此实体与 AspNetUserClaims 表中用户的其他声明相结合,以创建 ClaimsPrincipal。正是此 ClaimsPrincipal 用于身份验证,并序列化到身份验证 cookie,而不是 IdentityUser。
有 Identity 使用的底层数据库模式的心理模型是很有用的,但在日常工作中,您不必直接与它交互,毕竟这就是 Identity 的用途!在下一节中,我们将查看规模的另一端——应用程序的 UI,以及使用默认 UI 可以获得什么。
14.3.4 与 ASP.NET Core 身份交互
您可能希望自己探索默认 UI,以了解各部分是如何组合在一起的,但在本节中,我将重点介绍您从中获得的内容,以及通常需要立即额外注意的领域。
默认 UI 的入口点是应用程序的用户注册页面,如图 14.12 所示。注册页面允许用户通过使用电子邮件和密码创建新的 IdentityUser 来注册您的应用程序。创建帐户后,用户将重定向到一个屏幕,指示他们应该确认电子邮件。默认情况下未启用电子邮件服务,因为这取决于您配置外部电子邮件服务。您可以在 Microsoft 的“ASP.NET Core 中的帐户确认和密码恢复”文档中阅读如何启用电子邮件发送,网址为 http://mng.bz/6gBo。一旦您对此进行了配置,用户将自动收到一封带有确认其帐户链接的电子邮件。
图14.12 使用默认Identity UI的用户注册流程。用户输入电子邮件和密码,然后重定向到“确认您的电子邮件”页面。默认情况下,这是一个占位符页面,但如果启用电子邮件确认,此页面将相应更新。
默认情况下,用户电子邮件必须是唯一的(不能有两个用户使用相同的电子邮件),密码必须满足各种长度和复杂性要求。您可以在 Startup.cs 中调用 AddDefaultIdentity() 的配置 lambda 中自定义这些选项和更多选项,如下表所示。
清单 14.3 自定义 Startup.cs 中 ConfigureServices 中的标识设置
services.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true; //要求用户在登录之前通过电子邮件确认其帐户。
options.Lockout.AllowedForNewUsers = true; //启用用户锁定,以防止针对用户密码的暴力攻击
//更新密码要求。目前的指导意见是需要长密码。
options.Password.RequiredLength = 12;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = false;
})
.AddEntityFrameworkStores<AppDbContext>();
用户注册应用程序后,需要登录,如图 14.13 所示。在登录页面的右侧,默认 UI 模板描述了开发人员如何配置外部登录提供商,如 Facebook 和 Google。这对您来说是有用的信息,但这也是您可能需要自定义默认UI模板的原因之一,如第 14.5 节所示。
图 14.13 使用现有用户登录并管理用户帐户。登录页面描述如何配置外部登录提供商,如 Facebook 和 Google。用户管理页面允许用户更改电子邮件和密码,并配置双因素身份验证(2FA)。
一旦用户登录,他们就可以访问身份 UI 的管理页面。这些功能允许用户更改电子邮件、更改密码、使用验证器应用程序配置2FA或删除所有个人数据。假设您已经配置了一个电子邮件发送服务,这些功能中的大多数功能都可以在您不费吹灰之力的情况下工作。
注:您可以通过启用 QR 码生成来改进 2FA 身份验证器页面,如 Microsoft 的“在 ASP.NET.Core 中为 TOTP 身份验证器应用进程启用 QR 码生成”文档:http://mng.bz/nM5a 中所述。
这涵盖了默认 UI 模板中的所有内容。它可能看起来有些微不足道,但它涵盖了几乎所有应用程序都通用的许多要求。尽管如此,您几乎总是希望定制一些东西:
- 配置电子邮件发送服务,以启用帐户确认和口令恢复,如 Microsoft 的“ASP.NET Core 中的帐户确认和密码恢复”文档所述:http://mng.bz/vzy7。
- 为启用 2FA 页面添加 QR 码生成器,如 Microsoft 的“在 ASP.NET Core 中为 TOTP 验证器应用程序启用 QR 码生成”文档所述:http://mng.bz/4Zmw。
- 自定义注册和登录页面以删除启用外部服务的文档链接。您将在第 14.5 节中了解如何执行此操作。或者,您可能希望完全禁用用户注册,如 Microsoft 的“ASP.NET Core 项目中的脚手架标识”文档所述:http://mng.bz/QmMG。
- 在注册页面上收集有关用户的其他信息。您将在第 14.6 节中了解如何执行此操作。
您可以通过多种方式扩展或更新身份系统,并提供了许多选项,因此我鼓励您浏览 Microsoft 的“ASP.NET Core 身份验证概述”,网址:http://mng.bz/XdGv 查看您的选项。在下一节中,您将看到如何实现另一个常见需求:向现有应用程序添加用户。
14.4 向现有项目添加 ASP.NET Core 标识
在本节中,我们将从第 12 章和第 13 章向配方应用程序添加用户。这是一个你想要添加用户功能的工作应用程序。在第 15 章中,我们将扩展这项工作,以限制对谁可以在应用程序上编辑食谱的控制。在本节结束时,您将拥有一个具有注册页面、登录屏幕和管理帐户屏幕的应用程序,就像默认模板一样。屏幕右上方还有一个持久的小部件,显示当前用户的登录状态,如图 14.14 所示。
图14.14 添加验证后的配方应用程序,显示了登录小部件。
如第 14.3 节所述,此时我不会自定义任何默认值,因此我们不会设置外部登录提供者、电子邮件确认或 2FA。我只关心将 ASP.NET Core 标识添加到已经使用 EF Core 的现有应用程序中。
提示:在将 Identity 添加到现有项目之前,确保您熟悉新项目模板是值得的。创建一个测试应用程序,并考虑设置外部登录提供商、配置电子邮件提供商和启用 2FA。这需要一点时间,但在将 Identity 添加到现有应用程序中时,它对于破解错误非常有用。
要将 Identity 添加到应用程序,您需要执行以下操作:
- 添加 ASP.NET Core Identity NuGet 包。
- 配置 Startup 以使用 AuthenticationMiddleware 并将 Identity 服务添加到 DI 容器。
- 使用 Identity 实体更新 EF Core 数据模型。
- 更新 Razor 页面和布局,以提供指向 Identity UI 的链接。
本节将依次处理这些步骤。在第 14.4 节末尾,您将成功将用户帐户添加到配方应用程序。
14.4.1 配置 ASP.NET Core 标识服务和中间件
通过引用两个 NuGet 包,您可以将具有默认 UI 的 ASP.NET Core Identity 添加到现有应用程序:
- Microsoft.AspNetCore.Identity EntityFrameworkCore —— 提供所有核心 Identity 服务并与 EF core 集成
- Microsoft.AspNetCore.Identity.UI —— 提供默认 UI Razor Pages
更新 project.csproj 文件以包含以下两个包:
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="5.0.0" />
这些软件包带来了在默认 UI 中添加 Identity 所需的所有附加依赖项。确保在将它们添加到项目后运行 dotnet 还原。
添加 Identity 包后,可以更新 Startup.cs 文件以包含 Identity 服务,如下所示。这类似于您在清单 14.1 中看到的默认模板设置,但请确保引用现有的 AppDbContext。
清单14.4 向配方应用程序添加 ASP.NET Core Identity 服务
public void ConfigureServices(IServiceCollection services)
{
//现有服务配置不变。
services.AddDbContext<AppDbContext>(options => options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
//将Identity服务添加到DI容器中,并使用自定义用户类型ApplicationUser
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<AppDbContext>(); //确保使用现有DbContext应用程序的名称
services.AddRazorPages(); services.AddScoped<RecipeService>();
}
这将添加所有必要的服务并配置 Identity 以使用 EF Core。我在这里引入了一种新类型 ApplicationUser,稍后我们将使用它来定制用户实体。您将在第 14.4.2 节中看到如何添加此类型。
配置 AuthenticationMiddleware 稍微简单一些:将其添加到 Configure 方法中的管道中。如清单 14.5 所示,我在 UseRouting() 之后、UseAuthorization() 之前添加了中间件。正如我在 14.3.2 节中提到的,在应用程序中使用中间件的这个顺序很重要。
//StaticFileMiddleware永远不会看到经过身份验证的请求,即使在您登录后也是如此。 app.UseRouting(); app.UseAuthentication(); //在UseRouting()之后和UseAuthorization之前添加AuthenticationMiddleware //AuthenticationMiddleware之后的中间件可以从HttpContext.user读取用户主体。 app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); }
您已将应用程序配置为使用 Identity,因此下一步是更新 EF Core 的数据模型。您已经在这个应用程序中使用了 EFCore,因此您需要更新数据库模式以包含 Identity 所需的表。
14.4.2 更新 EF 核心数据模型以支持身份
清单 14.4 中的代码不会编译,因为它引用了 ApplicationUser 类型,而该类型还不存在。使用以下行在 Data 文件夹中创建 ApplicationUser:
public class ApplicationUser : IdentityUser { }
在这种情况下,创建自定义用户类型并不是绝对必要的(例如,默认模板使用原始 IdentityUser),但我发现现在添加派生类型比以后如果需要为用户类型添加额外属性时尝试对其进行修改更容易。
在 14.3.3 节中,您看到 Identity 提供了一个名为 IdentityDbContext 的 DbContext,您可以从中继承。IdentityDbContext 基类包含使用 EF Core 存储用户实体所需的 DbSet<T>。
为 Identity 更新现有的 DbContext 很简单 —— 更新应用程序的 DbContext 以从 IdentityDbContext 继承,如下所示。在本例中,我们使用基本 Identity 上下文的通用版本,并提供 ApplicationUser 类型。
ApplicationUser> { //该类的其余部分保持不变。 public AppDbContext(DbContextOptions<AppDbContext> options): base(options) { } public DbSet<Recipe> Recipes { get; set; } }
实际上,通过以这种方式更新上下文的基类,您已经向 EFCore 的数据模型添加了大量新实体。正如您在第 12 章中看到的,每当 EFCore 的数据模型发生变化时,您都需要创建一个新的迁移并将这些变化应用到数据库中。
此时,您的应用程序应该进行编译,因此您可以使用
dotnet ef migrations add AddIdentitySchema
最后一步是更新应用程序的 RazorPages 和布局,以引用默认标识 UI。通常,在应用程序中添加 30 个新的 RazorPages 会是一项艰巨的工作,但使用默认的 Identity UI 会让它变得轻而易举。
14.4.3 更新 Razor 视图以链接到 Identity UI
从技术上讲,您不必更新 Razor Pages 来引用默认 UI 中包含的页面,但您可能希望至少将登录小部件添加到应用程序的布局中。您还需要确保 Identity Razor Pages 使用与应用程序其余部分相同的基本 Layout.cshtml。
我们将首先修复身份页面的布局。在“magic”路径 Areas/Identity/Pages/_ViewStart.cshtml 创建一个文件,并添加以下内容:
@{ Layout = "/Pages/Shared/_Layout.cshtml"; }
这会将 Identity 页的默认布局设置为应用程序的默认布局。接下来,在 Pages/Shared 中添加一个 _LoginPartial.cshtml 文件来定义登录小部件,如下面的列表所示。这与默认模板生成的模板几乎相同,但使用我们的自定义 ApplicationUser 而不是默认 IdentityUser。
清单 14.7 向现有应用程序添加 _LoginPartial.cshtml
@using Microsoft.AspNetCore.Identity
<!--更新到包含ApplicationUser的项目命名空间-->
@using RecipeApplication.Data;
<!--默认模板使用IdentityUser。更新以改用ApplicationUser。-->
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello User.Identity.Name!</a>
</li>
<li class="nav-item">
<form class="form-inline" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" asp-area="Identity" method="post" >
<button class="nav-link btn btn-link text-dark" type="submit">Logout</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
</li>
}
</ul>
此部分显示用户的当前登录状态,并提供注册或登录的链接
<partial name="_LoginPartial" />
在应用程序的主布局文件 _layout.cshtml 中。
现在,您已经将 Identity 添加到现有应用程序中。默认的 UI 使这一操作相对简单,您可以通过构建自己的 UI 来确保没有引入任何安全漏洞!
正如我在第 14.3.4 节中所描述的,默认 UI 没有提供一些您需要自己实现的功能,例如电子邮件确认和 2FA 二维码生成。你经常会发现你想在这里或那里更新一个页面。在下一节中,我将展示如何在默认 UI 中替换页面,而不必自己重新构建整个 UI。
14.5 在 ASP.NET Core Identity 的默认 UI 中自定义页面
在本节中,您将学习如何使用“脚手架”替换默认 Identity UI 中的各个页面。您将学习如何构建页面,使其覆盖默认 UI,从而允许您自定义 Razor 模板和 PageModel 页面处理程序。让 Identity 为您的应用程序提供整个 UI 在理论上是很好的,但在实践中存在一些问题,正如您在 14.3.4 节中已经看到的那样。默认 UI 提供了尽可能多的功能,但有些东西可能需要调整。例如,登录和注册页面都描述了如何为 ASP.NET Core 应用程序配置外部登录提供程序,如图 14.12 和 14.13 所示。这对作为开发人员的您来说是有用的信息,但不是您想向用户展示的信息。另一个经常被引用的要求是希望改变一个或多个页面的外观和感觉。
幸运的是,默认的 Identity UI 设计为可增量替换,因此您可以覆盖单个页面,而无需自己重新构建整个 UI。除此之外,Visual Studio 和 .NET CLI 都具有允许您在默认 UI 中构建任何(或所有)页面的功能,因此当您想要调整页面时,不必从头开始。
定义:脚手架是在项目中生成作为自定义基础的文件的过程。Identity scaffolder 将 Razor Pages 添加到正确的位置,以便使用默认 UI 覆盖等效页面。最初,框架页面中的代码与默认 Identity UI 中的代码匹配,但您可以自由自定义它。
作为您可以轻松进行更改的示例,我们将构建注册页面并删除有关外部提供者的附加信息部分。以下步骤描述如何在 Visual Studio 中构建 Register.cshtml 页面。或者,您可以使用 .NET CLI 构建注册页。
- 如果尚未添加 Microsoft.VisualStudio.Web.CodeGeneration.Design 和 Microsoft.EntityFrameworkCore.Tools NuGet 包,请将它们添加到项目文件中。Visual Studio 使用这些包来正确地构建应用程序,如果没有它们,您可能会在运行构建程序时出错。
<PackageReference Version="5.0.0" Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" />
<PackageReference Version="5.0.0" Include="Microsoft.EntityFrameworkCore.Tools" /> - 确保您的项目已生成 —— 如果未生成,则在添加新页面之前,脚手架将失败。
- 右键单击项目并选择“添加”>“新建脚手架项目”。
- 在选择对话框中,从类别中选择“标识”,然后单击“添加”。
- 在 AddIdentity 对话框中,选择 Account/Register 页面,并选择应用程序的 AppDbContext 作为 Data 上下文类,如图 14.15 所示。单击“添加”以构建页面。
图 14.15 使用 Visual Studio 构建 Identity 页。生成的 Razor Pages 将覆盖默认 UI 提供的版本。
Visual Studio 构建应用程序,然后为您生成 Register.cshtml 页面,并将其放入 Areas/Identity/Pages/Account 文件夹中。它还生成了几个支持文件,如图 14.16 所示。这些是确保新的 Register.cshtml 页面可以引用默认 Identity UI 中的其余页面所必需的。
图 14.16 脚手架生成 Register.cshtml Razor 页面,以及与默认 Identity UI 的其余部分集成所需的支持文件。
我们对 Register.cshtml 页面很感兴趣,因为我们希望自定义 Register 页面上的 UI,但如果您查看代码隐藏页面 Register.cshhtml.cs,就会发现默认 Identity UI 隐藏了多少复杂性。这不是不可逾越的(我们将在第 14.6 节中定制页面处理程序),但如果您能够帮助,避免编写代码总是很好的。
现在,您的应用程序中有了 Razor 模板,您可以根据自己的需要对其进行自定义。缺点是现在维护的代码比使用默认 UI 时多。您不必编写它,但当 ASP.NET Core 的新版本发布时,您可能仍然需要更新它。
我喜欢在重写默认的 Identity UI 时使用一些技巧。在许多情况下,您实际上不想更改 Razor page 的页面处理程序,只想更改 Razur 视图。您可以通过删除 Register.cshtml.cs PageModel 文件,并将新构建的 .cshtml 文件指向原始 PageModel(这是默认 UI NuGet 包的一部分)来实现这一点。
这种方法的另一个好处是,您可以删除一些自动生成的其他文件。总共,您可以进行以下更改:
- 更新 Register.cshtml 中的 @model 指令,以指向默认 UI 页面-模型:
@model Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal.RegisterModel - 将 Areas/Identity/Pages/ViewImports.cshtml 更新为以下内容:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers - 删除 Areas/Identity/Pages/IdentityHostingStartup.cs。
- 删除 Areas/Identity/Pages/_ValidationScriptsPartial.cshtml。
- 删除 Areas/Identity/Pages/Account/Register.cshtml.cs。
- 删除 Areas/Identity/Pages/Account/ViewImports.cshtml。
完成所有这些更改后,您将获得两全其美的效果。您可以更新默认的 UI Razor Pages HTML,而无需承担维护默认 UI 代码的责任。
提示:在本书的源代码中,您可以看到这些变化,其中 Register 视图已被自定义,以删除对外部身份提供者的引用。
不幸的是,并不总是可以使用默认的 UI PageModel。有时,您需要更新页面处理程序,例如当您想要更改 Identity 区域的功能时,而不仅仅是外观。一个常见的需求是需要存储有关用户的附加信息,您将在下一节中看到。
14.6 管理用户:向用户添加自定义数据
在本节中,您将了解如何在创建用户时通过向 AspNetUserClaims 表中添加其他声明来自定义分配给用户的 ClaimsPrincipal。您还将看到如何在 Razor 页面和模板中访问这些声明。
通常,将 Identity 添加到应用程序后的下一步是自定义它。默认模板只需要电子邮件和密码即可注册。如果您需要更多详细信息,例如用户的友好名称,该怎么办?此外,我已经提到,我们使用声明是为了安全,所以如果您想向某些用户添加一个名为 IsAdmin 的声明呢?
您知道每个用户主体都有一个声明集合,因此,从概念上讲,添加任何声明都只需要将其添加到用户集合中。有两种主要情况下,您希望向用户授予声明:
- 对于每个用户,当他们第一次在应用程序上注册时。例如,您可能希望将“名称”字段添加到“注册”表单中,并在用户注册时将其添加为声明。
- 在用户注册后手动执行。这对于用作权限的声明是常见的,其中现有用户在应用程序上注册后可能希望向特定用户添加 IsAdmin 声明。
在本节中,我将向您展示第一种方法,即在创建新声明时自动向用户添加新声明。后一种方法更灵活,最终是许多应用程序,特别是业务线应用程序所需要的方法。幸运的是,在概念上没有什么困难;它需要一个简单的 UI,允许您查看用户并通过我将在这里展示的相同机制添加声明。
提示:另一种常见的方法是自定义 IdentityUser 实体,例如添加 Name 属性。如果您想让用户编辑该属性,这种方法有时更容易使用。Microsoft 的“在 ASP.NET Core 项目中向 Identity 添加、下载和删除自定义用户数据”文档描述了实现这一目标所需的步骤:http://mng.bz/aoe7。
假设您想向用户添加一个名为 FullName 的新 Claim。典型的方法如下:
- 如您在 14.5 节中所做的那样,搭建 Register.cshtmlRazor 页面。
- 将“名称”字段添加到 Register.cshtml.cs PageModel 中的 InputModel。
- 将“名称”输入字段添加到 Register.cshtmlRazor 视图模板。
- 通过在 UserManager<ApplicationUser>上调用 CreateAsync,在 OnPost() 页面处理程序中创建新的 ApplicationUser 实体。
- 通过调用UserManager.AddClaimAsync()向用户添加新的Claim。
- 继续之前的方法,发送确认电子邮件或在不需要电子邮件确认的情况下登录用户。
步骤 1–3 非常简单,只需要使用新字段更新现有模板即可。步骤 4–6 都发生在 OnPost() 页面处理程序中的 Register.cshtml.cs 中,总结如下列表。实际上,页面处理程序有更多的错误检查和样板;我们这里的重点是将额外的 Claim 添加到 ApplicationUser 的附加行。
OnPostAsync(string returnUrl = null) { //照常创建ApplicationUser实体的实例 if (ModelState.IsValid) { var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email }; //验证提供的密码是否有效,并在数据库中创建用户 var result = await _userManager.CreateAsync( user, Input.Password ); if (result.Succeeded) { var claim = new Claim("FullName", Input.Name); //使用字符串名称“FullName”和提供的值创建声明 await _userManager.AddClaimAsync(user, claim); //将新声明添加到ApplicationUser集合 //如果已配置电子邮件发件人,则向用户发送确认电子邮件 var code = await _userManager .GenerateEmailConfirmationTokenAsync(user); await _emailSender.SendEmailAsync( Input.Email, "Confirm your email", code ); await _signInManager.SignInAsync(user); //通过设置主体将包括自定义声明来登录用户 return LocalRedirect(returnUrl); } } //创建用户时出现问题。将错误添加到ModelState,然后重新显示页面。 foreach (var error in result.Errors) { ModelState.AddModelError( string.Empty, error.Description); } return Page(); }
提示:清单 14.8 显示了如何在注册时添加额外的声明,但您通常需要在以后添加额外的数据,例如与权限相关的声明或其他信息。您需要创建额外的端点和页面来添加此数据,并根据需要保护页面(例如,这样用户就不能更新自己的权限)。
这是添加新声明所需的全部内容,但您当前没有在任何地方使用它。如果你想显示它怎么办?嗯,您已经向 ClaimsPrincipal 添加了一个声明,当您调用 SignInAsync 时,该声明被分配给 HttpContext.User 属性。这意味着您可以在任何有权访问 claimsPrincipal 的地方检索索赔,包括在页面处理程序和视图模板中。例如,您可以使用以下语句在Razor模板中的任何位置显示用户的 FullName 声明:
@User.Claims.FirstOrDefault(x=>x.Type == "FullName")?.Value
这将查找当前用户主体上类型为“FullName”的第一个声明,并打印分配的值(或者如果未找到该声明,则不打印任何内容)。Identity 系统甚至包括一个方便的扩展方法,用于整理此 LINQ 表达式(在 system.Security.Claims 命名空间中找到):
@User.FindFirstValue("FullName")
有了最后一个花絮,我们已经到了本章关于 ASP.NET Core 身份的结尾。我希望您已经意识到使用 Identity 可以节省您的工作量,特别是当您使用默认的 Identity UI 包时。
向应用程序添加用户帐户和身份验证通常是进一步定制应用程序的第一步。一旦您进行了身份验证,您就可以进行授权,这允许您根据当前用户锁定应用程序中的某些操作。在下一章中,您将了解 ASP.NET Core 授权系统,以及如何使用它来定制应用程序;特别是配方应用程序,进展顺利!
总结
- 身份验证是确定你是谁的过程,而授权是确定你被允许做什么的过程。在应用授权之前,你需要对用户进行身份验证。
- ASP.NET Core 中的每个请求都与一个用户(也称为主体)相关联。默认情况下,在没有身份验证的情况下,这是一个匿名用户。您可以使用索赔主体根据提出请求的人而采取不同的行为。
- 请求的当前主体在 HttpContext.User 上公开。您可以从 Razor Pages 和视图访问此值,以查找用户的属性,例如用户的 ID、名称或电子邮件。
- 每个用户都有一组声明。这些声明是关于用户的单个信息。声明可以是物理用户的属性,如名称和电子邮件,也可以与用户拥有的属性相关,如 HasAdminAccess 或 IsVipCustomer。
- 早期版本的 ASP.NET 使用角色而不是声明。如果需要,您仍然可以使用角色,但应尽可能使用声明。
- ASP.NET Core 中的身份验证由 AuthenticationMiddleware 和许多身份验证服务提供。这些服务负责在用户登录时设置当前主体,将其保存到 cookie 中,并在后续请求时从 cookie 加载主体。
- AuthenticationMiddleware 是通过调用中间件管道中的 UseAuthentication() 添加的。这必须放在调用 UseRouting() 之后、UseAuthorization() 和 UseEndpoints() 之前。
- ASP.NET Core 支持使用承载令牌来验证 API 调用,并包括用于配置 IdentityServer 的助手库。有关详细信息,请参阅 Microsoft 的“SPA 认证和授权”文档:http://mng.bz/go0V.
- ASP.NET Core Identity 处理将用户存储在数据库中所需的低级服务,确保其密码安全存储,以及用户登录和注销。您必须自己为功能提供 UI,并将其连接到 Identity 子系统。
- Microsoft.AspNetCore.Identity.UI 包为 Identity 系统提供默认 UI,并包括电子邮件确认、2FA 和外部登录提供程序支持。您需要进行一些额外的配置以启用这些功能。
- 具有个人帐户身份验证的 Web 应用程序的默认模板使用 ASP.NET Core 标识将用户存储在具有 EF Core 的数据库中。它包括将 UI 连接到 Identity 系统所需的所有样板代码。
- 您可以使用 UserManager<T>类创建新的用户帐户,从数据库中加载它们,并更改它们的密码。SignInManager<T>用于通过为请求分配主体并设置身份验证 cookie 来登录和注销用户。默认 UI 为您使用这些类,以方便用户注册和登录。
- 您可以通过从 IdentityDbContext<TUser>派生来更新 EF Core DbContext 以支持 Identity,其中 TUser 是从 IdentityUser 派生的类。
- 您可以使用 UserManager<TUser>.add-ClaimAsync(TUser user,Claim Claim)方法向用户添加其他声明。当用户登录到您的应用程序时,这些声明将添加到 HttpContext.User 对象中。
- 声明由类型和值组成。这两个值都是字符串。您可以为 ClaimTypes 类上公开的类型使用标准值,例如 ClaimTypes.GivenName 和 ClaimTypes.FirstName,也可以使用自定义字符串,例如“FullName”。