本章包括

  • ASP.NET Core中web应用程序的身份验证工作原理
  • 使用ASP.NET Core标识系统创建项目
  • 向现有web应用添加用户功能
  • 自定义默认ASP.NET Core标识UI

像ASPNETCore这样的web框架的卖点之一是能够提供一个动态应用程序,该应用程序可为个人用户定制。许多应用程序都有一个服务“帐户”的概念,您可以“登录”到该服务并获得不同的体验。

根据服务的不同,帐户会为您提供不同的功能:在某些应用程序上,您可能需要登录才能访问其他功能,在其他应用程序上您可能会看到建议的文章。在电子商务应用程序上,您可以下单并查看过去的订单;在Stack Overflow上,您可以发布问题和答案;而在新闻网站上,您可能会根据以前看过的文章获得定制体验。

当您考虑向应用程序添加用户时,通常需要考虑两个方面:

  • Authentication——创建用户并让他们登录应用程序的过程
  • Authorization——根据当前登录的用户自定义体验并控制用户可以做什么

在这一章中,我将讨论第一点,身份验证和成员资格,在下一章中我将讨论第二点,授权。在第14.1节中,我将讨论身份验证和授权之间的区别、身份验证在传统ASPNETCore web应用程序中的工作方式,以及如何对系统进行架构以提供登录功能。

我还谈到了传统web应用程序和客户端或移动web应用程序使用的web API在身份验证方面的典型差异。本书重点介绍了用于身份验证的传统web应用程序,但许多原则都适用于两者。

在第14.2节中,我介绍了一个名为ASPNETCore Identity(简称Identity)的用户管理系统。Identity与EF Core集成,并提供创建和管理用户、存储和验证密码以及用户登录和退出应用程序的服务。

在第14.3节中,您将使用包含ASPNETCore标识的默认模板创建一个应用程序。这将为您提供一个应用程序,让您了解Identity提供的功能以及它没有的所有功能。

创建一个应用程序非常有助于了解各个部分是如何结合在一起的,但您通常需要向现有应用程序添加用户和身份验证。在第14.4节中,您将看到将ASPNETCore Identity添加到现有应用程序所需的步骤:第12章和第13章中的配方应用程序。

在第14.5节和第14.6节中,您将学习如何通过“脚手架”单个页面来替换默认Identity UI中的页面。在第14.5节中,您将了解如何自定义Razor模板以在用户注册页面上生成不同的HTML,在第14.6节中,将了解如何定制与Razor页面相关的逻辑。您将看到如何存储有关用户的其他信息(例如,用户的姓名或出生日期),以及如何为他们提供权限,以便您以后可以使用这些权限自定义应用程序的行为(例如,如果用户是VIP)。

在我们具体研究ASPNETCore身份系统之前,让我们先看看ASPNETCore中的身份验证和授权,当您登录网站时会发生什么,以及如何设计应用程序以提供此功能。

14.1 认证和授权简介

当您向应用程序添加登录功能并基于当前登录用户控制对某些功能的访问时,您使用的是两个不同的安全方面:

  • 身份验证(Authentication)——确定您是谁的过程
  • 授权(Authorization)——决定你被允许做什么的过程

通常,您需要知道用户是谁,然后才能确定允许他们做什么,所以身份验证总是首先进行,然后是授权。在本章中,我们只关注身份验证;我们将在第15章讨论授权。在本节中,我将首先讨论ASPNETCore如何看待用户,并介绍一些对身份验证至关重要的术语和概念。我总是觉得这是第一次学习身份验证时最难掌握的部分,所以我会慢慢来。

接下来,我们将了解登录到传统web应用程序意味着什么。毕竟,您只在一个页面上提供密码并登录应用程序,应用程序如何知道后续请求来自您?

最后,我们将研究当您需要支持客户端应用程序和调用Web API的移动应用程序以及传统Web应用程序时,身份验证是如何工作的。许多概念类似,但支持多种类型的用户、传统应用程序、客户端应用程序和移动应用程序的需求导致了替代解决方案。

14.1.1 了解ASPNETCore中的用户和声明

ASPNETCore中融入了用户的概念。在第3章中,您了解到HTTP服务器Kestrel为它接收的每个请求创建一个HttpContext对象。此对象负责存储与该请求相关的所有详细信息,例如请求URL、发送的任何标头、请求主体等。

HttpContext对象还将请求的当前主体公开为User属性。这是ASPNETCore用户提出请求的视图。任何时候,你的应用程序需要知道当前用户是谁,或者他们可以做什么,它都可以查看HttpContext.user主体。

定义:您可以将委托人视为应用程序的用户。

在ASPNETCore中,主体被实现为ClaimsPrincipals,它有一个与之相关联的声明集合,如图14.1所示。

图14.1 主体是当前用户,实现为ClaimsPrincipal。它包含描述用户的声明集合。

