ASP.NET Core 微服务之路 第三部分:身份






4.95/5 (21投票s)
实现 ASP.NET Core Identity,让我们的电子商务网站能够受益于身份验证、授权和用户识别
文章系列
- ASP.NET Core 微服务之路 第一部分:构建视图
- ASP.NET Core 微服务之路 第二部分:视图组件
- ASP.NET Core 微服务之路 第三部分:ASP.NET Core Identity
- 第四部分:SQLite
- 第五部分:Dapper
- 第六部分:SignalR
- 第七部分:Web API 单元测试
- 第八部分:Web MVC App 单元测试
- 第九部分:监控健康检查
- 第十部分:Redis 数据库
- 第十一部分:IdentityServer4
- 第十二部分:订单 Web API
- 第十三部分:购物车 Web API
- 第十四部分:目录 Web API
- 第十五部分:使用 Polly 实现有弹性的 HTTP 客户端
- 第十六部分:使用 Swagger 文档化 Web API
- 第十七部分:Docker 容器
- 第十八部分:Docker 配置
- 第十九部分:使用 Kibana 进行集中式日志记录
引言
在本系列文章的第二部分结束时,我们有一个电子商务应用程序,用户可以在视图中从目录中选择产品,将其放入购物车,并填写包含地址和其他个人数据的注册表单,以备将来的运输流程。当然,所有这些都是使用虚拟数据完成的。
我们的应用程序目前不需要任何形式的用户登录或密码。越来越多的电子商务网站也选择不要求此类信息,而只在结账页面请求客户的信用卡或其他支付方式。另一方面,许多电子商务网站需要登录名和密码来验证用户。
这两种模式都有优缺点。不要求身份验证的电子商务网站对客户来说更方便,因为它减少了可能损害转化率的摩擦,用用户体验的术语来说。另一方面,身份验证使您能够识别用户并可能更好地分析他们随时间推移的行为,并且允许您为用户提供某些好处,例如显示以前在网站上购买过的客户的订单历史记录。在本文中,我们将采用第二种方法。
在本系列文章的第三部分中,我们将使用登录系统并确保我们的应用程序只能由经过身份验证的用户访问。它允许您保护应用程序的敏感点免受匿名用户的攻击。通过身份验证,我们确保用户通过安全的身份识别服务进入系统。这还使应用程序能够跟踪用户访问、识别使用模式、自动填写注册表单、查看客户订单历史记录以及其他便利,从而增强用户体验。
如果您的应用程序只需要一个包含登录名和密码列的用户表以及用户配置文件,那么 ASP.NET Core Identity 是您的最佳选择。
在本章中,我们将学习如何在我们的电子商务解决方案中安装 ASP.NET Core Identity,并利用该框架提供的安全、登录/注销、身份验证和用户配置文件功能。
默认情况下,Identity 使用的数据库引擎是 SQL Server。但是,我们将使用 SQLite,它是一个比 SQL Server 更简单、更紧凑的数据库引擎。在安装 Identity 之前,我们将准备项目以使用这个新的数据库引擎。
右键单击 MVC 项目名称,选择 **添加 NuGet 包** 子菜单,然后打开包安装页面,输入包名称:`Microsoft.EntityFrameworkCore.SQLite`。
现在点击“**安装**”按钮,等待包安装完成。
好的,现在项目已准备好接收 ASP.NET Core Identity 基架。
安装 ASP.NET Core Identity
应用 ASP.NET Core Identity 基架
从头开始安装带有 Identity 的新 ASP.NET Core 与在现有项目中安装它不同。由于我们的项目没有 Identity,我们将安装一个包含所需功能的包文件和程序集。这个过程类似于在建筑工地使用预制模块建造墙壁。这个过程称为基架(scaffolding)。
如果我们需要在应用程序中手动创建登录/注销、身份验证和其他功能,那将需要大量的精力。我们正在讨论视图、业务逻辑、模型实体、数据访问、安全性等的开发,此外还有许多小时的单元测试、功能测试、集成测试等等。
幸运的是,我们的应用程序可以不费吹灰之力就能受益于身份验证和授权功能。身份验证和授权在 Web 应用程序中无处不在。因此,Microsoft 提供了一个可以在缺少此类功能的 ASP.NET Core 项目中透明安装的包。它被称为 ASP.NET Core Identity。
要将 ASP.NET Core Identity 应用到我们的解决方案中,我们右键单击项目,点击 **添加基架项**,然后选择 **添加** 选项。这将打开一个新的 **添加基架** 对话框窗口。
在这里,我们将选择 **已安装 > 身份 > 身份**。
ASP.NET Core Identity 基架将打开一个包含一系列配置参数的新对话框窗口。您可以在其中定义页面的布局、要包含的源代码、数据和用户上下文类,以及 Identity 将使用的数据库类型(SQL Server 或 SQLite)。
让我们选择这些选项
- **布局**:我们项目中已经存在的 * _Layout.cshtml* 文件。它将定义一个由 Identity 页面和我们应用程序的其余部分共享的基本标记。
- **身份页面**:`Login`、`Logout`、`Register`、`ExternalLogin`。基架过程会将这些页面复制到我们的应用程序中,您可以在其中编辑它们。请注意,您仍然可以导航到您未标记的其他 Identity 页面,但由于它们不会出现在项目中,因此您无法修改或自定义它们。
- **上下文类**:`AppIdentityContext`。
- **用户类**:`AppIdentityUser`。表示身份系统中的用户
确认这些参数后,基架将修改我们的项目。最显著的变化是项目 *Areas / Identity* 文件夹下的新文件结构。
观察 *Areas* 文件夹下的新结构
- `AppIdentityContext` 类:这是用于 ASP.NET Core Identity 的 Entity Framework 数据库上下文的类。
- `AppIdentityUser` 类:表示身份系统中的用户。
- **Pages / Account** 下的页面:这些页面包含 Identity 页面的标记代码。它们是 Razor Pages,即一种 MVC 结构类型,其中视图位于文件中,控制器和模板的操作位于单个文件中。正如我们所说,这些页面可以在我们的应用程序中修改和自定义,但其他 Identity 页面可以访问,但不能更改,因为它们的文件不存在于项目中。
- **局部视图**:`_ValidationScriptPartial`、`_ViewImports`、`_ViewStart`
- `IdentityHostingStartup` 类:ASP.NET Core WebHost 在应用程序运行后立即执行此代码。`IdentityHostingStartup` 类配置 Identity 正常工作所需的数据库和其他服务。
创建并应用 ASP.NET Core Identity 模型迁移
仅仅在我们的项目中安装 ASP.NET Core Identity 包是不够的;我们仍然需要生成数据库架构,其中包括 ASP.NET Identity Core 所需的表和初始数据。
当我们执行 ASP.NET Identity Core 的基架操作时,一个新的 Identity 数据模型会自动添加到我们的项目中,正如我们在 *IdentityHostingStartup.cs* 文件类中看到的那样
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices((context, services) => {
services.AddDbContext<AppIdentityContext>(options =>
options.UseSqlite(
context.Configuration.GetConnectionString("AppIdentityContextConnection")));
services.AddDefaultIdentity<AppIdentityUser>()
.AddEntityFrameworkStores<AppIdentityContext>();
});
}
请注意,上面显示的 Entity Framework 配置 (`AddDbContext` 方法) 如何使用 `AppIdentityContext` 类,这是我们在基架过程中选择的名称。
相同的过程还在 *appsettings.json* 配置文件中添加了一个新的 `AppIdentityContextConnection` 连接字符串。ASP.NET Core Identity 将使用此连接字符串访问 SQLite 数据库
.
.
.
"AllowedHosts": "*",
"ConnectionStrings": {
"AppIdentityContextConnection": "DataSource=MVC.db"
}
但请注意,仅凭基架过程本身并未创建 Identity SQLite 数据库。这可以通过创建新的 Entity Framework 迁移来实现。
要添加新迁移,请打开 **工具 > 包管理器控制台** 菜单,然后在控制台中输入。
PM> Add-Migration Identity
上面的命令添加了包含迁移语句的类,但它本身并未创建数据库
为了创建 SQLite 数据库,您必须通过执行 `Update-Database` 命令来应用迁移
PM> Update-Database -verbose
此命令创建在 *appsettings.json* 配置文件中包含的连接字符串中定义的 *MVC.db* 数据库文件
现在让我们双击此文件查看它。这将打开我们在本文开头安装的 DB Browser for SQLite 应用程序
就是这样!现在我们的应用程序已经具备执行身份验证和授权所需的所有组件。从现在开始,我们将开始使用这些组件将 ASP.NET Core Identity 功能集成到我们的应用程序中。
配置 ASP.NET Core Identity
将 Identity 组件添加到后端
Identity 组件已经存在于我们的项目中。但是,我们需要添加进一步的配置,将这些组件与应用程序的其余部分集成。
在软件架构中,这被称为 **中间件**。
ASP.NET Core 提供了一种标准方法,将中间件集成到应用程序的正常执行中。此机制类似于水管。每个新服务都进一步扩展了管道系统,在一端取水,然后将其传递到下一个段。
同样,ASP.NET Core 将请求沿一系列中间件传递。收到请求后,每个中间件决定是处理它还是将请求传递给链中的下一个中间件。如果用户是匿名用户并且资源需要授权,则 Identity 会将用户重定向到登录页面。
基架过程创建了 `IdentityHostingStartup` 类,该类已经配置了一些 Identity 服务。
public void Configure(IWebHostBuilder builder)
{
...
services.AddDefaultIdentity<AppIdentityUser>()
.AddEntityFrameworkStores<AppIdentityContext>();
...
}
`AddDefaultIdentity()` 方法向应用程序添加了一组常见的身份服务,包括默认 UI、令牌提供程序,并配置身份验证以使用身份 cookie。
通过调用 `UseAuthentication()` 扩展方法启用 Identity。此方法将身份验证中间件添加到请求管道
...
app.UseStaticFiles();
app.UseAuthentication();
...
`UseAuthentication()` 方法将身份验证中间件添加到指定的 `ApplicationBuilder`,从而启用身份验证功能。
然而,上面的代码只配置了后端行为。对于前端,您可以通过在布局标记中包含一个局部视图来将 ASP.NET Core Identity 视图与应用程序用户界面集成,这将允许用户登录或注册。让我们在下一节中看看它。
将 Identity 组件添加到前端
ASP.NET Core Identity 基架过程在 *Views\Shared* 文件夹中包含 `_LoginPartial` 文件。此文件包含显示经过身份验证的用户名或登录和注册超链接的局部视图。
@using Microsoft.AspNetCore.Identity
@using MVC.Areas.Identity.Data
@inject SignInManager<AppIdentityUser> SignInManager
@inject UserManager<AppIdentityUser> UserManager
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<a id="manage" class="nav-link text-dark" asp-area="Identity"
asp-page="/Account/Manage/Index"
title="Manage">Hello @UserManager.GetUserName(User)!</a>
</li>
<li class="nav-item">
<form id="logoutForm" class="form-inline" asp-area="Identity"
asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action
("Index", "Home", new { area = "" })">
<button id="logout" type="submit" class="nav-link btn btn-link text-dark">
Logout</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" id="register" asp-area="Identity"
asp-page="/Account/Register">Register</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" id="login" asp-area="Identity"
asp-page="/Account/Login">Login</a>
</li>
}
</ul>
您可以使用以下行将此组件添加到任何应用程序视图中
<partial name="_LoginPartial" />
但是,多次添加此行会导致不必要的代码重复。我们可以通过将上述行包含在应用程序的标准布局视图(_Layout.cshtml 文件)中来避免这种冗余,因为这将使该组件在我们的所有电子商务视图中可见。我们需要更具体地将其包含在应用程序的导航栏中,在包含“`navbar-collapse`”类的元素内
<div class="navbar-collapse collapse justify-content-end">
<partial name="_LoginPartial" />
<ul class="nav navbar-nav">
通过运行应用程序,我们现在可以在产品搜索页面右上角看到日志和登录链接
现在我们将点击添加任何产品以导航到购物车页面。请注意,这里也存在登录和注册链接
Razor Pages
当您安装 ASP.NET Core Identity 基架时,项目中包含的新 Identity 组件不遵循 MVC 架构。相反,Identity 组件基于 Razor Pages。
但是 MVC 和 Razor Pages 之间有什么区别呢?
从下面的截图中我们可以看到,典型的 MVC 项目如何将单个页面的组件保存在一组分散在许多文件和文件夹中的文件中
因此,在 MVC 中,没有一个“网页”文件。向新手解释这一事实有点尴尬。
如果您有一个 MVC 应用程序,然后您将视图称为“`页面`”(例如,在 *Index.cshtml* 文件中),并且您不仅将模型数据集中化,还将与该页面相关的服务器端代码(以前驻留在控制器中)集中化到专门用于该页面的类中(在 *Index.cshtml.cs* 文件中)——您现在称之为页面模型?
如果您已经从事过原生移动应用程序开发,那么您可能在 Model-View-ViewModel (MVVM) 模式中见过类似的情况。
尽管 Razor Pages 与 MVC 不同,但它仍然依赖于 ASP.NET Core MVC 框架。一旦您使用 Razor Pages 模板创建一个新项目,Visual Studio 就会通过 *Startup.cs* 文件配置应用程序以启用 ASP.NET Core MVC 框架,正如我们刚刚看到的那样。
该模板不仅为 MVC 使用配置了新的 Web 应用程序,还创建了 *Page* 文件夹和一组 Razor 页面和页面模型用于示例应用程序
Razor 页面剖析
乍一看,Razor 页面与普通的 ASP.NET MVC 视图文件非常相似。但是 Razor 页面需要一个新的指令。每个 Razor 页面都必须以 `@page` 指令开头,该指令告诉 ASP.NET Core 将其视为 Razor 页面。下图显示了有关典型 Razor 页面的更多详细信息。
- `@page` - 将文件标识为 Razor 页面。没有它,页面就无法被 ASP.NET Core 访问。
- `@model` - 类似于 MVC 应用程序,定义了绑定数据的源类,以及页面请求的 Get/Post 方法。
- `@using` - 定义命名空间的常规指令。
- `@inject` - 配置要注入页面模型类的接口实例。`@{ }` - Razor 括号中的 C# 代码片段,在这种情况下用于定义页面标题。
创建新用户
由于我们创建了一个没有用户的 SQLite 数据库,因此我们的客户需要填写 Identity 的注册页面。这些是 *Layout.cshtml* 页面中 * _LoginPartial.cshtml* 局部视图渲染的链接
现在,让我们创建一个名为 **alice@smith.com** 的新客户帐户。
当客户点击 **注册** 链接时,他们会被重定向到 */Identity/Account/Register* 页面。正如我们所见,ASP.NET Core Identity 已经为常见的用户注册问题提供了一个强大的解决方案。此外,ASP.NET Core Identity 页面与我们的电子商务前端无缝集成。
ASP.NET Core Identity 还提供了许多需要大量精力才能实现的功能,例如“**忘记密码?**”和用户锁定(当用户多次输入错误密码后被阻止登录时)。
@if (User.Identity.IsAuthenticated)
{
<ul class="nav navbar-nav">
<li>
<vc:notification-counter title="Notifications"...
<vc:basket-counter title="Basket"...
</li>
</ul>
}
授权 ASP.NET Core 资源
现在 Identity 已经工作了,我们将开始保护我们 MVC 项目的某些区域免受匿名访问,即未经身份验证的访问。这将确保只有输入了有效登录名和密码的用户才能访问受保护的系统资源。但是哪些资源应该受到保护以防止匿名访问呢?
控制器 (Controller) | 是否应该受到保护? |
CatalogController | 否 |
BasketController | 是 |
CheckoutController | 是 |
NotificationsController | 是 |
RegistrationController | 是 |
请注意,`CatalogController` 将不受保护。为什么?我们希望允许用户自由浏览网站产品,而无需强制他们使用密码登录。其他控制器都受到保护,因为它们涉及订单处理,而这只能由客户完成。但是我们将如何保护这些资源呢?我们必须使用授权属性标记这些控制器
[Authorize]
public class BasketController : BaseController
{
public IActionResult Index()
...
[Authorize]
public class BasketController : BaseController
...
[Authorize]
public class CheckoutController : BaseController
...
[Authorize]
public class NotificationsController : BaseController
...
[Authorize]
public class RegistrationController : BaseController
...
现在我们来做一个测试:当匿名用户试图访问其中一个带有 `[Authorize]` 标记的功能时会发生什么?ASP.NET Core Identity 将接收对应用程序发出的每个请求。如果用户已经通过身份验证,Identity 会将处理传递给管道的下一个组件。如果用户是匿名用户并且正在访问的资源需要授权,则 Identity 会将用户重定向到登录页面。
以匿名用户身份运行应用程序,我们转到产品搜索页面,我们可以毫无问题地访问该页面,因为此操作不受保护(即没有 `[Authorize]` 属性)
当 ASP.NET Core 尝试在 Basket 控制器中执行 Index 操作时,`[Authorize]` 属性会检查用户是否已通过身份验证。由于没有经过身份验证的用户,ASP.NET Core 通过 URL 重定向请求
https://:44340/Identity/Account/Login?ReturnUrl=%2FBasket
请注意,此 URL 有两部分
- 用户需要验证身份的 URL:https://:5001/Identity/Account/Login
- 原始 URL,用户身份验证后将返回该 URL
我们可以通过打开开发者工具(Chrome 键 **F12**)并打开 **Headers** 选项卡来更仔细地查看此重定向过程,我们看到对 Action / Cart 操作的调用通过 HTTP 代码 302 进行重定向,这是重定向代码
至此,我们结束了关于 ASP.NET Core Identity 配置的主题。从现在开始,我们将开始获取最终可在我们应用程序中使用的用户信息。
管理用户数据
准备用户注册表单
用户提交表单后,`RegistrationViewModel` 必须准备好传输所有数据。
因此,我们将自定义用户信息添加到注册视图模型类中。
public class RegistrationViewModel
{
public string UserId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string Address { get; set; }
public string AdditionalAddress { get; set; }
public string District { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
@using MVC.Models.ViewModels
@model RegistrationViewModel
@{
ViewData["Title"] = "Registration";
}
<h3>Registration</h3>
<form method="post" asp-controller="checkout" asp-action="index">
<input type="hidden" asp-for="@Model.UserId" />
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<label class="control-label" for="name">Customer Name</label>
<input type="text" class="form-control"
id="name" asp-for="@Model.Name" />
<span asp-validation-for="@Model.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label" for="email">Email</label>
<input type="email" class="form-control" id="email"
asp-for="@Model.Email">
<span asp-validation-for="@Model.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label" for="phone">Phone</label>
<input type="text" class="form-control"
id="phone" asp-for="@Model.Phone" />
<span asp-validation-for="@Model.Phone" class="text-danger"></span>
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label class="control-label" for="address">Address</label>
<input type="text" class="form-control" id="address"
asp-for="@Model.Address" />
<span asp-validation-for="@Model.Address" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label" for="additionaladdress">
Additional Address</label>
<input type="text" class="form-control"
id="additionaladdress" asp-for="@Model.AdditionalAddress" />
<span asp-validation-for="@Model.AdditionalAddress"
class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label" for="district">District</label>
<input type="text" class="form-control" id="district"
asp-for="@Model.District" />
<span asp-validation-for="@Model.District"
class="text-danger"></span>
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label class="control-label" for="city">City</label>
<input type="text" class="form-control" id="city"
asp-for="@Model.City" />
<span asp-validation-for="@Model.City" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label" for="state">State</label>
<input type="text" class="form-control"
id="state" asp-for="@Model.State" />
<span asp-validation-for="@Model.State" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label" for="zipcode">Zip Code</label>
<input type="text" class="form-control"
id="zipcode" asp-for="@Model.ZipCode" />
<span asp-validation-for="@Model.ZipCode" class="text-danger"></span>
</div>
<div class="form-group">
<a class="btn btn-success" href="/">
Keep buying
</a>
</div>
<div class="form-group">
<button type="submit"
class="btn btn-success button-notification">
Check out
</button>
</div>
</div>
</div>
</div>
</div>
</form>
此外,新的用户信息必须由 `AppIdentityUser` 类持有。这不仅仅是一个普通的类。它定义了用于创建或修改 `User` 实体(表 `AspNetUsers`)数据库表的模型。
public class AppIdentityUser : IdentityUser
{
public string Name { get; set; }
public string Phone { get; set; }
public string Address { get; set; }
public string AdditionalAddress { get; set; }
public string District { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
但请注意,我们必须再次创建一个新的 Entity Framework Core 迁移,以便将模型中的这些更改应用到数据库表中。
要添加新迁移,请打开 **工具 > 包管理器控制台** 菜单,然后在控制台中输入。
PM> Add-Migration UserProfileData
上述命令创建了“`UserProfileData`”迁移,其中包含添加新用户表字段的迁移语句
为了创建 **SQLite** 数据库,您必须通过执行 `Update-Database` 命令来应用迁移
PM> Update-Database -verbose
此命令将当前模型与数据库快照进行比较,并将差异应用回数据库。在我们的例子中,检测到的差异是缺失的用户属性。
从 Identity 数据库检索用户数据
填写表单字段总是一项繁琐的任务。但在某些情况下,它无法避免或推迟。考虑电子商务网站上的客户信息:如果没有所有正确的数据,就无法计算运费,也无法处理发货。但是有一些方法可以减轻客户对此过程的不满。例如,您可以保存客户数据以备将来订单使用。但是我们如何使用 ASP.NET Core Identity 保存用户数据呢?
幸运的是,Identity 带有一个名为 `UserManager
`UserManager
在 `RegistrationController` 的 `Index` 方法中, 我们可以看到 `GetUserAsync()` 方法是如何用于异步地从 Identity 存储(即 SQLite 数据库)中检索当前用户的。
[Authorize]
public class RegistrationController : BaseController
{
private readonly UserManager userManager;
public RegistrationController(UserManager<AppIdentityUser> userManager)
{
this.userManager = userManager;
}
public async Task<IActionResult> Index()
{
var user = await userManager.GetUserAsync(this.User);
var viewModel = new RegistrationViewModel(
user.Id, user.Name, user.Email, user.Phone,
user.Address, user.AdditionalAddress, user.District,
user.City, user.State, user.ZipCode
);
return View(viewModel);
}
}
在此之后,我们使用从 `AspNetUsers` 表检索到的用户数据填充 `RegistrationViewModel` 类。反过来,视图模型被传递到视图中,以便为第二次访问的客户自动填充注册表单,正如我们在下面的 `<input>` 字段中看到的那样。
<input class="form-control" asp-for="@Model.Phone" />
...
<input class="form-control" asp-for="@Model.Address" />
...
<input class="form-control" asp-for="@Model.AdditionalAddress" />
...
<input class="form-control" asp-for="@Model.District" />
...
<input class="form-control" asp-for="@Model.City" />
...
<input class="form-control" asp-for="@Model.State" />
...
<input class="form-control" asp-for="@Model.ZipCode" />
...
将用户数据持久化到 Identity 数据库
以下代码展示了如何
- 检查模型是否有效,即 `RegistrationViewModel` 类验证规则是否满足
- 使用 `GetUserAsync()` 方法异步检索用户
- 通过应用表单数据修改来自数据库的 `user` 对象
- 使用 `UpdateAsync()` 方法更新 **SQLite** 数据库
- 如果模型无效,则重定向回注册视图
[Authorize]
public class CheckoutController : BaseController
{
private readonly UserManager<AppIdentityUser> userManager;
public CheckoutController(UserManager<AppIdentityUser> userManager)
{
this.userManager = userManager;
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(RegistrationViewModel registration)
{
if (ModelState.IsValid)
{
var user = await userManager.GetUserAsync(this.User);
user.Email = registration.Email;
user.Phone = registration.Phone;
user.Name = registration.Name;
user.Address = registration.Address;
user.AdditionalAddress = registration.AdditionalAddress;
user.District = registration.District;
user.City = registration.City;
user.State = registration.State;
user.ZipCode = registration.ZipCode;
await userManager.UpdateAsync(user);
return View(registration);
}
return RedirectToAction("Index", "Registration");
}
}
订单下达后,`Checkout` 视图会显示订单确认以及网站的“谢谢”消息。我们再次使用 `RegistrationViewModel` 作为视图绑定的来源。
@model RegistrationViewModel
...
<p>Thank you very much, <b>@Model.Name</b>!</p>
<p>Your order has been placed.</p>
<p>Soon you will receive an e-mail at <b>@Model.Email</b> including all order details.</p>
请注意,`CheckoutController` 类在更新数据库用户信息之前检查了模型是否有效
public async Task<IActionResult> Index(RegistrationViewModel registration)
{
if (ModelState.IsValid)
{
服务器端需要进行此验证,以避免使用不一致的信息更新数据库表。但这只是我们应用程序的最后一道防线。您绝不应该仅仅依赖服务器端检查进行数据验证。还需要做些什么?您还应该强制执行早期验证,在用户尝试提交订单时执行客户端检查。幸运的是,ASPNET Core 项目提供了一个用于客户端验证的局部视图
<environment include="Development">
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</environment>
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.17.0/
jquery.validate.min.js"
asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator"
crossorigin="anonymous">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/
jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"
asp-fallback-src="~/lib/jquery-validation-unobtrusive/
jquery.validate.unobtrusive.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator &&
window.jQuery.validator.unobtrusive"
crossorigin="anonymous">
</script>
</environment>
验证局部视图必须使用 `
@section Scripts
{
<partial name="~/Views/Shared/_ValidationScriptsPartial.cshtml"/>
}
使用 Microsoft 帐户、Google、Facebook 等登录
为什么要允许外部帐户登录?
在网络应用程序中注册新用户和密码通常是一个繁琐的过程,在电子商务应用程序中,这不仅会令客户失望,还会损害业务,因为任何额外的步骤都可能阻止潜在买家。他们可能更喜欢另一个更友好、更少官僚主义的电子商务网站。简而言之:强制用户注册可能会损害销售额。
外部登录过程允许我们将身份验证过程与外部服务(例如 Microsoft、Google 和 Facebook)上的现有帐户集成,而无需创建新密码,可以为我们的客户提供更方便的注册过程。
然而,此外部登录过程必须作为替代方案实现,而不是唯一的注册方法。
幸运的是,ASP.NET Core Identity 提供了一种机制,允许用户通过 Microsoft、Google、Facebook、Twitter 等外部提供商进行登录。
使用 Microsoft 帐户配置外部登录
请记住,外部登录服务不了解您的应用程序,反之亦然。双方都需要一个配置,定义哪些应用程序/服务将参与身份验证过程。
让我们创建必要的配置,以便我们的用户可以使用他们的 Microsoft 帐户(@hotmail.com、@outlook.com 等)作为我们应用程序的登录方式。
首先,Microsoft 身份验证服务需要知道我们的应用程序。我们需要输入名为 Microsoft 应用程序注册门户 https://apps.dev.microsoft.com 的服务地址,并为我们的应用程序创建设置。
首先,您(开发人员)需要使用您的 Microsoft 帐户登录门户
在这个开发者门户中,您可以查看您已注册的应用程序。如果您还没有 Microsoft 帐户,可以创建一个。登录后,您将被重定向到 **我的应用** 页面
在这里,您将注册一个新应用程序。选择右上角的 **添加应用程序**,然后输入应用程序名称。
让我们给它一个有意义的名称,例如 `GroceryStore`。
单击 **创建应用程序** 以继续到注册页面。提供一个名称并记下应用程序 ID 的值,您可以稍后将其用作 `ClientId`。
在这里,您将点击平台部分的 **添加平台**,并选择 **Web 平台**。
在 Web 平台下,输入您的开发 URL,并在重定向字段 URL 中添加 /signin-microsoft(例如:https://:44320/signin-microsoft)。我们稍后将配置的 Microsoft 身份验证方案将自动处理 /signin-microsoft 路由中的请求以实现 OAuth 流
请注意,在此页面上,我们将点击 **添加 URL** 以确保 URL 已添加。
如有必要,填写任何其他应用程序设置,然后单击页面底部的保存以保存对应用程序配置的更改。
现在,查看注册页面上显示的应用程序 ID。单击“**应用程序机密**”部分中的“生成新密码”。这将显示一个框,您可以在其中复制应用程序密码
我们将把这个密码存放在哪里?在一个真实的商业应用程序中,我们必须使用某种形式的安全存储,例如环境变量或 Secret Manager 工具(https://docs.microsoft.com/aspnet/core/security/app-secrets?view=aspnetcore-2.2&tabs=windows)。
然而,为了方便起见,我们只需使用 *appsettings.json* 配置文件来存储在 Microsoft 开发者门户上注册的应用程序密码。在这里,我们创建了两个新的配置键
- `ExternalLogin:Microsoft:ClientId`:在 Microsoft 创建的 Web 应用程序 ID
- `ExternalLogin:Microsoft:ClientSecret`:在 Microsoft 创建的 Web 应用程序密码
"ExternalLogin": {
"Microsoft": {
"ClientId": "nononononononononononnnononoon",
"ClientSecret": "nononononononononononnnononoon"
}
}
现在让我们将以下摘录添加到 `Startup` 类的 `CofigureServices` 方法中,以通过 Microsoft 帐户启用身份验证
services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
options.ClientId = Configuration["ExternalLogin:Microsoft:ClientId"];
options.ClientSecret = Configuration["ExternalLogin:Microsoft:ClientSecret"];
});
再次运行我们的电子商务应用程序,我们可以在登录页面的右侧面板中看到一个新按钮,它允许您使用 Microsoft 外部提供商登录。
登录 Microsoft 页面后,我们的用户将被重定向到 ASP.NET Core Identity 提供的“帐户关联页面”。在这里,我们将点击“**注册**”以完成 Microsoft 帐户与我们的电子商务帐户之间的关联
正如我们下面所见,客户现在已使用 Microsoft 电子邮件注册,并且我们的应用程序不再需要其他登录信息!
请注意,直接由 Identity 创建的帐户可以与在 Google、Microsoft、Facebook 等外部机制中创建的用户帐户共存,正如我们在 SQLite 数据库 *MVC.db* 的用户表 (`AspNetUsers`) 中看到的那样:
最终,我们可以调查哪些用户帐户是在我们系统之外创建的。只需打开 *MVC.db* 数据库中的 `AspNetUserLogins` 表
使用 Google 帐户配置外部登录
现在让我们创建必要的配置,以便我们的用户可以使用 Google 帐户(@gmail.com)作为我们应用程序的替代登录方式。
Google 身份验证服务也需要了解我们的应用程序。我们首先需要前往 **Google 网站登录**。在该页面上,您必须单击以配置您的项目
现在您必须为 Google 登录配置一个项目。在此处输入您的项目名称。让我们给它一个有意义的名称,例如 `GroceryStore`
现在是时候配置您的 OAuth 客户端了。输入您的应用程序的友好名称,以便在用户使用其 Google 帐户登录时向用户显示
接下来,您告诉 Google 您的应用程序从何处调用。在这种情况下,我们选择 **Web 服务器**,因为是我们的 ASP.NET Core Web 应用程序将调用 Google 外部登录提供商进行用户身份验证。
Google 还需要我们应用的重定向 URI。一旦我们的用户通过 Google 身份验证,**https://:5001/signin-google** URI 将被调用,并带有用于访问的授权码。
点击 **创建** 按钮后,您可以查看新创建的客户端 ID 和客户端密钥值,这些值需要在您的应用程序中使用。
现在,让我们打开 *appsettings.json* 文件并插入以下键和值
- `ExternalLogin:Google:ClientId`:在 Google 创建的 Web 应用程序 ID
- `ExternalLogin:Google:ClientSecret`:在 Google 创建的 Web 应用程序密码
"ExternalLogin": {
"Microsoft": {
"ClientId": "nononononononononononnnononoon",
"ClientSecret": "nononononononononononnnononoon"
},
"Google": {
"ClientId": "nononononononononononnnononoon",
"ClientSecret": "nononononononononononnnononoon"
}
}
现在让我们将以下摘录添加到 `Startup` 类的 `CofigureServices` 方法中,以通过 Google 帐户启用身份验证
services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
options.ClientId = Configuration["ExternalLogin:Microsoft:ClientId"];
options.ClientSecret = Configuration["ExternalLogin:Microsoft:ClientSecret"];
})
.AddGoogle(options =>
{
options.ClientId = Configuration["ExternalLogin:Google:ClientId"];
options.ClientSecret = Configuration["ExternalLogin:Google:ClientSecret"];
});
再次运行我们的电子商务应用程序,我们可以在登录页面的右侧面板中看到一个新按钮,它允许您使用 Google 外部提供商登录。
登录 Microsoft 页面后,我们的用户将被重定向到 ASP.NET Core Identity 提供的“帐户关联页面”。在这里,我们将点击“**注册**”以完成 Google 帐户与我们的电子商务帐户之间的关联
正如我们下面所见,客户现在已使用 Google 电子邮件注册,并且我们的应用程序不再需要其他登录信息!
配置 Google 帐户外部登录
最后,让我们将 `AddGoogle()` 扩展方法的调用添加到 `Startup` 类的 `CofigureServices` 方法中,以启用通过 Google 帐户进行身份验证
services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
options.ClientId = Configuration["ExternalLogin:Microsoft:ClientId"];
options.ClientSecret = Configuration["ExternalLogin:Microsoft:ClientSecret"];
})
.AddGoogle(options =>
{
options.ClientId = Configuration["ExternalLogin:Google:ClientId"];
options.ClientSecret = Configuration["ExternalLogin:Google:ClientSecret"];
});
结论
我们到达了“ASP.NET Core 微服务之路 第三部分”的末尾。在本文中,我们了解了我们在上一课程(ASP.NET Core 微服务之路 第二部分)结束时拥有的应用程序的用户身份验证需求。然后我们决定使用 ASP.NET Core Identity 作为我们电子商务 Web 应用程序的身份验证解决方案。
我们学习了如何使用 ASP.NET Core Identity 身份创建过程来授权 MVC 应用程序,使其能够利用用户身份验证、资源保护和敏感页面(如购物车、注册和结账)的好处。
我们了解了用户布局流程的工作原理,并学习了如何配置 MVC Web 应用程序以满足该流程的要求。一旦了解了登录和注销过程,我们就开始修改我们的 MVC 应用程序,以使用 ID 和用户名,以及每个登录用户的其他注册信息,在我们的 MVC 应用程序上下文中,这些信息代表正在进行购买的客户。
最后,我们学习了如何执行外部登录过程,这使我们能够将身份验证过程与 Microsoft、Google 和 Facebook 等外部服务中的现有帐户集成,从而为我们的客户提供更方便的注册过程。
历史
- 2019年6月29日:初始版本