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