您可以将声明视为当前用户的属性。例如,您可以对电子邮件、姓名或出生日期等内容提出索赔。

定义:声明是关于委托人的单一信息;它由声明类型和可选值组成。

声明也可以与权限和授权间接相关,因此您可以有一个名为HasAdminAccess或IsVipCustomer的声明。这些将以与用户主体相关联的声明完全相同的方式存储。

注意:早期版本的ASPNET使用基于角色的安全方法,而不是基于声明的方法。ASPNETCore中使用的ClaimsPrincipal由于传统原因与此方法兼容,但您应该对新应用程序使用基于声明的方法。

Kestrel为到达应用程序的每个请求分配一个用户主体。最初,该主体是一个通用的、匿名的、未经身份验证的主体,没有声明。您如何登录,ASPNETCore如何知道您已登录后续请求?

在下一节中,我们将了解使用ASPNETCore的传统web应用程序中的身份验证工作原理,以及登录用户帐户的过程。

14.1.2 ASPNETCore中的身份验证:服务和中间件

将身份验证添加到任何web应用程序都涉及许多移动部件。无论您是构建传统的web应用程序还是客户端应用程序,相同的一般过程都适用,尽管在实现方面通常存在差异,正如我将在第14.1.3节中讨论的:

  1. 客户端向应用程序发送标识当前用户的标识符和密码。例如,您可以发送电子邮件地址(标识符)和密码(机密)。
  2. 应用程序验证标识符是否与应用程序已知的用户相对应,以及相应的秘密是否正确。
  3. 如果标识符和密码有效,应用程序可以为当前请求设置主体,但也需要为后续请求存储这些详细信息的方法。对于传统的web应用程序,这通常是通过在cookie中存储用户主体的加密版本来实现的。

这是大多数web应用程序的典型流程,但在本节中,我将介绍它在ASPNETCore中的工作方式。整个过程是相同的,但很高兴看到这种模式如何适合ASPNETCore应用程序的服务、中间件和MVC方面。当您以用户身份登录时,我们将逐步了解典型应用程序中的各种功能,这意味着什么,以及您如何以该用户身份发出后续请求。

登录到ASPNETCORE应用程序

当你第一次到达网站并登录到传统的web应用程序时,该应用程序会将你发送到登录页面,并要求你输入用户名和密码。将表单提交到服务器后,应用程序会将您重定向到一个新页面,您就神奇地登录了!图14.2显示了当您提交表单时ASPNETCore应用程序的幕后情况。

图14.2 登录ASPNETCore应用程序。SignInManager负责将HttpContext.User设置为新主体,并将主体序列化为加密的cookie。

这显示了从您在Razor页面上提交登录表单到重定向返回到浏览器的一系列步骤。当请求第一次到达时,Kestrel创建一个匿名用户主体,并将其分配给HttpContext.user属性。然后将请求路由到Login.cshtml Razor页面,该页面使用模型绑定从请求中读取电子邮件和密码。

重要的工作发生在SignInManager服务中。这负责从数据库中加载具有所提供用户名的用户实体,并验证他们提供的密码是否正确。

警告:切勿将密码直接存储在数据库中。应该使用强大的单向算法对它们进行散列。ASPNETCore身份系统为您提供了这一点,但重申这一点总是明智的!

如果密码正确,SignInManager将从数据库中加载的用户实体创建一个新的ClaimsPrincipal,并添加适当的声明,如电子邮件地址。然后,它用新的经过身份验证的主体替换旧的匿名HttpContext.User主体。

最后,SignInManager序列化主体,对其进行加密,并将其存储为cookie。cookie是一小段文本,与每个请求一起在浏览器和应用程序之间来回发送,由名称和值组成。

此身份验证过程说明了如何在用户首次登录应用程序时为请求设置用户,但后续请求如何?您只在首次登录应用程序时发送密码,那么应用程序如何知道是同一用户发出请求?

为后续请求验证用户

在多个请求中持久化身份的关键在于图14.2的最后一步,其中在cookie中序列化了主体。浏览器会自动将此cookie与向您的应用程序发出的所有请求一起发送,因此您无需在每次请求时提供密码。

ASPNETCore使用随请求发送的身份验证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应用程序之外,通常使用ASPNETCore作为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。

开箱即用,ASPNETCore支持这样的架构,并支持使用已发行的承载令牌,但它不支持在核心框架中发行这些令牌。这意味着您需要为身份提供者使用其他库或服务。

身份提供商的一种选择是将所有身份验证责任委托给第三方身份提供商,如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应用程序。ASPNETCore包含一个用于使用IdentityServer和客户端SPA的帮助程序库。有关如何开始的详细信息,请参阅Microsoft的“SPA验证和授权”文档,网址为http://mng.bz/w9Mq以及IdentityServer文档:https://docs.identityserver.io/.

ASPNETCore Identity(以下简称Identity)是一种系统,它可以使构建应用程序(或身份提供商应用程序)的用户管理方面更简单。它处理将用户保存和加载到数据库的所有模板,以及许多安全性最佳实践,例如用户锁定、密码散列和双因素身份验证。

定义:双因素身份验证(2FA)是指除密码外,还需要额外的信息才能登录。例如,这可能涉及通过短信向用户的手机发送代码,或使用移动应用程序生成代码。

在下一节中,我将讨论ASPNETCore Identity系统,它解决的问题,何时你想使用它,何时你不想使用它。

14.2 什么是ASPNETCore身份?

无论您是使用Razor Pages编写传统的web应用程序,还是使用IdentityServer这样的库设置新的身份提供者,您都需要一种方法来保存用户的详细信息,例如用户名和密码。

这可能看起来是一个相对简单的要求,但是,考虑到这与安全性和人们的个人信息有关,您必须正确处理。除了存储每个用户的声明外,重要的是使用强大的哈希算法存储密码,以允许用户在可能的情况下使用2FA,并防止暴力攻击,这是众多要求中的几个。尽管手动编写所有代码并构建自己的身份验证和会员系统是完全可能的,但我强烈建议您不要这样做。

我已经提到了第三方身份提供商,如Auth0或Azure Active Directory B2C。这些是软件即服务(SaaS)解决方案,为您解决应用程序的用户管理和身份验证方面的问题。如果您正在将应用程序迁移到云端,那么像这样的解决方案很有意义。

如果您不能或不想使用这些第三方解决方案,我建议您考虑使用ASPNETCore Identity系统在数据库中存储和管理用户详细信息。ASPNETCore Identity负责与身份验证相关的大部分模板,但它仍然很灵活,如果需要,还可以让您控制用户的登录过程。

注意:ASPNETCore Identity是ASPNET Identity的一种改进,经过一些设计改进并转换为与ASPNETCore一起使用。

默认情况下,ASPNETCore Identity使用EF Core在数据库中存储用户详细信息。如果您已经在项目中使用了EF Core,这是一个完美的选择。或者,也可以编写自己的商店,以另一种方式加载和保存用户详细信息。

Identity负责用户管理的低级部分,如表14.1所示。正如你从这个列表中看到的,身份给了你很多,但并不是所有的东西!

表14.1 哪些服务由ASPNETCore Identity处理

管理ASPNETCore的认证

需要开发人员实施

用于存储用户和声明的数据库架构。

在数据库中创建用户。密码验证和规则。

处理用户帐户锁定(以防止暴力攻击)。

管理和生成2FA代码。正在生成密码重置令牌。将其他索赔保存到数据库。

管理第三方身份提供商(例如,Facebook、Google、Twitter)。

用于登录、创建和管理用户(Razor Pages或控制器)的UI。这包含在一个可选包中,该包提供了默认UI。

发送电子邮件。

自定义用户声明(添加新声明)。

正在配置第三方身份提供程序。

最大的缺失是,您需要为应用程序提供所有UI,以及将所有单独的Identity服务绑定在一起,以创建一个正常运行的登录过程。这是一个相当大的缺失,但它使身份系统非常灵活。

幸运的是,ASPNETCore包含一个助手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 Identity为身份验证提供了较低级别的服务,所以您可以随心所欲地编写这些内容。它也是可扩展的,因此,如果您需要在创建主体之前转换声明,可以这样做。
  • 我不喜欢默认的Razor Pages UI——Identity的默认UI是完全可选的。您仍然可以使用Identity服务和用户管理,但提供自己的UI用于登录和注册用户。然而,请注意,尽管这样做给了您很大的灵活性,但在用户管理系统中引入安全漏洞也是非常容易的,这是您最不希望出现安全漏洞的地方!
  • 我没有使用Bootstrap来设置应用程序的样式-默认的Identity UI使用Bootstrab作为样式框架,与默认的ASPNETCore模板相同。不幸的是,你不能轻易改变这一点,所以如果你使用不同的框架,或者你需要自定义生成的HTML,你仍然可以使用Identity,但你需要提供自己的UI。
  • 我不想建立自己的身份系统-我很高兴听到这个消息。使用Azure Active Directory B2C或Auth0这样的外部身份提供商是一种很好的方式,可以将与将用户个人信息存储到第三方相关的责任和风险转移。

每当您考虑将用户管理添加到ASPNETCore应用程序时,我都建议将Identity视为一个很好的选择。在下一节中,我将通过使用默认的Identity UI创建一个新的Razor Pages应用程序来演示Identity所提供的功能。在第14.4节中,我们将使用该模板并将其应用于现有应用程序,在第14.5节和第14.6节中,您将看到如何覆盖默认页面。

14.3 创建使用ASPNETCore标识的项目

我已经概括介绍了身份验证和身份验证,但最好的方法是查看一些有效的代码。在本节中,我们将查看ASPNETCore模板使用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:

  1. 选择“文件”>“新建”>“项目”,或从启动屏幕中选择“创建新项目”。
  2. 从模板列表中,选择ASPNETCore Web应用程序,确保选择C#语言模板。
  3. 在下一个屏幕上,输入项目名称、位置和解决方案名称,然后单击“创建”。
  4. 选择Web应用程序模板,然后单击Authentication下的Change以打开Authentication对话框,如图14.7所示。
  5. 选择“个人用户帐户”以创建配置有EF Core和ASPNETCore标识的应用程序。单击“确定”。
  6. 单击“创建”以创建应用程序。Visual Studio将自动运行dotnet还原以还原项目所需的所有NuGet包。
  7. 运行应用程序以查看默认应用程序,如图14.8所示。

图14.7 在VS 2019中选择新ASPNETCore应用程序模板的身份验证模式

图14.8 带有个人帐户身份验证的默认模板与无身份验证模板类似,在页面右上方添加了一个Login小部件。

这个模板看起来应该很熟悉,有一个转折点:您现在有了“注册”和“登录”按钮!可以随意使用模板——创建用户、登录和注销——来体验应用程序。一旦你高兴了,看看模板生成的代码和它省去你编写的样板。

14.3.2 在解决方案资源管理器中探索模板

由模板生成的项目(如图14.9所示)与默认的无身份验证模板非常相似。这在很大程度上是由于默认的UI库,它带来了大量的功能,而不会让您接触到具体的细节。

图14.9 默认模板的项目布局。根据您的Visual Studio版本,确切的文件可能略有不同。

最大的添加是项目根目录中的Areas文件夹,其中包含Identity子文件夹。区域有时用于组织功能部分。每个区域都可以包含自己的Pages文件夹,这类似于应用程序中的主Pages文件夹。

定义:区域用于将Razor Pages分组为单独的层次结构,以便于组织。我很少使用区域,而是更喜欢在“主页”文件夹中创建子文件夹。唯一的例外是Identity UI,它默认使用单独的Identity区域。有关领域的更多详细信息,请参阅Micro-soft的“ASPNETCore中的领域”文档:http://mng.bz/7Vw9.

Microsoft.AspNetCore.Identity.UI包在“标识”区域中创建Razor 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区域属性。

除了ASPNETCore Identity提供的新文件外,还值得打开Startup.cs并查看其中的更改。最明显的变化是ConfigureServices中的附加配置,它添加了Identity所需的所有服务。

清单14.1 向ConfigureServices添加ASPNETCore标识服务

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()扩展方法执行以下操作:

  • 添加核心ASPNETCore标识服务。
  • 将应用程序用户类型配置为IdentityUser。这是存储在数据库中的实体模型,表示应用程序中的“用户”。如果需要,可以扩展此类型,但这并不总是必要的,如第14.6节所示。
  • 添加用于注册、登录和管理用户的默认UI Razor Pages。
  • 配置用于生成2FA和电子邮件确认令牌的令牌提供程序。

在“启动”中,Configure方法还有一个非常重要的变化:

app.UseAuthentication();

这将AuthenticationMiddleware添加到管道中,以便您可以对传入请求进行身份验证,如图14.3所示。这个中间件的位置非常重要。它应该放在UseRouting()之后、UseAuthorization()和UseEndpoints()之前,如下面的列表所示。

清单14.2 将AuthenticationMiddleware添加到中间件管道

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStaticFiles(); //放置在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 ASPNETCore身份数据模型

开箱即用,在默认模板中,Identity使用EF Core存储用户帐户。它提供了一个可以从中继承的基本DbContext,称为IdentityDb-Context,它使用IdentityUser作为应用程序的用户实体。

在模板中,应用程序的DbContext称为ApplicationDbContext。如果你打开这个文件,你会发现它非常稀疏;它继承了我前面描述的IdentityDbContext基类,就是这样。这个基类给你什么?最简单的方法是用迁移更新数据库并查看。

应用迁移的过程与第12章相同。确保连接字符串指向要创建数据库的位置,在项目文件夹中打开命令提示符,然后运行此命令以使用迁移更新数据库:

dotnet ef database update

如果数据库还不存在,CLI将创建它。图14.10显示了默认模板的数据库外观。

图14.10 ASPNETCore Identity使用的数据库模式

提示:如果在运行dotnet-ef命令后看到错误,请按照12.3.1节中的说明安装.NET工具。还要确保从项目文件夹而不是解决方案文件夹运行该命令。

那是很多桌子!您不需要直接与这些表交互——Identity为您处理这些表——但对它们的用途有一个基本的了解也无妨:

  • EFMigrationsHistory——标准的EFCore迁移表,用于记录已应用的迁移。
  • 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“ASPNETCore中的身份模型自定义”文档的“更改主键类型”部分:http://mng.bz/5jdB。

用户的任何其他属性都作为声明存储在与该用户关联的AspNetUserClaims表中。这允许您添加任意的附加信息,而无需更改数据库架构以适应它。要存储用户的出生日期吗?您可以向该用户添加声明,而无需更改数据库模式。当您向每个新用户添加Name声明时,您将在第14.6节中看到这一点。

注意:添加声明通常是扩展默认Identity-User的最简单方法,但您也可以直接向IdentityUser添加其他属性。这需要更改数据库,但在许多情况下仍然有用。您可以在此处阅读如何使用此方法添加自定义数据:http://mng.bz/Xd61.

了解IdentityUser实体(存储在AspNetUsers表中)和ClaimsPrincipal(在HttpContext.User上公开)之间的区别很重要。当用户首次登录时,将从数据库中加载IdentityUser。此实体与AspNetUserClaims表中用户的其他声明相结合,以创建ClaimsPrincipal。正是此ClaimsPrincipal用于身份验证,并序列化到身份验证cookie,而不是IdentityUser。

有Identity使用的底层数据库模式的心理模型是很有用的,但在日常工作中,您不必直接与它交互,毕竟这就是Identity的用途!在下一节中,我们将查看规模的另一端——应用程序的UI,以及使用默认UI可以获得什么。

14.3.4 与ASPNETCore身份交互

您可能希望自己探索默认UI,以了解各部分是如何组合在一起的,但在本节中,我将重点介绍您从中获得的内容,以及通常需要立即额外注意的领域。

默认UI的入口点是应用程序的用户注册页面,如图14.12所示。注册页面允许用户通过使用电子邮件和密码创建新的IdentityUser来注册您的应用程序。创建帐户后,用户将重定向到一个屏幕,指示他们应该确认电子邮件。默认情况下未启用电子邮件服务,因为这取决于您配置外部电子邮件服务。您可以在Microsoft的“ASPNETCore中的帐户确认和密码恢复”文档中阅读如何启用电子邮件发送,网址为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或删除所有个人数据。假设您已经配置了一个电子邮件发送服务,这些功能中的大多数功能都可以在您不费吹灰之力的情况下工作。2

这涵盖了默认UI模板中的所有内容。它可能看起来有些微不足道,但它涵盖了几乎所有应用程序都通用的许多要求。尽管如此,您几乎总是希望定制一些东西:

  • 配置电子邮件发送服务,以启用帐户确认和口令恢复,如Microsoft的“ASPNETCore中的帐户确认和密码恢复”文档所述:http://mng.bz/vzy7.
  • 为启用2FA页面添加QR码生成器,如Microsoft的“在ASPNETCore中为TOTP验证器应用程序启用QR码生成”文档所述:http://mng.bz/4Zmw.
  • 自定义注册和登录页面以删除启用外部服务的文档链接。您将在第14.5节中了解如何执行此操作。或者,您可能希望完全禁用用户注册,如Micro-soft的“ASPNETCore项目中的脚手架标识”文档所述:http://mng.bz/QmMG.
  • 在注册页面上收集有关用户的其他信息。您将在第14.6节中了解如何执行此操作。

您可以通过多种方式扩展或更新身份系统,并提供了许多选项,因此我鼓励您浏览Microsoft的“ASPNETCore身份验证概述”,网址:http://mng.bz/XdGv查看您的选项。在下一节中,您将看到如何实现另一个常见需求:向现有应用程序添加用户。

14.4 向现有项目添加ASPNETCore标识

在本节中,我们将从第12章和第13章向配方应用程序添加用户。这是一个你想要添加用户功能的工作应用程序。在第15章中,我们将扩展这项工作,以限制对谁可以在应用程序上编辑食谱的控制。在本节结束时,您将拥有一个具有注册页面、登录屏幕和管理帐户屏幕的应用程序,就像默认模板一样。屏幕右上方还有一个持久的小部件,显示当前用户的登录状态,如图14.14所示。

图14.14 添加验证后的配方应用程序,显示了登录小部件。

如第14.3节所述,此时我不会自定义任何默认值,因此我们不会设置外部登录提供者、电子邮件确认或2FA。我只关心将ASPNETCore标识添加到已经使用EF Core的现有应用程序中。

提示:在将Identity添加到现有项目之前,确保您熟悉新项目模板是值得的。创建一个测试应用程序,并考虑设置外部登录提供商、配置电子邮件提供商和启用2FA。这需要一点时间,但在将Identity添加到现有应用程序中时,它对于破解错误非常有用。

要将Identity添加到应用程序,您需要执行以下操作:

  1. 添加ASPNETCore Identity NuGet包。
  2. 配置Startup以使用AuthenticationMiddleware并将Identity服务添加到DI容器。
  3. 使用Identity实体更新EF Core数据模型。
  4. 更新Razor页面和布局,以提供指向Identity UI的链接。

本节将依次处理这些步骤。在第14.4节末尾,您将成功将用户帐户添加到配方应用程序。

14.4.1 配置ASPNETCore标识服务和中间件

通过引用两个NuGet包,您可以将具有默认UI的ASPNETCore 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 向配方应用程序添加ASPNETCore 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节中提到的,在应用程序中使用中间件的这个顺序很重要。

清单14.5 向配方应用程序添加AuthenticationMiddleware

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 其他配置未显示
app.UseStaticFiles(); //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提供了一个名为IdentityDb-Context的DbContext,您可以从中继承。IdentityDbContext基类包含使用EF Core存储用户实体所需的DbSet<T>。

为Identity更新现有的DbContext很简单——更新应用程序的DbContext以从IdentityDbContext继承,如下所示。在本例中,我们使用基本Identity上下文的通用版本,并提供ApplicationUser类型。

清单14.6 更新AppDbContext以使用IdentityDbContext

//要从Identity上下文继承的更新,而不是直接从DbContext继承
public class AppDbContext : IdentityDbContext<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 在ASPNETCore Identity的默认UI中自定义页面

在本节中,您将学习如何使用“脚手架”替换默认Identity UI中的各个页面。您将学习如何构建页面,使其覆盖默认UI,从而允许您自定义Razor模板和PageModel页面处理程序。让Identity为您的应用程序提供整个UI在理论上是很好的,但在实践中存在一些问题,正如您在14.3.4节中已经看到的那样。默认UI提供了尽可能多的功能,但有些东西可能需要调整。例如,登录和注册页面都描述了如何为ASPNETCore应用程序配置外部登录提供程序,如图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构建注册页。

  1. 如果尚未添加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" />
  2. 确保您的项目已生成--如果未生成,则在添加新页面之前,脚手架将失败。
  3. 右键单击项目并选择“添加”>“新建脚手架项目”。
  4. 在选择对话框中,从类别中选择“标识”,然后单击“添加”。
  5. 在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.cshtmlRazor页面,以及与默认Identity UI的其余部分集成所需的支持文件。

我们对Register.cshtml页面很感兴趣,因为我们希望自定义Register页面上的UI,但如果您查看代码隐藏页面Register.cshhtml.cs,就会发现默认Identity UI隐藏了多少复杂性。这不是不可逾越的(我们将在第14.6节中定制页面处理程序),但如果您能够帮助,避免编写代码总是很好的。

现在,您的应用程序中有了Razor模板,您可以根据自己的需要对其进行自定义。缺点是现在维护的代码比使用默认UI时多。您不必编写它,但当ASPNETCore的新版本发布时,您可能仍然需要更新它。

我喜欢在重写默认的Identity UI时使用一些技巧。在许多情况下,您实际上不想更改Razor page的页面处理程序,只想更改Razur视图。您可以通过删除Register.cshtml.cs PageModel文件,并将新构建的.cshtml文件指向原始PageModel(这是默认UINuGet包的一部分)来实现这一点。

这种方法的另一个好处是,您可以删除一些自动生成的其他文件。总共,您可以进行以下更改:

  1. 更新Register.cshtml中的@model指令,以指向默认UI页面-模型:
    @model Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal
          .RegisterModel
  2. 将Areas/Identity/Pages/ViewImports.cshtml更新为以下内容:
    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
  3. 删除Areas/Identity/Pages/IdentityHostingStartup.cs。
  4. 删除Areas/Identity/Pages/_ValidationScriptsPartial.cshtml。
  5. 删除Areas/标识/页面/帐户/注册表.cshtml.cs。
  6. 删除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的“在ASPNETCore项目中向Identity添加、下载和删除自定义用户数据”文档描述了实现这一目标所需的步骤:http://mng.bz/aoe7。

假设您想向用户添加一个名为FullName的新Claim。典型的方法如下:

  1. 如您在14.5节中所做的那样,搭建Register.cshtmlRazor页面。
  2. 将“名称”字段添加到Register.cshtml.cs PageModel中的InputModel。
  3. 将“名称”输入字段添加到Register.cshtmlRazor视图模板。
  4. 通过在UserManager<ApplicationUser>上调用CreateAsync,在OnPost()页面处理程序中创建新的ApplicationUser实体。
  5. 通过调用UserManager.AddClaimAsync()向用户添加新的Claim。
  6. 继续之前的方法,发送确认电子邮件或在不需要电子邮件确认的情况下登录用户。

步骤1–3非常简单,只需要使用新字段更新现有模板即可。步骤4–6都发生在OnPost()页面处理程序中的Register.cshtml.cs中,总结如下列表。实际上,页面处理程序有更多的错误检查和样板;我们这里的重点是将额外的Claim添加到ApplicationUser的附加行。

清单14.8 向Register.cshtml.cs页面中的新用户添加自定义声明

public async Task<IActionResult> 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属性。这意味着您可以在任何有权访问claims-Principal的地方检索索赔,包括在页面处理程序和视图模板中。例如,您可以使用以下语句在Razor模板中的任何位置显示用户的FullName声明:

@User.Claims.FirstOrDefault(x=>x.Type == "FullName")?.Value

这将查找当前用户主体上类型为“FullName”的第一个声明,并打印分配的值(或者如果未找到该声明,则不打印任何内容)。Identity系统甚至包括一个方便的扩展方法,用于整理此LINQ表达式(在system.Security.Claims命名空间中找到):

@User.FindFirstValue("FullName")

有了最后一个花絮,我们已经到了本章关于ASPNETCore身份的结尾。我希望您已经意识到使用Identity可以节省您的工作量,特别是当您使用默认的Identity UI包时。

向应用程序添加用户帐户和身份验证通常是进一步定制应用程序的第一步。一旦您进行了身份验证,您就可以进行授权,这允许您根据当前用户锁定应用程序中的某些操作。在下一章中,您将了解ASPNETCore授权系统,以及如何使用它来定制应用程序;特别是配方应用程序,进展顺利!

总结

  • 身份验证是确定你是谁的过程,而授权是确定你被允许做什么的过程。在应用授权之前,你需要对用户进行身份验证。
  • ASPNETCore中的每个请求都与一个用户(也称为主体)相关联。默认情况下,在没有身份验证的情况下,这是一个匿名用户。您可以使用索赔主体根据提出请求的人而采取不同的行为。
  • 请求的当前主体在HttpContext.User上公开。您可以从Razor Pages和视图访问此值,以查找用户的属性,例如用户的ID、名称或电子邮件。
  • 每个用户都有一组声明。这些声明是关于用户的单个信息。声明可以是物理用户的属性,如名称和电子邮件,也可以与用户拥有的属性相关,如HasAdminAccess或IsVipCustomer。
  • 早期版本的ASPNET使用角色而不是声明。如果需要,您仍然可以使用角色,但应尽可能使用声明。
  • ASPNETCore中的身份验证由AuthenticationMiddleware和许多身份验证服务提供。这些服务负责在用户登录时设置当前主体,将其保存到cookie中,并在后续请求时从cookie加载主体。
  • AuthenticationMiddleware是通过调用中间件管道中的UseAuthentication()添加的。这必须放在调用UseRouting()之后、UseAuthorization()和UseEndpoints()之前。
  • ASPNETCore支持使用承载令牌来验证API调用,并包括用于配置IdentityServer的助手库。有关详细信息,请参阅Microsoft的“SPA认证和授权”文档:http://mng.bz/go0V.
  • ASPNETCore Identity处理将用户存储在数据库中所需的低级服务,确保其密码安全存储,以及用户登录和注销。您必须自己为功能提供UI,并将其连接到Identity子系统。
  • Microsoft.AspNetCore.Identity.UI包为Identity系统提供默认UI,并包括电子邮件确认、2FA和外部登录提供程序支持。您需要进行一些额外的配置以启用这些功能。
  • 具有个人帐户身份验证的Web应用程序的默认模板使用ASPNETCore标识将用户存储在具有EF Core的数据库中。它包括将UI连接到Identity系统所需的所有样板代码。
  • 您可以使用UserManager<T>类创建新的用户帐户,从数据库中加载它们,并更改它们的密码。SignInManager<T>用于通过为请求分配主体并设置身份验证cookie来登录和注销用户。默认UI为您使用这些类,以方便用户注册和登录。
  • 您可以通过从IdentityDbContext<TUser>派生来更新EF Core DbContext以支持Identity,其中TUser是从Identity-User派生的类。
  • 您可以使用UserManager<TUser>.add-ClaimAsync(TUser user,Claim Claim)方法向用户添加其他声明。当用户登录到您的应用程序时,这些声明将添加到HttpContext.User对象中。
  • 声明由类型和值组成。这两个值都是字符串。您可以为ClaimTypes类上公开的类型使用标准值,例如ClaimTypes.GivenName和ClaimTypes.FirstName,也可以使用自定义字符串,例如“FullName”。

第14章 身份验证:使用Identity将用户添加到应用程序(ASP.NET Core in Action, 2nd Edition)的更多相关文章

  1. ASP.NET没有魔法——ASP.NET 身份验证与Identity

    前面的文章中为My Blog加入了文章的管理功能(ASP.NET没有魔法——ASP.NET MVC使用Area开发一个管理模块),但是管理功能应该只能由“作者”来访问,那么要如何控制用户的访问权限?也 ...

  2. 第二章 身份验证——《跟我学Shiro》

    转发:https://www.iteye.com/blog/jinnianshilongnian-2019547 目录贴:跟我学Shiro目录贴 身份验证,即在应用中谁能证明他就是他本人.一般提供如他 ...

  3. 使用Asp.Net Core Identity给用户添加及删除角色

    基于Asp.Net Core编制一个项目,需要给用户添加及删除角色的功能,于是使用到了Identity中的UserManager. 先后解决了几个问题,终于实现了设想. 1. 环境条件 Asp.Net ...

  4. ASP.NET Core 1.1 静态文件、路由、自定义中间件、身份验证简介

    概述 之前写过一篇关于<ASP.NET Core 1.0 静态文件.路由.自定义中间件.身份验证简介>的文章,主要介绍了ASP.NET Core中StaticFile.Middleware ...

  5. NET Core 1.1 静态文件、路由、自定义中间件、身份验证简介

    NET Core 1.1 静态文件.路由.自定义中间件.身份验证简介   概述 之前写过一篇关于<ASP.NET Core 1.0 静态文件.路由.自定义中间件.身份验证简介>的文章,主要 ...

  6. asp.net core 3.x 身份验证-3cookie身份验证原理

    概述 上两篇(asp.net core 3.x 身份验证-1涉及到的概念.asp.net core 3.x 身份验证-2启动阶段的配置)介绍了身份验证相关概念以及启动阶段的配置,本篇以cookie身份 ...

  7. 从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权

    OpenIDConnect是一个身份验证服务,而Oauth2.0是一个授权框架,在前面几篇文章里通过IdentityServer4实现了基于Oauth2.0的客户端证书(Client_Credenti ...

  8. ASP.NET MVC 随想录——探索ASP.NET Identity 身份验证和基于角色的授权,中级篇

    在前一篇文章中,我介绍了ASP.NET Identity 基本API的运用并创建了若干用户账号.那么在本篇文章中,我将继续ASP.NET Identity 之旅,向您展示如何运用ASP.NET Ide ...

  9. ASP.NET Identity 身份验证和基于角色的授权

    ASP.NET Identity 身份验证和基于角色的授权 阅读目录 探索身份验证与授权 使用ASP.NET Identity 身份验证 使用角色进行授权 初始化数据,Seeding 数据库 小结 在 ...

  10. ASP.NET Web API Basic Identity 中的基本身份验证

    缺点 用户凭证在请求中发送. 凭据作为明文发送. 每个请求都会发送凭据. 无法注销,除非结束浏览器会话. 易于跨站点请求伪造(CSRF); 需要反CSRF措施. 优点 互联网标准. 受所有主要浏览器支 ...

随机推荐

  1. astrocut:切割fitsfile

    from astrocut import fits_cut from astropy.io import fits from astropy.coordinates import SkyCoord i ...

  2. [Jquery]如何绑定相同id的所有元素?

    Jquery中相同的id不能用$()获得,只能获得第一个匹配的元素. 原因:id不可重复 解决方案: 方案1: 通过 $("input[id='xxxx']"); 可以选择多个相同 ...

  3. Vulnhub 靶场 HMS?: 1

    Vulnhub 靶场 HMS?: 1 前期准备: 靶机地址:https://www.vulnhub.com/entry/hms-1,728/ 攻击机ip:192.168.147.190 靶机ip:19 ...

  4. Oracle.DataAccess使用问题汇总

    1.使用参数化传参 先看一段sql select TABLE_COLUMN_NAME from CSV_PARA_MAPPING where TABLE_NAME = ':v_tabName' and ...

  5. tfidf与bm25

    https://www.cnblogs.com/johnnyzen/p/11298273.html 前言 本文主要是对TF-IDF和BM25在公式推演.发展沿革方面的演述,全文思路.图片基本来源于此篇 ...

  6. rn项目下载@ant-design/react-native时发生冲突

    rn项目,使用npm i @ant-design/react-native下载antd. 下载依赖时报错: 如果你也遇到这个问题,直接告诉你结论,那就是最新的@ant-design/react-nat ...

  7. spring security 从零开始搭建

    1.引入 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring ...

  8. WebService 客户端上传图片,服务器端接收图片并保存到本地

    需求:如题,C#本地要调用Webservice接口,上传本地的照片到服务器中: 参考:客户端: https://blog.csdn.net/tiegenZ/article/details/799276 ...

  9. mysql ID起始值重置方法

    方法一,执行SQL:truncate table `table_name`; 这种方法好处是运行速度超快 方法二,执行如下SQL: (还是假定表名是test)delete from `table_na ...

  10. android隐藏apk方式以及apk之间的启动方式

    一.隐藏apk的方式: 在每个项目(apk)中都有一个启动应用的Activity,他的标签是这个: <intent-filter> <action android:name=&quo ...