原文:
zh.annas-archive.org/md5/b71782931b1ec965a99239b2cd142e50译者:飞龙
第十一章:第十一章:创建 ASP.NET Core 6 网络应用程序
到目前为止,我们已经构建了应用程序的所有核心组件,如数据访问层和服务层,所有这些组件主要是服务器端组件,也称为后端组件。
在本章中,我们将构建我们的电子商务应用程序的呈现层/用户界面(UI),也称为客户端组件。UI 是应用程序的界面;一个好的呈现层不仅有助于保持用户对应用程序的参与度,而且鼓励用户返回应用程序。这在企业应用程序中尤为重要,一个好的呈现层有助于用户轻松地浏览应用程序,并帮助他们轻松地执行依赖于应用程序的日常活动。
我们将专注于理解 ASP.NET Core MVC,并使用 ASP.NET Core MVC 开发一个网络应用程序。主要涵盖以下主题:
-
前端网页开发简介
-
将 API 与服务层集成
-
创建控制器和操作
-
使用 ASP.NET Core MVC 创建用户界面
-
理解 Blazor
技术要求
对于本章,你需要具备基本的 C#、.NET Core、HTML 和 CSS 知识。本章的代码示例可以在这里找到:github.com/PacktPublishing/Enterprise-Application-Development-with-C-10-and-.NET-6-Second-Edition/tree/main/Chapter11/RazorSample.
你可以在这里找到更多代码示例:github.com/PacktPublishing/Enterprise-Application-Development-with-C-10-and-.NET-6-Second-Edition/tree/main/Enterprise%20Application.
前端网页开发简介
呈现层主要是指浏览器可以渲染并显示给用户的代码。每当一个页面在浏览器中加载时,它会创建一个包含各种元素(如文本框和标签)的层次结构,这些元素都存在于页面上。这个层次结构被称为文档对象模型(DOM)。
一个好的前端主要在于能够根据需要操作 DOM,并且有许多技术/库支持使用网络事实上的语言 JavaScript 动态地操作 DOM 和加载数据。无论是简化 JavaScript 使用的 jQuery,还是支持完整客户端渲染的完整客户端框架,如 Angular、React 或 Vue,或者是 ASP.NET Core 框架,如 ASP.NET Core MVC、Razor Pages 或 Blazor,它们都归结为处理网络的三个主要构建块:HTML、CSS 和 JavaScript。让我们来看看这三个构建块:
-
超文本标记语言(HTML):正如全称所述,HTML 是一种浏览器可以理解和显示内容的标记语言。它主要包含一系列称为 HTML 元素 的标签,允许开发者定义页面的结构。例如,如果你想创建一个需要允许用户输入他们的名字和姓氏的表单,可以使用输入 HTML 元素来定义它。
-
层叠样式表(CSS):表示层全部关于以吸引用户的方式展示数据,并确保无论用户尝试在哪种设备/分辨率上加载应用程序,应用程序都是可用的。这正是 CSS 在定义浏览器上内容显示方面发挥关键作用的地方。它控制各种事情,如页面的样式、应用程序的主题和调色板,更重要的是,它使它们具有响应性,这样用户在使用应用程序时,无论是在移动设备还是桌面设备上,都能获得相同的体验。
现代网络开发的优点是我们不需要从头开始编写一切,许多库都可以直接选择并用于应用程序中。我们将在 使用 ASP.NET Core MVC 创建 UI 部分中使用这样一个库,其解释如下。
- JavaScript: JavaScript 是一种脚本语言,有助于执行各种高级动态操作,例如,验证在表单中输入的文本或类似启用/禁用 HTML 元素条件性或从 API 获取数据等操作。JavaScript 为网页提供了更多功能,并添加了许多开发者可以用来在客户端执行高级操作的编程特性。就像 HTML 和 CSS 一样,所有浏览器都能理解 JavaScript,它构成了表示层的一个重要部分。所有这些组件都可以相互链接,如下面的图所示:
图 11.1 – HTML、CSS 和 JavaScript
注意
HTML、CSS 和 JavaScript 互相依存,在开发客户端/前端应用程序中发挥着重要作用,需要专门的书籍来全面解释。一些相关链接可以在 进一步阅读 部分找到。
现在我们已经了解了 HTML、CSS 和 JavaScript 的重要性,我们需要知道如何使用它们来构建网页应用程序的表示层,以便它能够支持具有不同分辨率的多个浏览器,并且能够管理状态(HTTP 是无状态的)。
一种技术可能是创建所有 HTML 页面并在 Web 服务器上托管它们;然而,虽然这对于静态站点工作得很好,并且也涉及从头开始构建一切,但如果我们希望内容更加动态并且想要丰富的 UI,我们需要使用能够动态生成 HTML 页面并提供与后端无缝交互支持的技术。让我们在下一节中看看可以用来生成动态 HTML 的各种技术。
Razor 语法
在我们开始理解 ASP.NET Core 提供的各种可能框架之前,让我们首先了解什么是 Razor 语法。它是一种标记语法,用于将服务器端组件嵌入到 HTML 中。我们可以使用 Razor 语法绑定任何动态数据以进行显示,或者将其从视图/页面发送回服务器以进行进一步处理。Razor 语法主要编写在 Razor 文件/Razor 视图页面上,这些文件不过是 C# 用来生成动态 HTML 的文件。它们带有 .cshtml 扩展名并支持 Razor 语法。Razor 语法由一个名为 视图引擎 的引擎处理,默认视图引擎被称为 Razor 引擎。
要嵌入 Razor 语法,我们通常使用 @,这告诉 Razor 引擎解析并生成 HTML。@ 后可以跟任何 C# 内置方法来生成 HTML。例如,<b>@DateTime.Now</b> 可以用于在 Razor 视图/页面上显示当前日期和时间。除此之外,就像 C# 一样,Razor 语法也支持代码块、控制结构和变量等。以下图展示了通过 Razor 引擎的一些示例 Razor 语法:
图 11.2 – Razor 语法
Razor 语法还支持定义 HTML 控件;例如,要定义一个文本框,我们可以使用以下 Razor 语法:
<input asp-for=' FirstName ' />
上述代码被称为 input 标记助手,Razor 语法负责处理称为 指令 标记助手的绑定数据到 HTML 控件并生成丰富、动态的 HTML。让我们简要讨论一下:
- 指令:在底层,每个 Razor 视图/页面都由 Razor 引擎解析,并使用一个 C# 类来生成动态 HTML,然后将其发送回浏览器。指令可以用来控制这个类的行为,进而控制生成的动态 HTML。
例如,@using 指令可以用于在 Razor 视图/页面上包含任何命名空间,或者 @code 指令可以用于包含任何 C# 成员。
最常用的指令之一是 @model,它允许你将模型绑定到视图,这有助于验证视图的类型以及帮助 Intellisense。将视图绑定到特定类/模型的过程称为 强类型 视图。在我们的电子商务应用程序中,我们将对所有视图进行强类型处理,这将在 使用 ASP.NET Core MVC 创建 UI 部分中看到。
- 标签助手:如果你在 ASP.NET Core 之前使用过 ASP.NET MVC,你可能会遇到 HTML 助手,这些是帮助绑定数据和生成 HTML 控件的类。
然而,在 ASP.NET Core 中,我们有标签助手,它们帮助我们将数据绑定到 HTML 控件。与 HTML 助手相比,标签助手的优点是它们使用与 HTML 相同的语法,并为标准 HTML 控件分配了额外的属性,这些属性可以由动态数据生成。例如,要生成一个 HTML 文本框控件,通常我们会编写以下代码:
<input type='text' id='Name' name='Name' value=' Mastering enterprise application development Book'>
使用标签助手,这将被重写为以下代码,其中 @Name 是视图强类型关联的模型属性:
<input type='text' asp-for='@Name'>
因此,正如你所看到的,这完全是关于编写 HTML,但利用 Razor 标记来生成动态 HTML。ASP.NET Core 内置了许多标签助手,更多关于它们的信息可以在以下链接中找到:docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/?view=aspnetcore-6.0。
不需要了解/记住每个标签助手,我们将在开发应用程序 UI 时使用此参考文档。
注意
由于 Razor 语法是标记,因此不需要了解所有语法。以下链接可以用作 Razor 语法的参考:docs.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-6.0。
因此,让我们来看看 ASP.NET Core 以及其他常见框架中用于开发表示层的各种选项。
探索 Razor Pages
Razor Pages 是使用 ASP.NET Core 实现网络应用程序的默认方式。Razor Pages 依赖于拥有一个可以直接处理请求的 Razor 文件的概念,以及与该 Razor 文件相关联的可选 C# 文件,用于任何额外的处理。可以使用以下命令创建一个典型的 Razor 应用程序:
dotnet new webapp --framework net6.0 -o RazorSample
正如你所看到的,项目包含 Razor 页面及其对应的 C# 文件。在打开任何 Razor 视图时,我们会看到一个名为 @page 的指令,它有助于浏览页面。例如,/index 将被路由到 index.cshtml。重要的是,所有 Razor 页面都必须在页面顶部包含 @page 指令,并且放置在 Pages 文件夹中,因为 ASP.NET Core 运行时会在此文件夹中查找所有 Razor 页面。
可以通过使用另一个名为 @model 的指令将 Razor 页面进一步关联到一个 C# 类,也称为 PageModel 类。以下是 index.cshtml 页面的代码:
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<form method="post">
<div class="text-center">
<select asp-for="WeekDaySelected"
asp-items="Model.WeekDay"></select>
<button type=Submit name="Submit">Submit</button>
<br>
<h3>@ViewData["Message"]</h3>
</div>
</form>
PageModel 类不过是一个可以具有特定 GET 和 POST 调用方法的 C# 类,以便从 API 等动态获取 Razor 页面上的数据。这个类需要继承自 Microsoft.AspNetCore.Mvc.RazorPages.PageModel,并且是一个标准的 C# 类。index.cshtml 的 PageModel,它是 index.cshtml.cs 的一部分,如下代码所示:
public class IndexModel : PageModel
{
public IndexModel(ILogger<IndexModel> logger)
{
}
public List<SelectListItem> WeekDay { get; set; }
public void OnGet()
{
this.WeekDay = new List<SelectListItem>();
this.WeekDay.Add(new SelectListItem
{
Value = 'Monday',
Text = 'Monday'
});
this.WeekDay.Add(new SelectListItem
{
Value = 'Tuesday',
Text = 'Tuesday'
});
}
}
在这里,你可以看到我们正在通过 OnGet 方法填充在 Razor 页面上使用的数据,这同样也被称为 PageModel 处理器,可以用于初始化 Razor 页面。像 OnGet 一样,我们可以添加一个 OnPost 处理器,它可以用于将数据从 Razor 页面提交回 PageModel 并进一步处理。
如果 OnPost 方法满足以下两个条件,它将自动绑定 PageModel 类中的所有属性:
-
属性被注解为
BindProperty属性。 -
Razor 页面有一个与属性同名的 HTML 控件。
例如,如果我们想要绑定前面代码中 select 控件的值,我们需要首先在 PageModel 类中添加一个属性,如下代码所示:
[BindProperty]
public string WeekDaySelected { get; set; }
然后,使用 select 控件的属性名,如这里所示,Razor Pages 将自动将选中的值绑定到这个属性:
<select asp-for='WeekDaySelected' asp-items='Model.WeekDay'></select>
我们可以使用异步命名约定为 OnGet 和 OnPost 方法命名,如果我们在使用异步编程,它们可以命名为 OnGetAsync/OnPostAsync。
Razor Pages 还支持根据动词调用方法。方法名的模式应遵循 OnPost[handler]/OnPost[handler]Async 规范,其中 [handler] 是设置在任何标签助手 asp-page-handler 属性上的值。
例如,以下代码将调用对应 PageModel 类中的 OnPostDelete/OnPostDeleteAsync 方法:
<input type='submit' asp-page-handler='Delete' value='Delete' />s
对于服务配置部分,Razor Pages 可以通过在 Program 类中使用 AddRazorPages 方法来配置,通过将服务添加到 ASP.NET Core 的 MapRazorPages 中间件,并在 Program 类中注入,如下代码所示。这样做是为了使所有 Razor 页面都可以使用页面名称进行请求:
app.MapRazorPages();
这完成了简单的 Razor 页面应用程序设置;我们在 第九章,在 .NET 6 中使用数据,中看到了另一个示例,它使用了 Razor Pages 从数据库中检索数据,使用了 Entity Framework Core (EF Core)。
Razor Pages 是在 ASP.NET Core 中开发 Web 应用程序的最简单形式;然而,对于一种更结构化的开发 Web 应用程序的形式,可以处理复杂功能,我们可以选择 ASP.NET Core MVC。让我们在下一节中探索使用 ASP.NET Core MVC 开发 Web 应用程序。
探索 ASP.NET Core MVC 网站
如其名所示,ASP.NET Core MVC 基于第十章创建 ASP.NET Core 6 Web API中讨论的 MVC 模式,是 ASP.NET Core 中用于构建 Web 应用程序的框架。我们在第十章创建 ASP.NET Core 6 Web API中看到,ASP.NET Core Web API 也使用 MVC 模式;然而,ASP.NET Core MVC 还支持用于显示数据的视图。底层设计模式是相同的,其中我们有一个模型来存储数据,一个控制器来传输数据,以及视图来渲染和显示数据。
ASP.NET Core MVC 支持在第十章创建 ASP.NET Core 6 Web API中讨论的所有功能,例如路由、依赖注入、模型绑定和模型验证,并使用与 Web API 相同的启动技术,即使用 Program 类。就像 Web API 一样,.NET 6 应用程序服务和中间件在 Program 类中进行配置。
与 MVC 的一个主要区别是还需要加载视图,因此,我们不是在 Program 类中使用 AddControllers,而是需要使用 AddControllersWithViews。以下图示了一个示例:
Figure 11.3 – MVC 请求生命周期
AddControllersWithViews 主要负责加载视图和处理控制器发送的数据,但最重要的是,它负责配置用于在视图中处理 Razor 语法并生成动态 HTML 的 Razor 引擎服务。
在 ASP.NET Core MVC 中,控制器操作需要根据 URL 中传递的动作名称进行路由,因此在路由部分,我们不是调用 MapController,而是配置 MapControllerRoute 并向其传递一个模式。因此,UseEndpoints 中间件中的默认路由配置看起来如下代码片段所示:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: 'default',
pattern: '{controller=Products}/{action=Index}/{id?}');
});
在这个模式中,我们正在告诉中间件,URL 的第一部分应该是 controller 名称,后面跟着动作名称和可选的 id 参数。如果 URL 中没有传递任何内容,则默认路由是 ProductsController 的 Index 动作方法。因此,这主要是我们在第十章创建 ASP.NET Core 6 Web API中讨论的基于约定的路由。
就像 Razor Pages 一样,ASP.NET Core MVC 应用程序中的视图支持 Razor 语法,并允许强类型视图;也就是说,一个视图可以绑定到一个模型进行类型检查,并且模型属性可以与具有编译时智能感知支持的 HTML 控件相关联。
由于 ASP.NET Core MVC 为应用程序提供了更多的结构,因此我们将使用 ASP.NET Core MVC 进行我们的表示层开发,这将在后续章节中详细讨论。
注意
总是有一个关于选择前端开发技术的常见问题。以下链接提供了一些关于这个主题的建议:docs.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/choose-between-traditional-web-and-single-page-apps。在选择前端技术之前,应该评估所有优点和缺点,因为没有一种适合所有情况的要求。
在这个基础上,让我们继续到下一节,我们将开始将到目前为止开发的后端 API 与我们的表示层集成。
将 API 与服务层集成
在本节中,我们将开发 Packt.Ecommerce.Web ASP.NET Core MVC 应用程序,该应用程序是通过添加 ASP.NET Core web application(Model-View-Controller) 模板创建的。由于我们已经为表示层开发了各种所需的 API,因此我们首先将构建一个用于与这些 API 通信的包装类。
这是一个用于与各种 API 通信的单个包装类,因此让我们为这个类创建一个合约。为了简单起见,我们将要求限制到我们电子商务应用中最重要的工作流程,如下所示:
-
登录页面检索系统中的所有产品,并允许用户搜索/过滤产品。
-
查看产品的详细信息,将它们添加到购物车中,并能够添加更多产品到购物车。
-
完成订单并查看发票。
为了采用更结构化的方法,我们将将各种类和接口分离到不同的文件夹中。让我们在以下步骤中看看如何操作:
-
首先,让我们向
Packt.Ecommerce.Web项目添加一个名为Contracts的文件夹,并添加一个名为IECommerceService的接口。这个接口将包含以下方法:// Method to retrieve all products and filter. Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null); // Method to get details of specific product. Task<ProductDetailsViewModel> GetProductByIdAsync(string productId, string productName); // Method to create and order, this method is primarily used to create a cart which is nothing but an order with order status as 'Cart'. Task<OrderDetailsViewModel> CreateOrUpdateOrder(OrderDetailsViewModel order); // Method to retrieve order by ID, also used to retrieve cart/order before checkout. Task<OrderDetailsViewModel> GetOrderByIdAsync(string orderId); Task<InvoiceDetailsViewModel> GetInvoiceByIdAsync(string invoiceId); // Method to submit cart and create invoice. Task<InvoiceDetailsViewModel> SubmitOrder(OrderDetailsViewModel order); // Method to retrieve invoice details by Id. Task<InvoiceDetailsViewModel> GetInvoiceByIdAsync(string invoiceId); -
现在,让我们添加一个名为
Services的文件夹,并添加一个名为EcommerceService的类。这个类将继承IECommerceService并实现所有方法。 -
由于我们需要调用各种 API,我们需要使用
Packt.Ecommerce.Common.Options.ApplicationSettings通过options模式来使用它。
Program 类将为我们的 MVC 应用程序配置以下服务:
-
AddControllersWithViews: 这将为 ASP.NET Core MVC 注入使用控制器和视图所需的服务。 -
ApplicationSettings: 这将使用以下代码使用IOptions模式配置ApplicationSettings类:builder.Services.Configure<ApplicationSettings>(this.Configuration.GetSection('ApplicationSettings')); -
AddHttpClient: 这将注入System.Net.Http.IHttpClientFactory和相关类,这将允许我们创建一个HttpClient对象。此外,我们将配置重试策略和断路器策略,如在第 第十章 中所述,创建 ASP.NET Core 6 Web API。 -
使用 .NET Core DI 容器将
EcommerceService映射到IECommerceService。
-
使用以下代码配置应用洞察:
string appinsightsInstrumentationKey = this.Configuration.GetValue<string>('AppSettings:InstrumentationKey'); if (!string.IsNullOrWhiteSpace(appinsightsInstrumentationKey)) { builder.Services.AddLogging(logging => { logging.AddApplicationInsights( appinsightsInstrumentationKey); }); builder.Services .AddApplicationInsightsTelemetry (appinsightsInstrumentationKey); }
接下来,关于中间件,我们将使用Program类注入以下中间件,除了默认的路由中间件之外:
-
UseStatusCodePagesWithReExecute: 这个中间件用于将请求重定向到除500错误代码外的自定义页面。在下一节中,我们将在ProductController中添加一个方法,该方法将被执行并基于错误代码加载相关视图。这个中间件接受一个字符串作为输入参数,这实际上是在出现错误时应执行的路由,并且为了传递错误代码,它允许使用占位符{0}。因此,中间件配置看起来如下所示:app.UseStatusCodePagesWithReExecute('/Products/Error/{0}'); -
错误处理:至于表示层,与 API 不同,我们需要将用户重定向到自定义页面,在运行时失败的情况下,该页面包含相关信息,例如用户友好的错误消息和可以用于稍后检索实际失败的相关的日志 ID。然而,在开发环境中,我们可以显示完整的错误以及堆栈。因此,我们将配置两个中间件,如下面的代码所示:
{ if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler('/Products/Error/500'); }
在这里,我们可以看到,对于开发环境,我们使用的是UseDeveloperExceptionPage中间件,它将加载完整的异常堆栈跟踪,而对于非开发环境,我们使用的是UseExceptionHandler中间件,它接受需要执行的错误操作方法的路径。此外,在这里,我们不需要我们的自定义错误处理中间件,因为 ASP.NET Core 中间件负责将详细的错误记录到日志提供程序,在我们的例子中是应用洞察。
UseStaticFiles: 为了允许各种静态文件,如 CSS、JavaScript、图像以及任何其他静态文件,我们不需要通过整个请求管道,这就是这个中间件发挥作用的地方,它允许服务静态文件并支持为静态文件短路其余的管道。
回到EcommerceService类,让我们首先定义这个类的局部变量和构造函数,它将使用以下代码注入HTTPClient工厂和ApplicationSettings:
private readonly HttpClient httpClient;
private readonly ApplicationSettings applicationSettings;
public ECommerceService(IHttpClientFactory httpClientFactory, IOptions<ApplicationSettings> applicationSettings)
{
NotNullValidator.ThrowIfNull(applicationSettings, nameof(applicationSettings));
IHttpClientFactory httpclientFactory = httpClientFactory;
this.httpClient = httpclientFactory.CreateClient();
this.applicationSettings = applicationSettings.Value;
}
现在,为了按照我们的IECommerceService接口实现方法,我们将使用以下步骤来处理 Get API:
图 11.4 – API 的 Get 调用
根据前面图中的步骤,GetProductsAsync的实现,主要用于检索用于着陆页的产品并在进行产品搜索时应用任何过滤器,将如下所示:
public async Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null)
{
IEnumerable<ProductListViewModel> products = new List<ProductListViewModel>();
using var productRequest = new
HttpRequestMessage(HttpMethod.Get,
$'{this.applicationSettings.ProductsApiEndpoint}
?filterCriteria={filterCriteria}');
var productResponse = await this.httpClient.SendAsync(
productRequest).ConfigureAwait(false);
if (!productResponse.IsSuccessStatusCode)
{ await this.ThrowServiceToServiceErrors(
productResponse).ConfigureAwait(false);
}
if (productResponse.StatusCode !=
System.Net.HttpStatusCode.NoContent)
{
products = await productResponse.Content
.ReadFromJsonAsync<Ienumerable
<ProductListViewModel>>().ConfigureAwait(false);
}
return products;
}
对于POST/PUT API,我们将有类似的步骤,但略有修改,如下面的图所示:
图 11.5 – API 的 Post 调用
基于此,CreateOrUpdateOrder的策略实现,主要用于创建购物车,其代码如下所示:
public async Task<OrderDetailsViewModel> CreateOrUpdateOrder(OrderDetailsViewModel order)
{
NotNullValidator.ThrowIfNull(order, nameof(order));
using var orderRequest = new
StringContent(JsonSerializer.Serialize(order),
Encoding.UTF8, ContentType);
var orderResponse = await this.httpClient.PostAsync(new
Uri($'{this.applicationSettings.OrdersApiEndpoint}'),
orderRequest).ConfigureAwait(false);
if (!orderResponse.IsSuccessStatusCode)
{
await this.ThrowServiceToServiceErrors(
orderResponse).ConfigureAwait(false);
}
var createdOrder = await orderResponse.Content
.ReadFromJsonAsync<OrderDetailsViewModel>()
.ConfigureAwait(false);
return createdOrder;
}
同样,我们将使用前面提到的一种策略和相关的 API 端点来实现GetProductByIdAsync、GetOrderByIdAsync、GetInvoiceByIdAsync和SubmitOrder。
现在,让我们创建将与EcommerceService通信并加载相关视图的控制器和操作方法。
创建控制器和操作
我们已经看到,路由负责将请求 URI 映射到控制器中的操作方法,因此让我们进一步了解操作方法是如何加载相应视图的。正如您所注意到的,ASP.NET Core MVC 项目中的所有视图都是Views文件夹的一部分,当操作方法执行完成后,它只需简单地查找Views/<ControllerName>/<Action>.cshtml。
例如,将操作方法映射到Products/Index路由将加载Views/Products/Index.cshtml视图。这是通过在每个操作方法结束时调用Microsoft.AspNetCore.Mvc.Controller.View方法来处理的。
除了可以覆盖此行为并按需路由到不同视图的额外重载和辅助方法外,我们将在讨论这些辅助方法之前,就像 Web API 一样,MVC 控制器中的每个操作方法也可以返回IActionResult,这意味着我们可以利用辅助方法来重定向到视图。在 ASP.NET Core MVC 中,每个控制器都继承自基类Microsoft.AspNetCore.Mvc.Controller,该类包含一些辅助方法,并通过以下Microsoft.AspNetCore.Mvc.Controller类中的辅助方法来处理通过操作方法加载视图:
-
View:此方法具有多种重载,主要根据控制器名称从Views文件夹下的文件夹加载视图。例如,在ProductsController中调用此方法可以加载Views/Products文件夹中的任何.cshtml文件。此外,它还可以接受视图名称,如果需要,可以加载,并支持通过强类型化视图来传递可以由视图检索的对象。 -
RedirectToAction:尽管View方法处理了大多数场景,但仍然会有需要在同一控制器或另一个控制器中调用另一个操作方法的场景,这就是RedirectToAction发挥作用的地方。此方法具有多种重载,允许我们指定操作方法、控制器方法以及操作方法可以接收作为路由值的对象。
简而言之,为了加载视图并从控制器传递数据,我们将通过View方法传递相应的模型,并在需要时使用RedirectToAction来调用另一个操作方法。
现在,问题是如何处理数据检索(GET调用)与数据提交(POST调用),在 ASP.NET Core MVC 中,所有动作方法都支持使用HttpGet和HttpPost属性来标记 HTTP 动词。以下是一些可以用来标记方法的规则:
-
如果我们想要检索数据,则动作方法应使用
HttpGet进行标记。 -
如果我们想要向动作方法提交数据,则应使用
HttpPost进行标记,并将相关对象作为动作方法的输入参数。
通常,需要从控制器向视图发送数据的函数应使用[HttpGet]进行标记,而需要从视图接收数据以进一步提交到数据库的函数应使用[HttpPost]进行标记。
现在,让我们继续添加所需的控制器并实现它们。当我们添加Packt.Ecommerce.Web时,它将创建一个包含默认创建的HomeController的Controllers文件夹,我们需要将其删除。然后,我们需要通过右键单击Controllers文件夹,然后选择ProductsController、CartController和OrdersController来添加三个控制器。
所有这些控制器都将具有以下两个共同属性,一个用于日志记录,另一个用于调用EcommerceService的方法。它们将通过构造函数注入进一步初始化,如下所示:
private readonly ILogger<ProductsController> logger;
private readonly IECommerceService eCommerceService;
让我们现在讨论每个控制器中定义的内容:
-
ProductsController:此控制器将包含一个public async Task<IActionResult> Index(string searchString, string category)动作方法,用于加载列出所有产品的默认视图,并进一步支持过滤。将会有另一个方法,public async Task<IActionResult> Details(string productId, string productName),它接受产品的 ID 和名称,并加载指定产品的详细信息。由于这两个方法都用于检索,因此它们将使用[HttpGet]进行标记。此外,此控制器将具有之前讨论过的Error方法。由于它可以从UseStatusCodePagesWithReExecute中间件接收错误代码作为输入参数,因此我们将有简单的逻辑来相应地加载视图:[Route('/Products/Error/{code:int}')] public IActionResult Error(int code) { if (code == 404) { return this.View('~/Views/Shared/NotFound.cshtml'); } else { return this.View('~/Views/Shared/Error.cshtml', new ErrorViewModel { CorrelationId = Activity.Current?.RootId ?? this.HttpContext.TraceIdentifier }); } } -
CartController:此控制器包含一个public async Task<IActionResult> Index(ProductListViewModel product)动作方法,用于将产品添加到购物车中。我们将创建一个订单,并将订单状态设置为'Cart',因为这需要接收数据并将其进一步传递到 API,该 API 将被标记为[HttpPost]。为了简化,这里将其留为匿名,但可以限制对已登录用户的访问。一旦创建订单,此方法将使用RedirectToAction辅助方法,并将重定向到控制器内的public async Task<IActionResult> Index(string orderId)动作方法,该方法进一步加载包含所有产品的购物车和结账表单。此方法也可以直接导航到购物车。 -
OrdersController: 这是流程中的最后一个控制器,其中包含一个public async Task<IActionResult> Create(OrderDetailsViewModel order)动作方法,用于在填写支付详情后提交订单。此方法将订单状态更新为Submitted,然后为订单创建发票,最后重定向到另一个动作方法public async Task<IActionResult> Index(string invoiceId),该方法加载订单的最终发票并完成交易。
下面的图表示了控制器之间完成购物流程的方法之间的流程:
图 11.6 – 控制器动作方法之间的流程
基于这些知识,让我们在下一节中设计视图。
使用 ASP.NET Core MVC 创建 UI
到目前为止,我们已经定义了一个服务来与后端 API 通信,并进一步定义了将使用模型将数据传递给视图的控制器。现在,让我们构建各种视图,这些视图将渲染数据并向用户展示。
首先,让我们看看参与渲染视图的各种组件:
Views文件夹:所有视图都是这个文件夹的一部分,每个控制器特定的视图都由一个子文件夹分隔,最后,每个动作方法都由一个.cshtml文件表示。
要添加一个视图,我们可以在动作方法上右键单击并选择添加视图,这将自动创建一个(如果尚未存在)以控制器命名的文件夹,并添加视图。此外,在执行此操作时,我们可以指定视图将绑定到的模型。
-
Layout页面:在 Web 应用程序中,我们通常有一个跨应用共用的部分,例如带有菜单或左侧导航的页眉。为了使我们的页面具有模块化结构并避免重复,ASP.Net Core MVC 提供了一个名为_Layout.cshtml的布局页面,它是Views/Shared文件夹的一部分。这个页面可以用作我们 MVC 项目中所有视图的父页面。一个典型的布局页面如下所示:<!DOCTYPE html> <html lang='en'> <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width, initial-scale=1'> <meta http-equiv='x-ua-compatible' content='ie=edge'> <title>Ecommerce Packt</title> </head> <body class='hold-transition sidebar-mini layout-top-nav'> <!-- Navbar --> <!-- Main content --> @RenderBody() </body> </html>
在这里,你可以看到它允许我们定义应用程序的骨架布局,然后最后,有一个名为@RenderBody()的 Razor 方法,它实际上加载了子视图。要指定任何视图中的布局页面,我们可以使用以下语法,它将_Layout.cshtml添加为视图的父页面:
@{
Layout = '~/Views/Shared/__Layout.cshtml';
}
然而,没有必要在所有视图中重复此代码,这正是_ViewStart.cshtml发挥作用的地方。让我们看看它是如何帮助我们在视图之间重用一些代码的:
-
_ViewStart.cshtml:这是一个通用的视图,位于Views文件夹下,并由 Razor 引擎执行需要在视图代码之前执行的任何代码。因此,通常情况下,它用于定义布局页面,所以前面的代码可以添加到这个文件中,以便它在整个应用程序中应用。 -
_ViewImports.cshtml:这是另一个可以用于在应用程序中导入任何公共指令或命名空间的页面。就像_ViewStart一样,它也位于根文件夹下;然而,_ViewStart和_ViewImport可以在一个(或多个)文件夹中,并且它们是按照从根视图文件夹中的开始,到任何子文件夹中的低级别文件夹的顺序执行的。要启用使用 Application Insights 的客户端遥测,我们按照以下代码注入JavaScriptSnippet。我们在 第五章 中学习了如何将依赖服务注入到视图中,在 .NET 6 中的依赖注入。在以下代码中,JavaScriptSnippet被注入到视图中:@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet -
wwwroot:这是应用程序的根文件夹,所有静态资源,如 JavaScript、CSS 以及任何图像文件,都放置在这里。这里还可以存放我们希望在应用程序中使用的任何 HTML 插件。由于我们已经在应用程序中配置了UseStaticFiles中间件,因此文件夹中的内容可以直接提供服务,无需任何处理。ASP.NET Core MVC 的默认模板根据类型对文件夹进行了隔离;例如,所有 JavaScript 文件都放置在js文件夹中,CSS 文件放置在css文件夹中,等等。我们将坚持使用这种文件夹结构来构建我们的应用程序。注意
通过右键单击操作方法并使用内置模板自动生成视图的过程称为 scaffolding,如果您对 Razor 语法不熟悉,可以使用它。然而,使用 scaffolding 或手动将其放置在相应的文件夹中并强类型化,会产生相同的行为。
设置 AdminLTE、布局页面和视图
要在整个应用程序中保持相同的视觉效果和感觉,一个重要的事情是选择正确的样式框架。这样做不仅提供了统一的布局,还简化了响应式设计,有助于在不同分辨率下正确渲染页面。我们为 Packt.Ecommerce.Web 使用的 ASP.NET Core MVC 项目模板默认包含 Bootstrap 作为其样式框架。我们将进一步扩展到名为 AdminLTE 的主题,它包含一些有趣的布局和仪表板,可以插入到我们的表示层中。
让我们执行以下步骤以将 AdminLTE 集成到我们的应用程序中:
-
从这里下载
AdminLTE的最新版本:github.com/ColorlibHQ/AdminLTE/releases。 -
提取之前下载的 ZIP 文件,并导航到
AdminLTE-3.0.5\dist\css。复制adminlte.min.css,并将其粘贴到Packt.Ecommerce.Web的wwwroot/css文件夹内。 -
导航到
AdminLTE-3.0.5\dist\js。复制adminlte.min.js,并将其粘贴到Packt.Ecommerce.Web的wwwroot/js文件夹内。 -
导航到
AdminLTE-3.0.5\dist\img。复制所需的图片,并将它们粘贴到Packt.Ecommerce.Web的wwwroot/img文件夹内。 -
复制
AdminLTE-3.0.5\plugins文件夹,并将其粘贴到Packt.Ecommerce.Web的wwwroot文件夹内。
更多关于AdminLTE的信息可以在adminlte.io/docs/2.4/installation找到。
现在,导航到Views/_Layout.cshtml页面,删除所有现有代码,并用Packt.Ecommerce.Web\Views\Shared_Layout.cshtml中的代码替换它。从高层次上讲,布局分为以下几部分:
-
在左侧页眉中的导航到主页
-
在页眉中添加一个搜索框,并在中间添加一个带有搜索类别的下拉菜单
-
在右侧页眉中的购物车
-
一个用于显示导航的面包屑路径
-
一个用于使用
@RenderBody()渲染子视图的部分
完成集成AdminLTE模板所需的一些其他关键事项如下:
-
在
<head>标签中添加以下样式定义:<link rel='stylesheet' href='~/plugins/fontawesome-free/css/all.min.css'> <link rel='stylesheet' href='~/css/adminlte.min.css'> -
在
<body>标签的末尾之前添加以下 JavaScript 文件:<!-- REQUIRED SCRIPTS (Order shouldn't matter)--> <!-- jQuery --> <script src='~/plugins/jquery/jquery.min.js'></script> <!-- Bootstrap 4 --> <script src='~/plugins/bootstrap/js/bootstrap.bundle.min.js'></script> <!-- AdminLTE App --> <script src='~/js/adminlte.min.js'></script>
通过这种方式,我们已经将AdminLTE主题集成到我们的应用程序中。要使用 Application Insights 启用客户端遥测所需的 JavaScript,请在_Layout.cshtml的head标签内添加以下代码:
@Html.Raw(JavaScriptSnippet.FullScript)
之前的代码注入了发送遥测数据所需的 JavaScript,以及仪表化密钥。与服务器端或客户端不同,仪表化密钥是公开的。任何人都可以从浏览器开发者工具中看到仪表化密钥。但是,这就是客户端遥测的设置方式。此时,这种风险是恶意用户或攻击者可以通过仪表化密钥的只写访问权限推送不希望的数据。如果您希望使客户端遥测更安全,您可以从您的服务中公开一个安全的 REST API,并从那里记录遥测事件。您将在第十四章中了解更多关于 Application Insights 功能的信息,健康和诊断。
现在,应用程序布局已准备就绪。让我们现在继续定义应用程序中的各种视图。
创建 Products/Index 视图
此视图将用于列出我们电子商务应用程序上所有可用的产品,并且使用IEnumerable<Packt.Ecommerce.DTO.Models.ProductListViewModel>模型进行强类型化。它使用ProductsController的Index操作方法检索数据。
在此视图中,我们将使用简单的 Razor @foreach (var item in Model) 循环,并为每个产品显示产品的图片、名称和价格。以下截图展示了此视图的一个示例:
图 11.7 – 产品视图
在这里,您可以看到有一个搜索栏和一个来自布局页面的分类下拉菜单。点击产品图片将导航到 Products/Details 视图。为了支持此导航,我们将使用 AnchorTagHelper 并将产品 ID 和名称传递给 ProductsController 的 Details 动作方法,以便进一步在 Products/Details 视图中加载产品的详细信息。
创建 Products/Details 视图
此视图将根据从 Products/Index 视图传递的产品 ID 和名称加载产品的详细信息。我们将使用以下示例页面:adminlte.io/themes/dev/AdminLTE/pages/examples/e_commerce.html。
此页面将使用 Packt.Ecommerce.DTO.Models.ProductDetailsViewModel 进行强类型定义,并显示产品的所有详细信息。以下截图展示了此页面的一个示例:
图 11.8 – 产品详情视图
如您所见,有一个 'Cart',这将调用 CartController 的 Index 动作方法以创建购物车。
要将数据传递回动作方法,我们将借助 FormTagHelper,它允许我们将页面包裹在一个 HTML 表单中,并使用以下代码指定页面可以提交到的动作和控制器:
<form asp-action='Index' asp-controller='Cart'>
使用此代码,一旦提交为 Submit 类型,页面就会提交到 CartController 的 Index 动作方法,以便进一步将其保存到数据库。然而,我们仍然需要将产品详情传递回 Index 动作方法,为此,我们将借助 InputTagHelper 并为所有需要传递回动作方法的值创建隐藏字段。
这里最重要的是隐藏变量的名称应与模型中属性的名称匹配,因此我们将在表单内添加以下代码,以将产品值传递回控制器:
<input asp-for='Id' type='hidden'>
<input asp-for='Name' type='hidden'>
<input asp-for='Price' type='hidden'>
<input asp-for='ImageUrls[0]' type='hidden'>
ASP.NET Core MVC 的模型绑定系统读取这些值,并为 CartController 的 Index 方法创建所需的产品对象,然后进一步调用后端系统以创建订单。
创建购物车/索引视图
此视图将加载购物车详情,并将有一个填写所有详细信息并完成订单的结账表单。在此,我们可以导航回主页以添加更多产品或完成订单。
此视图使用 Packt.Ecommerce.DTO.Models.OrderDetailsViewModel 进行强类型化,并使用 OrdersController 的 Index 动作方法加载数据。在这里,我们使用了来自 getbootstrap.com/docs/4.5/examples/checkout/ 的 Bootstrap 结账表单示例。
此表单利用模型验证和 HTML 属性对必填字段进行验证,我们借助 ASP.NET Core MVC 标签辅助器和一些 HTML 辅助器来渲染表单。以下代码展示了具有模型验证的示例属性:
public class AddressViewModel
{
[Required(ErrorMessage = 'Address is required')]
public string Address1 { get; set; }
[Required(ErrorMessage = 'City is required')]
public string City { get; set; }
[Required(ErrorMessage = 'Country is required')]
public string Country { get; set; }
}
由于此表单也需要提交,整个表单被包裹在 FormTagHelper 中,如下面的代码所示:
<form asp-action='Create' asp-controller='Orders'>
要在 UI 上显示这些验证,请将以下脚本添加到 _layout.cshtml 中,在添加了所有其他脚本之后:
<script src='~/lib/jquery-validation/dist/jquery.validate.min.js'></script>
<script src='~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js'></script>
要显示错误消息,我们可以使用验证消息标签辅助器,如下面的代码片段所示。在服务器端,这可以通过 ModelState.IsValid 进一步评估:
<input asp-for='ShippingAddress.Address1' class='form-control' placeholder='1234 Main St'/>
<span asp-validation-for='ShippingAddress.Address1' class='text-danger'></span>
此页面的示例截图如下:
图 11.9 – 购物车和结账页面
我们将使用 InputTagHelper 作为隐藏字段和文本框,以将任何附加信息传递回动作方法。文本框的好处是,如果文本框的 id 属性与属性名称匹配,那么数据将自动传递回动作方法,ASP.NET Core MVC 的模型绑定系统将负责将其映射到所需的对象,在这种情况下,是 Packt.Ecommerce.DTO.Models.OrderDetailsViewModel 类型的对象,最终提交订单、生成发票并重定向到 Orders/Index 动作方法。
注意
在前面的屏幕截图中,尽管在生产应用中我们有一个包含支付信息的结账表单,但我们将与第三方支付网关集成,通常,整个表单都位于应用程序的支付网关侧,出于各种安全原因。razorpay.com/docs/payment-gateway/server-integration/dot-net/ 和 stripe.com/docs/api 是这类第三方提供商的几个例子,它们有助于支付网关集成。
创建 Orders/Index 视图
最后,我们将有一个查看订单发票的视图,这是一个简单的只读视图,显示从 OrdersController 的 Index 动作方法发送的发票信息。以下截图显示了此页面的一个示例:
图 11.10 – 最终发票
这完成了各种视图的集成,如您所见,我们已经将视图限制在电子商务应用程序中最重要的流程中。然而,您可以使用相同的原理进一步添加更多功能。
理解 Blazor
Blazor 是从 .NET Core 3.1 开始可用的新框架,用于开发应用程序的前端层。它是 MVC 和 Razor Pages 的替代方案之一,并且应用程序模型非常接近 SPA;然而,我们可以用 C# 和 Razor 语法编写逻辑,而不是 JavaScript。
所有的 Blazor 编写代码都放在一个名为 .Razor 的地方,用于表示应用程序;无论是整个网页还是一个小对话框弹出窗口,所有内容都在 Blazor 应用程序中作为一个组件创建。一个典型的 Razor 组件如下代码片段所示:
@page '/counter'
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class='btn btn-primary' @onclick='IncrementCount'>Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{ currentCount++; }
}
在此代码中,我们创建了一个页面,该页面在按钮点击时增加计数器,点击事件的逻辑由 C# 代码处理,该代码更新 HTML 中的值。此页面可以通过 /counter 相对 URL 访问。
Blazor 与其他 MVC/Razor Pages 之间的主要区别在于,与请求-响应模型不同,其中每个请求都发送到服务器,并将 HTML 发送回浏览器,Blazor 将所有组件打包(就像 SPA 一样)并在客户端加载。当应用程序首次请求时,任何后续对服务器的调用都是为了检索/提交任何 API 数据或更新 DOM。Blazor 支持以下两种托管模型:
- Blazor WebAssembly(WASM):WASM 是可以在现代浏览器上运行的低级指令,这有助于在浏览器上运行用高级语言(如 C#)编写的代码,而无需任何附加插件。Blazor WASM 托管模型利用 WASM 提供的开放网络标准,在浏览器上的沙盒环境中运行任何 Blazor WASM 应用程序的 C# 代码。在较高层次上,所有 Blazor 组件都编译成 .NET 程序集,并下载到浏览器,WASM 加载 .NET Core 运行时和所有程序集。它进一步使用 JavaScript 互操作来刷新 DOM;唯一的服务器调用将是任何后端 API。架构如下所示:
图 11.11 – Blazor WASM 托管
blazor.server.js并使用 SignalR 接收所有 DOM 更新,这意味着每个用户交互都会有一个服务器调用(尽管非常轻量)。架构如下所示:
图 11.12 – Blazor Server 托管
.NET 6 为两种托管模型提供完整的工具支持,包括它们自己的项目模板,并且各有优缺点,这些将在以下内容中进一步解释:docs.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-6.0。
现在让我们按照以下步骤使用 Blazor Server 应用程序创建一个前端应用程序,这允许我们为我们的电子商务应用程序添加/修改产品详情:
-
将名为
Packt.Ecommerce.Blazorweb的新 Blazor Server 应用程序添加到企业解决方案中,并将Products.razor、AddProduct.Razor和EditProduct.razorRazor 组件添加到Pages文件夹。 -
此项目包含
Program类,它与其他任何 ASP.NET Core 应用程序完全相同,只是增加了一些额外的 Blazor 服务。_Host.cshtml是应用程序的根,应用程序的初始调用由该页面接收,并以 HTML 进行响应。此页面进一步引用blazor.server.js脚本文件以建立 SignalR 连接。另一个重要组件是App.Razor组件,它负责基于 URL 的路由。在 Blazor 中,任何需要映射到特定 URL 的组件都将在其开头包含@page指令,该指令指定应用程序的相对 URL。App.Razor拦截 URL 并将它们路由到指定的组件。所有 Razor 组件都是Pages文件夹的一部分,而Data文件夹包含用于FetchData.razor组件的示例模型和服务。 -
让我们在
NavMenu.razor中添加以下代码以将Products导航添加到左侧菜单。在这个阶段,如果你运行应用程序,你应该能够看到带有Products导航的左侧菜单;然而,它不会导航到任何页面:<li class='nav-item px-3'> <NavLink class='nav-link' href='products'> <span class='oi oi-list-rich' aria- hidden='true'></span> Products </NavLink> </li> -
由于我们将从 API 获取数据,我们需要将
HTTPClient注入到我们的Program类中,就像在 ASP.NET Core 应用程序中所做的那样。因此,将以下代码添加到Program类中:builder.Services.AddHttpClient("Products", client => { client.BaseAddress = new Uri(builder.Configuration["ApplicationSettings:ProductsApiEndpoint"]); }); -
将以下
ApplicationSettings:ProductsApiEndpoint设置添加到appsettings.json:'ApplicationSettings': { 'ProductsApiEndpoint': 'https://localhost:7256/api/products/' }, -
由于我们将要绑定
products数据,所以让我们将Packt.Ecommerce.DTO.Models添加为Packt.Ecommerce.Blazorweb项目的引用。在Pages文件夹中,将以下代码添加到Products.razor页面的@code块内,我们在其中使用IHttpClientFactory创建一个HttpClient对象,该对象将在下一步注入,并在OnInitializedAsync方法中检索products数据:private List<ProductListViewModel> products; protected override async Task OnInitializedAsync() { var client = Factory.CreateClient('Products'); var result = await client.GetAsync('').ConfigureAwait(false); result.EnsureSuccessStatusCode(); products = new List<ProductListViewModel>(); products = await result.Content.ReadFromJsonAsync <List<ProductListViewModel>>() .ConfigureAwait(false); } -
接下来,在
Products.Razor页面的开头添加以下代码(在@code块外部)。在这里,我们通过@page指令设置此组件的相对路由为/products。接下来,我们注入IHttpClientFactory和其他所需的命名空间,然后添加渲染产品列表的 HTML 部分。如您所见,它是 HTML 和 Razor 语法的混合体:@page '/products' @inject IHttpClientFactory Factory @using System.Net.Http.Json;@using Packt.Ecommerce.DTO.Models; <h1>Products</h1> <div> <a class='btn btn-info' href='addproduct'><i class='oi oi-plus'></i> Add Product</a> </div> @if (products == null) { <p><em>Loading...</em></p> } else { <table class='table'><thead><tr> <th>Id</th><th>Name</th> <th>Price</th><th>Quantity</th> <th>ImageUrls</th><th></th> </tr></thead><tbody> @foreach (var product in products) {<tr> <td>@product.Id</td> <td>@product.Name</td> <td>@product.Price</td> <td>@product.Quantity</td> <td><img src='@product.ImageUrls[0]' class='product-image w-10 col-3' alt='Product' /></td> <td><a class='btn btn-info' href='editproduct/ @product.Id/@product.Name'> <i class='oi oi-pencil'> </i></a></td></tr> } </tbody></table> }
在这一点上,如果您运行应用程序,应该会看到以下截图所示的输出:
图 11.13 – 产品列表 Blazor UI
- 接下来,让我们创建
Add/Edit页面,我们将使用 Blazor 表单。表单中可用的一些重要工具/组件如下。
Blazor 表单是使用 Blazor 中称为EditForm的内置模板创建的,并且可以直接使用模型属性绑定到任何 C#对象。一个典型的EditForm如下面的代码片段所示。在这里,我们定义在表单提交时调用OnSubmit方法。让我们将其添加到AddProduct.razor中:
<EditForm Model='@product' OnSubmit='@OnSubmit'>
</EditForm>
在这里,product是我们想要使用的模型对象,在我们的案例中是Packt.Ecommerce.DTO.Models.ProductDetailsViewModel。要将数据绑定到任何控件,我们可以使用 HTML 和 Razor 语法的组合,如下面的代码所示。在这里,我们将产品对象的Name属性绑定到文本框,同样,将Category属性绑定到下拉列表。一旦你在文本框中输入任何值或在下拉列表中选择一个值,它就会自动出现在这些属性中,以便将其传递回任何后端 API 或数据库。让我们以类似的方式将所有必需的属性添加到 HTML 元素中:
<InputText id='category' @bind-Value='product.Name'></InputText>
<InputSelect @bind-Value='product.Category'>
<option selected disabled value='-1'> Choose Category</option>
<option value='Clothing'>Clothing</option>
<option value='Books'>Books</option>
</InputSelect>
Blazor 表单支持使用数据注解进行数据验证,因此任何我们想要绑定到 UI 的模型都可以有数据注解,Blazor 会自动将这些验证应用到绑定的控件上。要应用验证,我们添加DataAnnotationsValidator组件,并可以使用ValidationSummary组件来显示所有验证失败的摘要。更多详细信息请参阅docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-6.0。我们还可以在控件级别使用ValidationMessage组件,如下面的代码片段所示:
<DataAnnotationsValidator />
<ValidationSummary />
<InputNumber id='quantity' @bind-Value='product.Quantity'></InputNumber>
<ValidationMessage For='@(() => product.Quantity)' />
- 在
code组件中,添加一个ProductDetailsViewModel对象,并将其命名为 product,即如EditForm的Model属性中定义的那样,并进一步实现OnSubmit方法。
AddProduct.Razor和EditProduct.Razor的完整代码可以在 GitHub 仓库中找到,一旦运行应用程序,我们就可以看到以下页面:
图 11.14 – 添加产品 Blazor UI
这是一个使用 Blazor 构建前端的基本示例,它执行列表、创建和更新操作。然而,Blazor 中还有许多概念可以进一步探索,请参阅docs.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-6.0。
摘要
在本章中,我们了解了表示层和 UI 设计的各个方面。此外,我们还学习了使用 ASP.NET Core MVC 和 Razor Pages 开发表示层的各种技能,最后,我们使用 ASP.NET Core MVC 和 Blazor 为我们的企业应用程序实现了表示层。
使用这些技能,你应该能够使用 ASP.NET Core MVC、Razor Pages 和 Blazor 构建表示层,并将其与后端 API 集成。
在下一章中,我们将看到如何将身份验证集成到我们系统的各个应用层中。
问题
- 以下哪个是推荐用于定义在整个 Web 应用程序中需要出现的左侧导航的页面?
a. _ViewStart.cshtml
b. _ViewImports.cshtml
c. _Layout.cshtml
d. Error.cshtml
答案:c
- 以下哪个页面可以用来配置整个应用程序的
Layout页面?
a. _ViewStart.cshtml
b. _ViewImports.cshtml
c. _Layout.cshtml
d. Error.cshtml
答案:a
- 以下哪个特殊字符用于在
.cshtml页面中编写 Razor 语法?
a. @
b. #
c. <% %>
d. 以上皆非
答案:a
-
在以下 Razor 页面应用程序的标签助手代码中,哪个方法会在按钮点击时被调用?
<input type='submit' asp-page-handler='Delete' value='Delete' />
a. OnGet()
b. onDelete()
c. OnPostDelete()
d. OnDeleteAsync()
答案:c
进一步阅读
-
www.packtpub.com/web-development/html5-and-css3-building-responsive-websites -
www.packtpub.com/product/bootstrap-for-asp-net-mvc-second-edition/9781785889479 -
docs.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-6.0 -
docs.microsoft.com/en-us/aspnet/core/mvc/views/partial?view=aspnetcore-6.0
第四部分:安全
本部分讨论编程的安全方面,包括身份验证、授权和加密,并解释在任何企业应用程序中,Web 层和服务层都需要如何进行安全保护。
本部分包括以下章节:
-
第十二章*,理解身份验证*
-
第十三章*,在.NET 6 中实现授权*
第十二章:第十二章:理解身份验证
到目前为止,我们已经构建了我们电子商务应用的用户界面(UI)和服务层。在本章中,我们将学习如何对其进行安全保护。我们的电子商务应用应该能够唯一地识别一个用户并对该用户的请求做出响应。建立用户身份的一个常用模式是提供用户名和密码。然后,这些信息将与存储在数据库或应用中的用户配置文件数据进行比较。如果匹配,将生成并存储一个包含用户身份的 cookie 或 token 在客户端的浏览器中,以便在后续请求中,将 cookie/token 发送到服务器并验证以服务请求。
身份验证是一个识别访问您应用程序受保护区域的用户或程序的过程。例如,在我们的电子商务应用中,用户可以浏览显示的不同页面和产品。然而,要下订单或查看过去的订单,用户需要提供用户名和密码来识别自己。如果用户是新的,他们应该创建这些信息以继续。
在本章中,我们将学习 ASP.NET Core 提供的与身份验证相关的功能,并了解实现身份验证的各种方法。在本章中,我们将涵盖以下主题:
-
理解身份验证的元素
-
ASP.NET Core Identity 简介
-
理解 OAuth 2.0
-
Azure Active Directory(Azure AD)简介
-
Windows 身份验证简介
-
理解保护客户端和服务器应用程序的最佳实践
技术要求
对于本章,您需要具备 Azure、Entity Framework(EF)、Azure AD B2C 和一个具有贡献者角色的活动 Azure 订阅的基本知识。如果您没有,您可以在 azure.microsoft.com/en-in/free/ 上注册一个免费账户。Visual Studio 2022 用于说明一些示例。您可以从 visualstudio.microsoft.com 下载它。
理解 .NET 6 中身份验证的元素
ASP.NET Core 中的身份验证由身份验证中间件处理,它使用注册的身份验证处理程序执行身份验证。已注册的身份验证处理程序及其相关配置称为身份验证方案。
以下列表描述了身份验证框架的核心元素:
-
Program.cs。它们包含一个身份验证处理程序,并具有配置此处理程序的选择。您可以注册多个身份验证方案以进行身份验证、挑战和禁止操作。或者,您可以在您配置的授权策略中指定身份验证方案。以下是一个注册OpenIdConnect身份验证方案的示例代码:services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(this.Configuration.GetSection("AzureAdB2C"));
在前面的代码片段中,认证服务被注册为使用与 Microsoft 身份平台的OpenIdConnect认证方案。此外,配置文件中AzureAdB2C部分指定的必要设置被用于初始化认证选项。
本章的Azure AD 简介部分将涵盖有关OpenIdConnect和AzureAdB2C的更多详细信息。
-
认证处理程序: 认证处理程序负责认证用户。根据认证方案,它们要么构建一个认证票据(通常,这是一个带有用户身份的令牌/cookie),要么在认证失败时拒绝请求。
-
认证: 此方法负责构建带有用户身份的认证票据。例如,一个 cookie 认证方案会构建一个 cookie,而一个JavaScript 对象表示法(JSON)Web 令牌(JWT)承载方案会构建一个令牌。
-
挑战: 当一个未经身份验证的用户请求需要认证的资源时,此方法由授权调用。根据配置的方案,用户随后会被要求进行认证。
-
禁止: 当一个经过身份验证的用户尝试访问他们无权访问的资源时,此方法由授权调用。
现在,让我们了解如何使用 ASP.NET Core Identity 框架添加认证。
ASP.NET Core Identity 简介
ASP.NET Core Identity 是一个基于成员的系统,它为您的应用程序提供了轻松添加登录和用户管理功能的方法。它提供 UI 和应用程序编程接口(API)来创建新用户账户、提供电子邮件确认、管理用户配置文件数据、管理密码(如更改或重置密码)、执行登录、注销等,并启用多因素认证(MFA)。此外,它允许您与外部登录提供者(如 Microsoft Account、Google、Facebook、Twitter 以及许多其他社交网站)集成。这样,用户可以使用他们现有的账户进行注册,而不是必须创建新的账户,从而增强用户体验。
默认情况下,ASP.NET Core Identity 使用 EF Code-First 方法在 SQL Server 数据库中存储用户信息,如用户名、密码等。此外,它允许您自定义表/列名称,并捕获额外的用户数据,例如用户的出生日期、电话号码等。您还可以自定义它以将数据保存到不同的持久存储中,例如 Azure Table Storage 或 NoSQL 数据库。它还提供了一个 API 来自定义密码散列、密码验证等。
在下一节中,我们将学习如何创建一个简单的 Web 应用程序并将其配置为使用 ASP.NET Core Identity 进行认证。
示例实现
在 Visual Studio 2022 中,创建一个新的项目,选择 ASP.NET Core Web 应用程序 模板,提供你的项目详细信息以继续,并更改 认证类型。你将找到以下选项供你选择:
-
无:如果你不需要你的应用程序进行认证,请选择此选项。
-
个人账户:如果你使用本地存储或 SQL 数据库来管理用户身份,请选择此选项。
-
Microsoft Identity Platform:如果你希望对 Azure AD 或 Azure AD B2C 进行用户认证,请选择此选项。
-
Windows:如果你的应用程序仅可在内网上使用,请选择此选项。
对于这个示例实现,我们将使用本地存储来保存用户数据。选择 个人账户,然后点击 创建 来创建项目,如图所示:
![图 12.1 – 认证类型
![图 12.1 – 图 12.1_B18507.jpg]
图 12.1 – 认证类型
或者,你可以使用 dotnet SQLite 作为数据库存储,如下所示:
dotnet new webapp --auth Individual -o AuthSample
要将 SQL 数据库配置为存储,请运行以下命令,确保应用迁移以在数据库中创建必要的表:
dotnet new webapp --auth Individual -uld -o AuthSample
现在,运行以下命令来构建和运行应用程序:
dotnet run --project ./AuthSample/AuthSample.csproj
你应该会看到以下类似的输出:
![图 12.2 – 参考的 dotnet run 命令输出
![图 12.2 – 参考的 dotnet run 命令输出
图 12.2 – 参考的 dotnet run 命令输出
在前面的屏幕截图中,注意控制台日志和应用程序可访问的 统一资源定位符(URL)及其端口号。
现在应用程序已经启动并运行,请在浏览器中打开 URL 并点击 注册。提供所需详细信息,然后点击 注册 按钮。你可能会在第一次尝试时看到以下错误信息:
![图 12.3 – 由于缺少迁移导致的运行时异常
![图 12.3 – 图 12.3_B18507.jpg]
图 12.3 – 由于缺少迁移导致的运行时异常
你可以点击 Update-Database 来应用迁移并重新运行应用程序。现在,你应该能够注册和登录到应用程序。接下来,让我们检查为我们创建的项目结构。
在 依赖包 下,你会注意到以下 NuGet 包:
-
Microsoft.AspNetCore.Identity.UI: 这是一个 Razor 类库,它包含了整个身份 UI,你可以通过浏览器进行导航——例如,/Identity/Account/Register或/Identity/Account/Login。 -
Microsoft.AspNetCore.Identity.EntityFrameworkCore: 这是由 ASP.NET Core Identity 用于与数据库存储交互的。 -
Microsoft.EntityFrameworkCore.SqlServer: 这是一个用于与 SQLDB 交互的库。
包可以在以下屏幕截图中看到:
![图 12.4 – AuthSample 项目的解决方案资源管理器视图
![图 12.4 – 图 12.4_B18507.jpg]
图 12.4 – AuthSample 项目的解决方案资源管理器视图
现在,让我们检查 Program.cs 的代码。
以下代码注册了启用身份验证功能的身份验证中间件:
app.UseAuthentication();
ApplicationDbContext通过提供包含在appsettings.json中指定的sql database连接字符串的options配置作为依赖服务进行注册,如下所示:
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
AddDefaultIdentity方法注册了生成 UI 并使用IdentityUser作为模型配置默认身份系统的服务。
ASP.NET Core Identity 允许我们配置多个身份选项以满足我们的需求——例如,以下代码允许我们禁用电子邮件确认,配置密码要求,并设置锁定超时设置:
services.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.Password.RequireDigit = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 8;
options.Lockout.DefaultLockoutTimeSpan =
TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
})
.AddEntityFrameworkStores<ApplicationDbContext>();
更多详情,请参阅docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-6.0。
框架
为了进一步自定义 UI 和其他设置,您可以选择性地添加 Razor 类库中包含的源代码。然后,您可以修改生成的源代码以满足您的需求。要创建框架,在解决方案资源管理器中,右键单击项目 | 添加 | 新建框架项 | 身份 | 添加。
这将打开一个窗口,您可以在其中选择要覆盖的文件,如下面的截图所示:
图 12.5 – 覆盖身份模块的对话框
您可以选择覆盖所有文件或仅选择您想要自定义的文件。选择您的数据上下文类,然后单击Identity文件夹——Razor 和相应的 C#文件都将被添加。以下截图说明了基于选择添加的文件:
图 12.6 – AuthSample 项目的解决方案资源管理器视图
更多有关自定义的详情,请参阅docs.microsoft.com/en-us/aspnet/core/security/authentication/scaffold-identity?view=aspnetcore-6.0。
现在,让我们了解如何将 ASP.NET Core 应用程序与外部登录提供者集成。
与外部登录提供者集成
在本节中,我们将学习如何将 ASP.NET Core 应用程序与外部登录提供者集成,例如 Microsoft Account、Google、Facebook、Twitter 等。此外,我们还将探讨如何使用 OAuth 2.0 流程进行身份验证,以便用户可以使用他们现有的凭据注册并访问我们的应用程序。将 ASP.NET Core 应用程序与任何外部登录提供者集成的常见模式如下:
-
获取用于从各自的开发者门户访问 OAuth API 进行身份验证的凭据(通常是客户端 ID 和密钥)。
-
在应用程序设置或用户密钥中配置凭据。
-
接下来,我们需要在添加中间件支持时将相应的 NuGet 包添加到项目中,以使用 OpenID 和 OAuth 2.0 流程。
-
在
Program.cs中,添加AddAuthentication方法以注册身份验证中间件。
配置 Google
要将 Google 配置为外部登录提供者,你需要执行以下步骤:
-
在
developers.google.com/identity/sign-in/web/sign-in创建 OAuth 凭据。 -
在用户密钥中配置凭据。你可以使用
dotnetCLI 将密钥添加到你的项目中,如下所示:dotnet user-secrets set "Authentication:Google:ClientId" "<client-id>" dotnet user-secrets set "Authentication:Google:ClientSecret" "<client-secret>" -
将
Microsoft.AspNetCore.Authentication.GoogleNuGet 包添加到你的项目中,并将以下代码添加到Program.cs:services.AddAuthentication() .AddGoogle(options => { IConfigurationSection googleAuthNSection = Configuration.GetSection ("Authentication:Google"); options.ClientId = googleAuthNSection["ClientId"]; options.ClientSecret = googleAuthNSection["ClientSecret"]; }); -
类似地,你可以添加多个提供者。
要了解如何与其他流行的外部身份验证提供者集成,你可以参考docs.microsoft.com/en-us/aspnet/core/security/authentication/social/?view=aspnetcore-6.0。
完成前面的步骤后,你应该能够使用 Google 凭据登录到你的应用程序。这结束了本节关于在应用程序中使用 ASP.NET Core Identity 与外部登录提供者进行身份验证的内容。在下一节中,让我们看看 OAuth 是什么。
理解 OAuth 2.0
OAuth 2.0 是一种现代且行业标准的安全 Web API 协议。它通过为 Web 应用、单页应用、移动应用等提供特定的授权流程来简化流程,以便访问受保护的 API。
让我们考虑一个用例,其中你想要构建一个网络门户,用户可以从他们喜欢的应用程序(如 Instagram、Facebook 或其他第三方应用程序)同步和查看照片/视频。你的应用程序应该能够代表用户从第三方应用程序请求数据。一种方法涉及存储与每个第三方应用程序相关的用户凭据,并且你的应用程序代表用户发送或请求数据。
这种方法可能导致许多问题。以下是如何概述:
-
你需要设计你的应用程序以安全地存储用户凭据。
-
用户可能不习惯他们的凭据被第三方应用程序在你的应用程序中共享和存储。
-
如果用户更改了他们的凭据,它们需要在你应用程序中更新。
-
在安全漏洞的情况下,欺诈者可以无限制地访问第三方应用程序中用户的数据。这可能导致潜在的收入和声誉损失。
OAuth 2.0 可以通过解决所有这些关注点来处理所有上述用例。让我们看看它是如何做到这一点的,如下所示:
-
用户登录到您的应用程序。为了同步图片/视频,用户将被重定向到第三方应用程序,并需要使用其凭据登录。
-
OAuth 2.0 审查并批准应用程序获取资源的请求。
-
用户带着授权码被重定向回您的应用程序。
-
为了同步图片/视频,您的应用程序可以通过交换授权码并使用该令牌调用第三方应用程序的 API 来获取一个令牌。
-
对于每个请求,第三方应用程序验证令牌并相应地响应。
在 OAuth 流程中,涉及四个方面:客户端、资源所有者、授权服务器和资源服务器。请参考以下截图:
图 12.7 – OAuth2 流程
从前面的截图,我们可以看到以下内容:
-
客户端:这指的是从授权服务器获取令牌并代表资源所有者向资源服务器发出请求的应用程序。
-
资源所有者:这是一个拥有资源/数据并能够授予客户端访问权限的实体。
-
授权服务器:这验证资源所有者并向客户端发行令牌。
-
资源服务器:这是托管资源或与资源所有者相关的数据的服务器,使用载体令牌进行验证,并对来自客户端的请求进行响应或拒绝。
令牌
授权服务器验证用户并提供了 ID 令牌、访问令牌和刷新令牌,这些令牌被原生/网络应用程序用于访问受保护的服务。让我们更深入地了解它们:
-
访问令牌:这是授权服务器作为 OAuth 流程的一部分发行的,通常以 JWT 格式;一个包含发行者、用户、范围、过期时间等信息的基础 64 编码的 JSON 对象。
-
刷新令牌:这是授权服务器与访问令牌一起发行的,客户端应用程序在访问令牌过期之前使用它来请求新的访问令牌。
-
ID 令牌:这是授权服务器作为 OpenID Connect 流程的一部分发行的,可以用来验证用户。
注意
OpenID Connect 是建立在 OAuth2 之上的一个身份验证协议。它可以用来验证身份验证服务器上的用户身份。
授权类型
OAuth 2.0 定义了客户端获取令牌以访问受保护资源的方法——这些被称为授权。它定义了五种授权类型:授权码流程、隐式流程、代表流程、客户端凭据流程和设备授权流程。它们在此概述:
- 授权码流:这种流适用于 Web、移动和单页应用程序,其中您的应用程序需要从另一个服务器获取数据。授权码流从客户端将用户重定向到授权服务器进行身份验证开始。如果成功,用户会同意客户端所需的权限,并被带有授权码重定向回客户端。
在这里,客户端的身份是通过授权服务器配置的重定向统一资源标识符(URI)进行验证的。接下来,客户端通过传递授权码来请求访问令牌,并作为回报,获得访问令牌、刷新令牌和过期日期。客户端可以使用访问令牌来调用 Web API。由于访问令牌的寿命较短,在它们过期之前,客户端应通过传递访问令牌和刷新令牌来请求新的访问令牌。
-
隐式流:这是一个适合单页、基于 JavaScript 的应用程序的简化代码流版本。在隐式流中,授权服务器不是发出授权码,而是只发出访问令牌。在这里,客户端身份未经验证,因为没有必要指定重定向 URL。
-
代表流:这种流最适合客户端调用 Web API(例如,A)的情况,该 API 反过来需要调用另一个 API(例如,B)。流程是这样的:用户将带有令牌的请求发送到 A;A 通过提供 A 的令牌和凭据(如 A 的客户 ID 和客户密钥)从授权服务器请求 B 的令牌。一旦它获得了 B 的令牌,它就在 B 上调用 API。
-
客户端凭据流:这种流用于需要服务器到服务器交互的情况(例如,A 到 B,其中 A 通过其凭据(通常是客户 ID 和客户密钥)获取令牌以与 B 交互,然后使用获得的令牌调用 API)。此请求在 A 的上下文中运行,而不是在用户的上下文中。应授予 A 执行必要操作所需的权限。
-
设备授权流:这种流用于用户需要在没有浏览器的情况下登录设备的情况,例如智能电视、物联网设备或打印机。用户访问移动或 PC 上的网页进行身份验证,并输入设备上显示的代码以获取令牌并刷新设备的令牌以连接。
现在我们已经了解了 OAuth 是什么,在下一节中,让我们了解 Azure AD 是什么,如何将其集成到我们的电子商务应用程序中,以及如何将其用作我们的身份服务器。
Azure AD 简介
Azure AD 是来自 Microsoft 的身份和访问管理(IAM)云服务。它为内部和外部用户提供了一个单一的身份存储库,因此您可以配置应用程序使用 Azure AD 进行身份验证。您可以将本地 Windows AD 同步到 Azure AD;因此,您可以为您的用户提供单点登录(SSO)体验。
用户可以使用他们的工作或学校凭证或个人 Microsoft 账户(如Outlook.com、Xbox 和 Skype)登录。它还允许您原生地添加或删除用户、创建组、进行自助密码重置、启用 Azure MFA 以及更多功能。
使用 Azure AD B2C,您可以自定义用户注册、登录和管理其个人资料的方式。此外,它还允许您的客户使用他们现有的社交凭证,如 Facebook 和 Google,来登录并访问您的应用程序和 API。
Azure AD 符合行业标准协议,如OpenID Connect,也称为OIDC和OAuth 2.0。OIDC 是在 OAuth 2.0 协议之上构建的身份层,用于验证和检索用户的个人资料信息。OAuth 2.0 用于授权,通过不同的流程(如隐式授权流程、代表流程、客户端凭证流程、代码流程和设备授权流程)获取对 HTTP 服务的访问权限。
Web 应用程序中典型的身份验证流程如下:
-
用户尝试访问应用程序的安全内容(例如,我的订单)。
-
如果用户未进行身份验证,则会被重定向到 Azure AD 登录页面。
-
一旦用户提交了他们的凭证,Azure AD 就会进行验证,并将令牌发送回 Web 应用程序。
-
一个 cookie 被保存在用户的浏览器中,并显示用户请求的页面。
-
在后续请求中,会发送一个 cookie 到服务器,用于验证用户。
Azure AD B2C允许您的客户使用他们首选的社交、企业或本地身份来访问您的应用程序或 API。它可以扩展到每天数百万用户和数十亿次身份验证。
让我们尝试将我们的电子商务应用程序与 Azure AD B2C 集成。在高层面上,我们需要执行以下步骤来集成:
-
创建 Azure AD B2C 租户。
-
注册应用程序。
-
添加身份提供者。
-
创建用户流程。
-
更新应用程序代码以进行集成。
注意
作为先决条件,您应该有一个活跃的 Azure 订阅,并具有贡献者角色。如果您还没有,您可以在
azure.microsoft.com/en-in/free/注册一个免费账户。
Azure AD B2C 的设置
使用 Azure AD B2C 作为身份服务将允许我们的电子商务用户注册、创建他们自己的凭证或使用他们现有的社交凭证,如 Facebook 或 Google。让我们看看我们需要执行以下步骤来配置 Azure AD B2C 作为我们的电子商务应用程序的身份服务:
-
登录到 Azure 门户,确保您位于包含您的订阅的同一目录中。
-
在 主页 上,点击 创建资源 并搜索 B2C。然后,从选项列表中选择 Azure Active Directory B2C。
-
选择 创建新的 Azure AD B2C 租户,如图下截图所示:
图 12.8 – Azure AD B2C
- 提供所需的详细信息,然后点击 审查 + 创建。然后,完成以下字段:
组织名称:这是您的 B2C 租户的名称。
内部域名:这是您的租户的内部域名。
国家/地区:选择您的租户应部署的国家或地区。
订阅 和 资源组:提供订阅和资源组详细信息。
这些字段在以下截图中显示:
图 12.9 – 新的 Azure AD B2C 配置部分
- 审查您的详细信息,然后点击 创建。创建新租户可能需要几分钟。一旦创建完成,您将在通知部分看到确认消息。在 通知 弹出窗口中,点击租户名称以导航到新创建的租户,如图下截图所示:
图 12.10 – Azure AD B2C 服务创建确认
- 在以下截图中,请注意 订阅状态 显示为 无订阅。另外,一条警告信息指出您应该 将订阅链接到您的租户。您可以点击链接进行修复,否则可以跳转到 步骤 9 继续配置 Azure AD:
图 12.11 – 显示未链接订阅的警告信息
- 链接将打开与 步骤 3 中相同的屏幕。这次,点击 将现有 Azure AD B2C 租户链接到我的 Azure 订阅 以继续,如图下截图所示:
图 12.12 – 将 Azure AD B2C 租户链接到订阅
- 从下拉列表中选择您的 B2C 租户订阅,提供一个 资源组 值,然后点击 创建 以链接订阅和租户,如图下截图所示:
图 12.13 – 订阅选择
-
您可以通过在 B2C 租户概览部分选择 打开 B2C 租户 链接来导航到您的 B2C 租户,继续配置的下一步,如下所示:
-
您需要将应用程序注册到 Azure AD B2C 租户中,才能将其用作身份服务。
-
您需要选择用户可以用来登录您应用的标识提供者。
-
选择用户流程以定义用户注册或登录的体验,如下所示截图:
-
图 12.14 – 配置 Azure AD B2C 的三个步骤
- 在 管理 下,点击 应用程序注册 并提供以下必要详细信息。然后,点击 注册 以创建 AD 应用程序:
名称:显示应用程序的显示名称。
支持的账户类型:选择 任何标识提供者或组织目录中的账户,这样我们就可以允许用户使用他们现有的凭据进行注册或登录。
重定向 URI:您需要提供用户在成功认证后将被重定向到的应用程序的 URL。目前,我们可以将其留空。
权限:选择 授予 openid 和 offline_access 权限的管理员同意。
以下截图说明了这些字段:
图 12.15 – 注册新的 Azure AD 应用程序
注意
为了本地设置和调试,我们可以使用 localhost 进行配置。这需要替换为您应用程序托管位置的 URL。
- 现在,让我们在 管理 下的 标识提供者 中进行选择以进行配置,如下所示:
本地账户:此选项允许用户以传统的用户名和密码方式在我们的应用程序中注册和登录。以下截图说明了这一点:
图 12.16 – 选择标识提供者
- 让我们配置 Google 作为我们应用程序的标识提供者。您可以通过访问
docs.microsoft.com/en-in/azure/active-directory-b2c/identity-provider-google中的步骤来获取客户端 ID 和密钥。您需要提供的详细信息如下所示:
图 12.17 – Google:新的 OAuth 客户端
在您提供所需详细信息并选择保存后,将生成客户端 ID 和密钥,如下所示:
图 12.18 – Google OAuth 客户端
- 一旦您创建了 认证客户端,请从 标识提供者 中点击 Google,然后提供 客户端 ID 和 客户端密钥 值以完成配置。请参考以下截图:
图 12.19 – 标识提供者配置
-
让我们配置 Facebook 作为我们电子商务应用程序的另一个身份提供者。您可以按照
docs.microsoft.com/en-in/azure/active-directory-b2c/identity-provider-facebook中概述的步骤进行操作。 -
一旦您创建了 客户端身份验证 设置,请点击 Facebook 从 身份提供者 中,然后提供 客户端 ID 和 客户端密钥 值以完成配置。请参阅 图 12.19 了解概述。
-
现在,让我们配置用户流程。用户流程允许您为您的用户配置和自定义身份验证体验。您可以在您的租户中配置多个流程并在您的应用程序中使用它们。用户流程允许您添加多因素认证(MFA)并自定义在注册时从用户那里捕获的信息——例如,他们的名字、国家、邮政编码,以及可选的,您是否希望将他们添加到声明中。您还可以自定义用户界面以获得更好的用户体验。要创建一个流程,请点击 策略 下的 用户流程 并选择一个流程类型,如图下截图所示:https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-appdev-csp10-dn6/img/Figure_12.20_B18507.jpg
图 12.20 – 新用户流程
- 提供必要的详细信息,然后点击 创建 保存:
名称:这是您流程的唯一名称。
身份提供者:选择您的身份提供者。
可选地,您可以选择额外的用户属性,如 名称、邮政编码 等,如图下截图所示:
图 12.21 – 用户流程配置
您可以在 用户属性和令牌声明 部分选择额外的属性,如图下截图所示:
图 12.22 – 额外的属性和声明配置
-
类似地,我们也应该设置密码重置策略。这对于本地账户是必需的。要创建一个,请在 创建用户流程 下选择 密码重置 并提供必要的详细信息。您可以参考 图 12.20。
-
完成了 Azure AD B2C 的最小必要设置后,我们准备测试流程。选择创建的用户流程并点击 运行用户流程。您可以看到为您创建的注册和登录页面,如图下截图所示:
图 12.23 – 登录和注册屏幕
让我们看看在 Packt.Ecommerce.Web 中我们需要进行哪些更改以集成 Azure AD。
将我们的电子商务应用程序与 Azure AD B2C 集成
我们将在 Web 应用程序上配置身份验证以使用 Azure AD B2C。让我们对我们的应用程序进行必要的更改以集成到 B2C 租户,如下所示:
- 将以下两个 NuGet 包添加到我们的
Packt.Ecommerce.Web项目中:
Microsoft.Identity.Web: 这是集成到 Azure AD 所需的主要包。
Microsoft.Identity.Web.UI: 此包生成登录和注销的 UI。在Program.cs中,我们需要使用OpenIdConnect方案以及Azure AD B2C配置添加一个身份验证服务,如下所示:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme).AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAdB2C"));
services.AddRazorPages().AddMicrosoftIdentityUI();
-
在
Configure方法中,在app.UseAuthorization()方法之前添加以下代码:app.UseAuthentication(); -
我们需要将
AzureAdB2C添加到appsettings.json中,如下所示:
实例: https://<domain>.b2clogin.com/tfp。将<domain>替换为您在创建 B2C 租户时选择的名称。
客户端 ID: 这是您在设置 Azure AD B2C 时创建的 AD 应用程序 ID。
域: <domain>.onmicrosoft.com。在此处,将<domain>替换为您在创建 B2C 租户时选择的域名。
更新SignUpSignInPolicyId和ResetPasswordPolicyId,如下所示:
"AzureAdB2C": {
"Instance":
"https://packtecommerce.b2clogin.com/tfp/",
"ClientId": "1ae40a96-60d7-4641-bb81-
bc3a47aad36d",
"Domain": "packtecommerce.onmicrosoft.com",
"SignedOutCallbackPath": "/signout/B2C_1_susi",
"SignUpSignInPolicyId": "B2C_1_packt_commerce",
"ResetPasswordPolicyId": "B2C_1_password_reset",
"EditProfilePolicyId": "",
"CallbackPath": "/signin-oidc"
}
-
您可以为控制器或操作方法添加一个
[Authorize]属性——例如,您可以将它添加到OrdersController.cs中的OrdersController,以强制用户进行身份验证才能访问Orders信息。 -
最后一步是更新回复 URI。为此,请导航到您的租户中的AD 应用程序。在管理下的身份验证部分更新回复 URI,并设置隐式授权权限。
回复 URI 是用户在成功认证后将被重定向到的应用程序的 URL。为了设置应用程序并在本地进行调试,我们可以配置 localhost URL,但一旦您将应用程序部署到服务器,您将需要更新服务器的 URL。
在隐式授权下,选择访问令牌和ID 令牌,这是我们的 ASP.NET Core 应用程序所需的,如图下所示:
图 12.24 – 回复 URL 配置
现在,运行您的应用程序并尝试访问订单页面。您将被重定向到登录和注册页面,如图图 12.23所示。这标志着我们的电子商务应用程序与 Azure AD B2C 的集成完成。
Azure AD 提供了许多更多选项和自定义功能以满足您的需求。有关更多详细信息,您可以查看docs.microsoft.com/en-in/azure/active-directory-b2c。
注意
您可以使用 Duende Identity Server 来设置自己的身份服务器。这使用 OpenID Connect 和 OAuth 2.0 框架来建立身份。它通过 NuGet 提供,并且可以轻松集成到 ASP.NET Core 应用程序中。有关更多详细信息,您可以参考docs.duendesoftware.com/identityserver/v6。
在下一节中,我们将看看如何使用 Windows 身份验证。
Windows 身份验证简介
ASP.NET Core 应用程序可以被配置为使用 Windows 身份验证,其中用户将使用其 Windows 凭据进行身份验证。当您的应用程序托管在 Windows 服务器上且您的应用程序仅限于内部网络时,Windows 身份验证是最佳选择。在本节中,我们将学习如何在 ASP.NET Core 应用程序中使用 Windows 身份验证。
在 Visual Studio 中,选择 --auth Windows 参数以使用 Windows 身份验证创建新的网络应用程序,如下所示:
dotnet new webapp --auth Windows -o WinAuthSample
如果您打开 launchSettings.json,您会注意到 WindowsAuthentication 设置为 true,而 anonymousAuthentication 设置为 false,如下面的代码片段所示。此设置仅在运行 Internet Information Services Express(IIS Express)的应用程序时适用:
"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": false,
"iisExpress": {
"applicationUrl": "http://localhost:21368",
"sslPort": 44384
}
}
当您在 IIS 上托管应用程序时,需要在 web.config 中将 WindowsAuthentication 配置为 true。默认情况下,web.config 不会添加到 .NET Core 网络应用程序中,因此您需要添加它并进行必要的更改,如下面的代码片段所示:
<location path="." inheritInChildApplications="false">
<system.webServer>
<security>
<authentication>
<anonymousAuthentication enabled="false"/>
<windowsAuthentication enabled="true"/>
</authentication>
</security>
</system.webServer>
</location>
上述配置使每个端点都变得安全。即使我们在每个控制器或操作上设置 AllowAnonymous,也不会产生影响。如果您想使任何端点匿名访问,需要将 anonymousAuthentication 设置为 true,并设置您想要使其安全的端点的 Authorize。此外,您还需要将身份验证服务与方案 Windows 注册,如下所示:
services.AddAuthentication(IISDefaults.AuthenticationScheme)
这就是您需要在应用程序中启用 Windows 身份验证所需要做的全部。有关更多详细信息,您可以参考 docs.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth?view=aspnetcore-6.0。
在下一节中,我们将探讨一些确保客户端和服务器应用程序安全的最佳实践。
理解确保客户端和服务器应用程序安全性的最佳实践
有几个最佳实践建议用于确保您的网络应用程序的安全性。.NET Core 和 Azure 服务使确保其采用变得容易。以下是一些您可以考虑的关键点:
-
为网络应用程序强制执行 HTTPS。使用
UseHttpsRedirection中间件将请求从 HTTP 重定向到 HTTPS。 -
使用基于 OAuth 2.0 和 OIDC 的现代身份验证框架来保护您的网络或 API 应用程序。
-
如果您使用的是 Microsoft 身份平台,请使用如 MSAL.js 和 MSAL.NET 这样的开源库来获取或更新令牌。
-
配置强密码要求,并在连续登录失败尝试的情况下锁定账户——例如,连续五次失败尝试。这可以防止暴力攻击。
-
为特权账户如后台办公室管理员、后台办公室员工账户等启用多因素认证(MFA)。
-
配置会话超时,在注销时使会话无效,并清除 cookies。
-
在所有受保护端点和客户端上强制执行授权。
-
将密钥/密码存储在安全位置,例如密钥库中。
-
如果您正在使用 Azure AD,请分别注册每个逻辑/环境特定的应用程序。
-
不要以纯文本形式存储敏感信息。
-
确保适当的异常处理。
-
对上传的文件进行安全/恶意软件扫描。
-
防止跨站脚本攻击——始终对用户输入数据进行 HTML 编码。
-
通过参数化 SQL 查询和使用存储过程来防止 SQL 注入攻击。
-
防止跨站请求伪造攻击——在操作、控制器或全局上使用
ValidateAntiForgeryToken过滤器。 -
在中间件中使用此策略强制执行跨源请求(CORS)。
虽然提供的一些最佳实践和指导是好的起点,但您需要始终考虑应用程序的上下文,并持续评估和增强您的应用程序,以解决安全漏洞和威胁。
摘要
在本章中,我们了解了什么是身份验证以及 ASP.NET Core 中身份验证的关键要素。我们探讨了 ASP.NET Core 框架提供的不同选项,并学习了如何使用 ASP.NET Core Identity 快速将身份验证添加到您的应用程序中。我们讨论了 OAuth 2.0 和授权流程,并了解了它们在您需要身份验证和连接到多个 API 服务时如何简化事情。
此外,我们还探讨了将 Azure AD 配置为您的身份服务,在您的应用程序中使用外部身份验证提供程序,如 Google 或 Facebook,以及在 ASP.NET Core 应用程序中使用 Windows 身份验证。我们通过讨论在开发服务器端和客户端应用程序时应该遵循的一些最佳实践来结束本章。
在下一章中,我们将了解授权是什么以及它是如何帮助控制对您资源的访问的。
问题
- 从 JWT 中可以提取哪些信息?
a. 发行者
b. 过期
c. 范围
d. 主题
e. 以上所有
答案:e
- 推荐用于单页应用的 OAuth 授权流程是什么?
a. 客户端凭据流程
b. 隐式流程
c. 代码授权流程
d. 代表流程
答案:b 和 c
- 集成 Azure AD 所需的最小 NuGet 包是什么?
a. Microsoft.AspNetCore.Identity
b. Microsoft.Identity.Web.UI
c. Microsoft.AspNetCore.Identity.UI
d. Microsoft.Identity.Web
答案:d
进一步阅读
要了解更多关于身份验证的信息,您可以参考以下资源:
第十三章:第十三章: 在.NET 6 中实现授权
构建安全应用程序的一个重要方面是确保用户只能访问他们需要的资源。在现实生活中,当你入住酒店时,前台员工会验证你的身份证和信用卡,并分配一张房卡以进入你的房间。根据你选择的房型,你可能享有一些特权,例如进入休息室、游泳池或健身房等。在这里,验证你的身份证和信用卡以及分配房卡被称为认证,而允许你访问各种资源被称为授权。所以,为了进一步解释,使用房卡,我们无法识别你是谁,但可以确定你能做什么。
授权是一种机制,通过它你可以确定用户可以做什么,并授予或拒绝对应用程序资源的访问权限。例如,我们电子商务应用程序的用户应该能够浏览产品、将它们添加到购物车并结账购买,而只有管理员或后台办公室用户应该能够添加或更新产品信息、更新产品价格以及批准或拒绝订单。
在本章中,我们将学习什么是授权,以及使用 ASP.NET Core 框架实现授权的各种方法。本章涵盖了以下主题:
-
理解.NET 6 中的授权
-
简单授权
-
基于角色的授权
-
基于声明(Claims)的授权
-
基于策略的授权
-
自定义授权
-
客户端和服务器应用程序中的授权
技术要求
对于本章,你需要具备 Azure、Azure AD B2C、C#、.NET Core 和 Visual Studio 2022 的基本知识。
回到一些基础知识
在我们深入更多细节之前,让我们了解认证和授权之间的区别。
认证和授权看起来可能相似,并且可以互换使用,但本质上它们是不同的。以下表格说明了这些差异:
表 13.1
注意
请参阅第十二章,理解认证,以获取更多关于 ASP.NET 6 中认证工作原理的详细信息。
总结一下,认证和授权是相辅相成的。授权只有在用户的身份确定之后才会生效,当用户尝试访问受保护资源时,授权会触发认证挑战。在本章的后续部分,我们将了解如何在 ASP.NET 6 应用程序中实现授权。
理解授权
ASP.NET Core 中的授权由 中间件 处理。当你的应用程序收到来自未经身份验证用户的第一个请求到受保护资源时,中间件将触发身份验证挑战,根据身份验证方案,用户要么被重定向到登录页面,要么访问被禁止。一旦在身份验证后确定了用户的身份,授权中间件会检查用户是否可以访问该资源。在后续请求中,授权中间件使用用户的身份来确定是否允许访问。
要在项目中配置授权中间件,你需要在 Program.cs 中调用 UseAuthorization()。在注册授权中间件之前,必须先注册身份验证中间件,因为授权只能在确定用户身份后执行。请参考以下代码:
var builder = WebApplication.CreateBuilder(args);
..
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
在前面的代码块中,你会注意到 app.UseAuthorization() 在 app.UseAuthentication() 之后和 app.UseEndpoints() 之前被调用。
ASP.NET 6 提供了简单、基于角色和声明的授权模型以及丰富的基于策略的模型。在接下来的章节中,我们将学习更多关于这些模型的具体细节。
简单授权
在 ASP.NET Core 中,授权是通过 AuthorizationAttribute 配置的。你可以在控制器、操作或 Razor 页面上应用 [Authorize] 属性。当你添加这个属性时,对该组件的访问权限将仅限于经过身份验证的用户。请参考以下代码块:
public class HomeController : Controller
{
[Authorize]
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
}
在前面的代码中,你会注意到 [Authorize] 属性被添加到了 Index 操作上。当用户尝试从浏览器访问 /Home/Index 时,中间件会检查用户是否已经经过身份验证。如果没有,用户将被重定向到登录页面。
如果我们将 [Authorize] 属性添加到一个控制器中,那么该控制器下所有操作的访问权限将仅限于经过身份验证的用户。在下面的代码中,你会注意到 [Authorize] 属性被添加到了 HomeController 中,使得其下的所有操作都变得安全:
[Authorize]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[AllowAnonymous]
public IActionResult Privacy()
{
return View();
}
}
有时,你可能希望允许应用程序的某些区域对任何用户都开放;例如,登录或重置密码页面应该对所有用户开放,无论用户是否经过身份验证。为了满足这样的要求,你可以在控制器或操作上添加 [AllowAnonymous] 属性,使它们对未经身份验证的用户也开放。
在前面的代码中,你会注意到 [AllowAnonymous] 属性被添加到了 Privacy 操作上,尽管控制器上已经有了 [Authorize] 属性。这个要求被操作方法上的 [AllowAnonymous] 属性覆盖,因此 Privacy 操作对所有用户都是可访问的。
注意
[AllowAnonymous] 属性会覆盖所有授权配置。如果你在一个控制器上设置了 [AllowAnonymous],那么在它下面的任何操作方法上设置 [Authorize] 属性将没有任何影响。在这种情况下,操作方法上的 Authorize 属性将被完全忽略。
到目前为止,我们已经看到了如何保护控制器或操作方法。在下一节中,我们将看到如何在 ASP.NET Core 应用程序中全局启用授权。
全局启用授权
到目前为止,我们已经看到了如何使用 [Authorize] 属性来保护控制器或操作方法。在大型项目中,为每个控制器或操作设置 authorize 属性是不可持续的;你可能会错过为新添加的控制器或操作方法配置它,这可能导致安全漏洞。
ASP.NET Core 允许你通过在应用程序中添加回退策略来全局启用授权。你可以在 Program.cs 中定义回退策略。回退策略将应用于所有未定义显式授权要求的请求:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
在全局范围内添加策略强制用户进行身份验证才能访问应用程序中的任何操作方法。这个选项很有用,因为你不需要为应用程序中的每个控制器/操作指定 [Authorize] 属性。
你仍然可以在控制器或操作方法上设置 [AllowAnonymous] 属性来覆盖默认行为,使其匿名访问。
现在我们已经了解了如何实现简单的授权,在下一节中,让我们了解基于角色的授权是什么以及它是如何简化实现的。
基于角色的授权
在你的应用程序的某些区域仅对特定用户可用是很常见的。而不是在用户级别授予访问权限,通常的做法是将用户分组到角色中,并授予角色访问权限。让我们考虑一个典型的电子商务应用程序,其中 用户 可以下订单,支持 员工可以查看、更新或取消订单并解决用户查询,而 管理员 角色则批准或拒绝订单,管理库存等。
基于角色的授权可以解决这样的需求。当你创建一个用户时,你可以将其分配到一个或多个角色中,当我们配置 [Authorize] 属性时,我们可以将一个或多个角色名称传递给 Authorize 属性的 Roles 属性。
以下代码限制了 Admin 控制器下所有操作方法的访问权限,仅限于属于 Admin 角色的用户:
[Authorize(Roles ="Admin")]
public class AdminController : Controller
{
public IActionResult Index()
{
return View();
}
}
类似地,你可以在 Authorize 属性的 Roles 属性中指定以逗号分隔的角色名称,这样属于配置的任一角色的用户都将能够访问该控制器下的操作方法。
在以下代码中,你会注意到 User,Support 被用作 [Authorize] 属性 Roles 属性的值;属于 User 或 Support 角色的用户可以访问 OrdersController 的操作方法:
[Authorize(Roles ="User,Support")]
public class OrdersController : Controller
{
public IActionResult Index()
{
return View();
}
}
您也可以指定多个授权属性。如果您这样做,用户必须是所有指定角色的成员才能访问它。
在以下代码中,对 InventoryController 配置了多个 [Authorize] 属性,用于 InventoryManager 和 Admin 角色。要访问 Inventory 控制器,用户必须具有 InventoryManager 和 Admin 角色:
[Authorize(Roles ="InventoryManager")]
[Authorize(Roles ="Admin")]
public class InventoryController : Controller
{
public IActionResult Index()
{
return View();
}
[Authorize(Roles ="Admin")]
public IActionResult Approve()
{
return View();
}
}
您可以通过指定授权属性进一步限制对 Inventory 控制器下操作方法的访问。在前面的代码中,用户必须具有 InventoryManager 和 Admin 角色才能访问 Approve 操作。
通过编程方式,如果您想检查用户是否属于某个角色,您可以使用 ClaimsPrinciple 的 IsInRole 方法。在以下示例中,您会注意到 User.IsInRole 接受 roleName,并根据用户的角色返回 true 或 false:
public ActionResult Index()
{
if (User.IsInRole("Admin"))
{
// Handle your logic
}
return View();
}
到目前为止,我们已经看到了如何通过在授权属性中指定角色名称来通过指定角色名称来保护控制器或操作。在下一节中,我们将看到如何使用基于策略的角色授权将这些配置集中在一个地方。
基于策略的角色授权
我们也可以在 Program.cs 中将角色要求定义为策略。这种方法非常有用,因为您可以在一个地方创建和管理基于角色的访问要求,并使用策略名称而不是角色名称来控制访问。为了定义基于策略的角色授权,我们需要在 Program.cs 中将一个或多个角色要求注册为授权策略,并将策略名称提供给 Authorize 属性的 Policy 属性。
在以下代码中,通过添加具有 Admin 角色的要求创建 AdminAccessPolicy:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminAccessPolicy",
policy => policy.RequireRole("Admin"));
});
在您的控制器中,您可以指定要应用的政策如下,并且对 AdminController 的访问被限制为具有 Admin 角色的用户:
[Authorize(Policy ="AdminAccessPolicy")]
public class AdminController : Controller
{
public IActionResult Index()
{
return View();
}
}
在定义策略时,您可以指定多个角色。当使用该策略授权用户时,属于任何角色的用户都可以访问资源。例如,以下代码将允许具有 User 或 Support 角色的用户访问资源:
options.AddPolicy("OrderAccessPolicy",
policy => policy.RequireRole("User","Support"));
您可以在控制器或操作方法上使用带有 Authorize 属性的 OrderAccessPolicy 策略来控制访问。
现在我们已经了解了如何使用基于角色的授权,在下一节中,我们将创建一个简单的应用程序并将其配置为使用基于角色的授权。
实现基于角色的授权
让我们创建一个示例应用程序,使用 ASP.NET Core Identity 实现基于角色的授权:
-
创建一个新的 ASP.NET Core 项目。您可以使用以下
dotnetIndividual账户作为Authentication模式,并使用SQLite作为数据库存储:dotnet new mvc --auth Individual -o AuthSample -
你需要在
Program.cs中调用AddRoles<IdentityRole>()来启用角色服务。你可以参考以下代码来启用它。你还会注意到RequireConfirmedAccount被设置为false。这对于本示例是必需的,因为我们以编程方式创建用户:{ var builder = WebApplication.CreateBuilder(args); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlite(connectionString)); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = false) .AddRoles<IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>(); builder.Services.AddControllersWithViews(); } -
接下来,我们需要创建角色和用户。为此,我们将在
Program.cs中添加两个方法,SetupRoles和SetupUsers。我们可以使用RoleManager和UserManager服务来创建角色和用户。在以下代码中,我们创建了三个角色。使用IServiceProvider,我们获取roleManager服务的实例,然后我们使用RoleExistsAsync和CreateAsync方法来创建它://Add this method to Program.cs async Task SetupRoles(IServiceProvider serviceProvider) { var rolemanager = serviceProvider .GetRequiredService<RoleManager<IdentityRole>>(); string[] roles = { "Admin", "Support", "User" }; foreach (var role in roles) { var roleExist = await rolemanager.RoleExistsAsync(role); if (!roleExist) { await rolemanager.CreateAsync(new IdentityRole(role)); } } } -
同样,我们使用
userManager服务创建用户并分配一个角色。在以下代码中,我们创建了两个用户 –admin@abc.com,分配了admin角色,以及support@abc.com,分配了support角色://Add this method to Program.cs async Task SetupUsers(IServiceProvider serviceProvider) { var userManager = serviceProvider .GetRequiredService<UserManager<IdentityUser>>(); var adminUser = await userManager.FindByEmailAsync("admin@abc.com"); if (adminUser == null) { var newAdminUser = new IdentityUser { UserName = "admin@abc.com", Email = "admin@abc.com", }; var result = await userManager .CreateAsync(newAdminUser, "Password@123"); if (result.Succeeded) await userManager.AddToRoleAsync(newAdminUser, "Admin"); } var supportUser = await userManager .FindByEmailAsync("support@abc.com"); if (supportUser == null) { var newSupportUser = new IdentityUser { UserName = "support@abc.com", Email = "support@abc.com", }; var result = await userManager .CreateAsync(newSupportUser, "Password@123"); if (result.Succeeded) await userManager.AddToRoleAsync(newSupportUser, "Support"); } } -
要调用这两个方法,我们需要一个
IServiceProvider的实例。以下代码获取IServiceProvider的实例来设置角色和用户数据:// // var app = builder.Build(); using (var scope = app.Services.CreateScope()) { var services = scope.ServiceProvider; await SetupRoles(services); await SetupUsers(services); } -
在
Home控制器内部,添加以下代码。为了简化实现,我们正在使用Index视图。在实际场景中,你需要返回为相应操作方法创建的视图:[Authorize(Roles = "Admin")] public IActionResult Admin() { return View("Index"); } [Authorize(Roles = "Support")] public IActionResult Support() { return View("Index"); } -
可选地,我们可以在
Layout.cshtml中添加逻辑来根据登录用户的角色显示导航链接。以下示例使用IsInRole来检查用户的角色并显示一个链接:<li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a> </li> @if (User.IsInRole("Admin")) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Admin">Admin</a> </li> } @if (User.IsInRole("Support")) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Support">Support</a> </li> }
通过前面的步骤,示例实现完成,你可以运行应用程序来查看其工作方式。
运行应用程序,使用 admin@abc.com 登录,你会注意到 管理员 菜单项是可见的,而 支持 是隐藏的:
图 13.1 – 管理员用户登录视图
当你使用 support@abc.com 登录时,你会注意到 支持 菜单项是可见的,而 管理员 项是隐藏的:
图 13.2 – 支持用户登录视图
在下一节中,我们将看到如何使用声明进行授权。
基于声明的授权
声明是在身份验证成功后与身份相关联的关键值对。声明可以是出生日期、性别或邮政编码等。一个或多个声明可以分配给一个用户。基于声明的授权使用声明的值来确定是否可以授予对资源的访问权限。你可以使用两种方法来验证声明;一种方法只是检查声明是否存在,另一种方法是检查声明是否存在并具有特定的值。
要使用基于声明的授权,我们需要在 Program.cs 中注册一个策略。你需要传递一个声明名称和可选值到 RequireClaim 方法来注册。例如,以下代码注册了 PremiumContentPolicy 并要求 PremiumUser 声明:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("PremiumContentPolicy",
policy => policy.RequireClaim("PremiumUser"));
});
在以下代码中,PremiumContentPolicy授权策略被用于PremiumContentController。它检查用户声明中是否存在PremiumUser声明来授权用户的请求;它不关心声明中的值是什么:
[Authorize(Policy ="PremiumContentPolicy")]
public class PremiumContentController : Controller
{
public IActionResult Index()
{
return View();
}
}
你也可以在定义声明时指定一个值列表。它们将被验证以授予对资源的访问权限。例如,根据以下代码,如果用户具有Country声明,其值为US、UK或IN,则用户请求被授权:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ExpressShippingPolicy",
policy => policy.RequireClaim(ClaimTypes.Country,
"US", "UK", "IN"));
});
在程序上,如果你想检查一个用户是否有声明,你可以通过指定一个匹配条件来使用ClaimsPrincipal的HasClaim方法。
要获取一个声明值,你可以使用FindFirst方法。以下代码展示了示例:
@if (User.HasClaim(x => x.Type == "PremiumUser"))
{
<h1>Yay, you are Premium User!!!, @User.FindFirst(x =>
x.Type == ClaimTypes.Country)?.Value</h1>
}
如在实现基于角色的授权部分所见,在向应用程序添加用户时,你也可以使用UserManager服务向用户添加一个声明。在以下代码中,你会注意到AddClaimAsync方法与IdentityUser和Claim一起被调用:
var user = await userManager.FindByEmailAsync("user@abc.com");
if (user == null)
{
var newUser = new IdentityUser
{
UserName = "user@abc.com",
Email = "user@abc.com",
};
var result = await userManager.CreateAsync(newUser, "Password@123");
if (result.Succeeded)
{
await userManager
.AddToRoleAsync(newUser, "User");
await userManager
.AddClaimAsync(newUser, new Claim("PremiumUser",
"true"));
await userManager
.AddClaimAsync(newUser, new Claim(ClaimTypes.Country,
"US"));
await userManager
.AddClaimAsync(newUser, new Claim(ClaimTypes.DateOfBirth, "1-5-2003"));
}
}
在前面的代码中,你会注意到创建了两个声明,并使用AddClaimAsync方法与用户关联。在下一节中,我们将看到如何使用基于策略的授权。
基于策略的授权
基于策略的授权允许你编写自己的逻辑来处理满足你需求的授权需求。例如,你有一个验证用户年龄的需求,并且只有当用户年龄超过 14 岁时才授权下单。你可以使用基于策略的授权模型来处理这样的需求。
要配置基于策略的授权,我们需要定义一个需求和处理器,然后使用需求注册策略。让我们了解这些组件:
-
策略是通过一个或多个需求定义的。
-
需求是一组数据参数的集合,政策使用这些参数来评估用户的身份。
-
处理器负责评估需求中的数据与上下文之间的数据,并确定是否可以授予访问权限。
在下一节中,我们将看到如何创建一个需求和处理器,并注册一个授权策略。
需求
要创建一个需求,你需要实现IAuthorizationRequirement接口。这是一个标记接口,所以你不需要实现任何成员。例如,以下代码创建了一个MinimumAgeRequirement,其中MinimumAge是一个数据参数:
public class MinimumAgeRequirement: IAuthorizationRequirement
{
public int MinimumAge { get; set; }
public MinimumAgeRequirement(int minimumAge)
{
this.MinimumAge = minimumAge;
}
}
需求处理器
需求处理器封装了允许或拒绝请求的逻辑。它们使用需求属性与AuthorizationHandlerContext进行比较以确定访问权限。处理器可以继承Authorizationhandler<TRequirement>,其中TRequirement是IauthorizationRequirement类型,或者实现IAuthorizationHandler。
在以下示例中,通过从 AuthorizationHandler 继承并使用 MinimumAgeRequirement 作为 TRequirement 来创建 MinimumAgeAuthorizationHandler。我们需要重写 HandleRequirementAsync 来编写自定义授权逻辑,其中用户的年龄是从 DateOfBirth 声明中计算出来的。如果用户的年龄大于或等于 MinimumAge,我们调用 context.Succeed 以授予访问权限。如果声明不存在或不符合年龄标准,则禁止访问:
public class MinimumAgeAuthorizationHandler
: AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
if (context.User.HasClaim(
c => c.Type == ClaimTypes.DateOfBirth))
{
var dateOfBirth = Convert.ToDateTime(
context.User.FindFirst(x =>
x.Type == ClaimTypes.DateOfBirth).Value);
var age = DateTime.Today.Year - dateOfBirth.Year;
if (dateOfBirth > DateTime.Today.AddYears(-age))
age--;
if (age >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
return Task.CompletedTask;
}
}
要标记一个需求为成功,您需要通过传递一个需求作为参数来调用 context.Succeed。您不需要处理失败,因为可能还有另一个处理程序会成功处理相同的需求。如果您想禁止请求,可以调用 context.Fail。
注意
必须在 Program.cs 中为服务集合注册处理程序。
注册策略
在 Program.cs 中,使用名称和需求注册策略。在定义策略时,您可以注册一个或多个需求。
在以下示例中,通过调用 policy.Requirements.Add() 并传递 MinimumAgeRequirement 的新实例来创建一个包含需求的策略。您还会注意到 MinimumAgeAuthorizationHandler 被添加到服务集合中,具有单例作用域:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Over14", policy =>
policy.Requirements.Add(new
MinimumAgeRequirement(14)));
});
builder.Services.AddSingleton<IAuthorizationHandler,
MinimumAgeAuthorizationHandler>();
然后,我们可以在控制器或操作上配置授权策略,根据用户的年龄来限制访问:
[Authorize(Policy ="Over14")]
public class OrdersController : Controller
{
public IActionResult Index()
{
return View();
}
}
如果我们注册了一个包含多个需求的策略,那么所有需求都必须得到满足才能成功授权。
在下一节中,我们将学习如何进一步自定义授权。
自定义授权
在上一节中,我们学习了如何使用基于策略的授权并实现自定义逻辑来处理授权需求。但,并不总是可以在 Program.cs 中像那样注册授权策略。在本节中,我们将看到如何使用 IAuthorizationPolicyProvider 在您的应用程序中动态构建策略配置。
IAuthorizationPolicyProvider 接口有三个方法需要实现:
-
GetDefaultPolicyAsync:此方法返回要使用的默认授权策略。 -
GetFallbackPolicyAsync:此方法返回回退授权策略。当没有定义明确的授权需求时使用。 -
GetPolicyAsync:此方法用于构建并返回提供的策略名称的授权策略。
让我们来看一个例子,您可能想要根据不同的年龄标准授权对多个控制器/操作的请求,比如 Over14、Over18、Over21、Over60 等。实现它的一个方法是将所有这些需求注册为策略并在您的控制器或操作中使用它们。但是,使用这种方法,代码的可维护性较低,在具有许多策略的大型应用程序中不可持续。让我们看看我们如何利用授权策略提供者。
我们需要创建一个实现 IAuthorizationPolicyProvider 的类,并实现 GetPolicy 和其他方法。
在以下示例中,MinimumAgePolicyProvider 类实现了 GetPolicyAsync 方法。此方法的输入是策略名称。由于我们的策略名称类似于 Over14 或 Over18,我们可以使用字符串函数从中提取年龄,并使用所需的年龄初始化一个要求,并将其注册为新的策略:
public class MinimumAgePolicyProvider : IAuthorizationPolicyProvider
{
const string PREFIX = "Over";
public Task<AuthorizationPolicy?>
GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(PREFIX,
StringComparison.OrdinalIgnoreCase) &&
int.TryParse(policyName.Substring(
PREFIX.Length), out var age))
{
var policy = new
AuthorizationPolicyBuilder();
policy.AddRequirements(new
MinimumAgeRequirement(age));
return Task.FromResult
<AuthorizationPolicy?>(policy.Build());
}
return
Task.FromResult<AuthorizationPolicy?>(null);
}
}
注意
对于 MinimumAgeRequirement 的实现,请参阅 基于策略的授权 部分。
ASP.NET Core 只使用一个 IAuthorizationPolicyProvider 实例。因此,您应该自定义一个 Default 和 Fallback 授权策略,或者使用备用提供程序。
在以下代码中,您将看到 MinimumAgePolicyProvider 类中 GetDefaultPolicyAsync 和 GetFallbackPolicyAsync 方法的示例实现。
AuthorizationOptions 注入到构造函数中,并用于初始化 DefaultAuthorizationPolicyProvider。BackupPolicyProvider 对象用于实现 GetDefaultPolicyAsync 和 GetFallbackPolicyAsync 方法:
public MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options)
{
this.BackupPolicyProvider =
new DefaultAuthorizationPolicyProvider(options);
}
Private DefaultAuthorizationPolicyProvider BackupPolicyProvider { get; }
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
=> this.BackupPolicyProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
=> this.BackupPolicyProvider.GetFallbackPolicyAsync();
这就完成了 MinimumAgePolicyProvider 的实现。现在您可以在控制器或操作方法上使用授权策略。在以下代码中,您会注意到使用了两个策略,一个在控制器上使用 Over14,另一个在 Index 操作方法上使用 Over18:
[Authorize(Policy ="Over14")]
public class OrdersController : Controller
{
[Authorize(Policy ="Over18")]
public IActionResult Index()
{
return View();
}
}
年龄超过 14 岁的用户将能够访问 OrdersController 下的任何操作方法,而年龄超过 18 岁的用户只能访问 Index 操作。
在下一节中,我们将学习如何创建和使用自定义授权属性。
自定义授权属性
在前面的示例中,一个包含年龄的策略名称作为字符串传递,但这种方式代码不够整洁。如果能够将 age 作为参数传递给授权属性会更好。为此,您需要创建一个继承自 AuthorizeAttribute 类的自定义授权属性。
在以下示例代码中,AuthorizeAgeOverAttribute 类继承自 AuthorizeAttribute 类。该类的构造函数接受 age 作为输入。在设置器中,我们通过连接 PREFIX 和 Age 来构造并设置一个策略名称:
public class AuthorizeAgeOverAttribute : AuthorizeAttribute
{
const string PREFIX = "Over";
public AuthorizeAgeOverAttribute(int age) => Age = age;
public int Age
{
get
{
if
(int.TryParse(Policy.Substring(PREFIX.Length),
out var age))
{
return age;
}
return default(int);
}
set
{
Policy = $"{PREFIX}{value.ToString()}";
}
}
}
要使用 AuthorizeAgeOver 属性,我们必须在 Program.cs 中注册 AuthorizationHandler 和 AuthorizationPolicyProvider 服务。在以下代码中,MinimumAgeAuthorizationHandler 和 MinimumAgePolicyProvider 类型分别注册为 Singleton 用于 IAuthorizationHandler 和 IauthorizationPolicyProvider:
builder.Services.AddSingleton<IAuthorizationHandler,
MinimumAgeAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationPolicyProvider,
MinimumAgePolicyProvider>();
现在自定义属性实现完成后,我们可以在控制器或操作方法上使用它。在以下示例中,您可以看到一个示例实现,其中年龄作为参数传递给我们的自定义授权属性 AuthorizeAgeOver:
[AuthorizeAgeOver(14)]
public class OrdersController : Controller
{
[AuthorizeAgeOver(18)]
public IActionResult Index()
{
return View();
}
}
在下一节中,我们将学习如何在 Azure AD 应用程序中配置角色并使用基于角色的身份验证。
客户端和服务器应用程序的授权
在前面的章节中,我们学习了如何使用Azure Active Directory(AAD)作为身份服务来验证用户,但为了使用基于角色的授权,我们需要在 Azure AD 中进行一些配置更改。在本节中,我们将了解如何在 Azure AD 应用程序中启用和创建自定义角色,并在我们的电子商务应用程序中授权用户。
当用户登录到应用程序时,Azure AD 会将分配的角色和声明添加到用户的身份中。
前提条件
您应该已经设置了 Azure AD 和 AD 应用程序。如果没有,您可以参考第十二章的Azure Active Directory 简介部分,了解身份验证以进行设置。
让我们看看在 Azure AD 应用程序中启用角色需要执行的步骤:
-
在 Azure 门户中,导航到您的Active Directory租户。
-
在左侧菜单下,在管理中,选择应用注册:
图 13.3 – Azure AD 应用程序
- 在应用注册页面搜索并选择您的 AD 应用程序。请参考以下截图:
图 13.4 – Azure AD 应用程序
- 点击左侧菜单中的清单进行编辑,如图中所示。
图 13.5 – 编辑清单
-
定位
appRoles以配置多个角色。请参考以下代码以添加角色:{ "allowedMemberTypes": [ "User" ], "description": "Admin Users", "displayName": "Admin", "id": "6ef9b400-0219-463c-a542-5f4693c4e286", "isEnabled": true, "lang": null, "origin": "Application", "value": "Admin" }
您需要为displayName、value、description和id提供值。id的值是Guid,并且对于您添加的每个角色都必须是唯一的。同样,对于value,您需要提供在代码中引用的角色名称,并且它应该是唯一的。
- 保存清单以完成它。
保存包含所需详细信息的清单将启用 Azure AD 应用程序中的自定义角色。在下一节中,我们将学习如何将这些自定义角色分配给用户。
为用户分配角色
下一步是为用户分配角色。用户角色的分配可以通过 Azure 门户或使用 Graph API 编程方式完成。在本节中,我们将使用 Azure 门户来分配角色,同样也可以使用 Graph API 实现。更多信息,请参阅docs.microsoft.com/en-us/graph/azuread-identity-access-management-concept-overview:
-
在 Azure 门户中,导航到Azure Active Directory租户。
-
点击左侧菜单中的企业应用程序并搜索并选择您的 AD 应用程序。
-
前往管理 | 用户和组 | 添加用户。
-
搜索并选择用户,然后点击确定。
-
点击选择角色以选择您想要分配的角色。
-
点击分配以保存选择。
您可以继续这些步骤将角色分配给多个用户。
要保护控制器或操作,您可以在角色一起添加 Authorize 属性。在以下代码中,Admin 控制器仅对具有 Admin 角色的用户可访问:
[Authorize(Roles ="Admin")]
public class AdminController : Controller
{
public IActionResult Index()
{
return View();
}
}
到目前为止,我们学习了如何在 Azure AD 中启用角色并使用基于角色的模型进行授权。在下一节中,我们将看到如何使用视图中的用户身份访问角色和声明。
视图中的用户身份
用户声明原则可用于视图,根据需要有条件地显示或隐藏数据。例如,以下代码检查用户身份的 IsAuthenticated 属性以确定用户是否已认证。如果用户未认证,则显示 Sign in 链接;否则,显示带有 Sign out 链接的用户名:
<ul class="navbar-nav">
@if (User.Identity.IsAuthenticated)
{
//// HTML code goes here
}
else
{
////
}
</ul>
类似地,我们可以使用 IsInRole 或 HasClaim 并编写我们的逻辑来向用户显示内容或从用户隐藏内容:
@if (User.HasClaim(x => x.Type == "PremiumUser"))
{
<h1>Yay, you are Premium User!!!, @User.FindFirst(x => x.Type == ClaimTypes.Country)?.Value</h1>
}
更多详细信息,请参阅docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps。
摘要
在本章中,我们学习了什么是授权以及使用 ASP.NET Core 框架的不同实现方式。我们学习了如何使用简单、声明性的基于角色和声明的模型限制或匿名允许用户访问资源,我们还学习了如何使用丰富的基于策略的授权模型实现自定义逻辑以授权用户请求。
我们学习了如何使用授权策略提供程序动态添加授权策略,构建自定义授权属性。我们还学习了如何在 Azure AD 中配置自定义角色并在 ASP.NET Core 应用程序中使用它们。根据您的授权需求,您可以使用一个或多个授权模型来保护您的应用程序。
在下一章中,我们将学习如何监控 ASP.NET Core 应用程序的健康状况和性能。
问题
阅读本章后,您应该能够回答以下问题:
- 以下哪个是确定授权是否成功的主要服务?
a. IAuthorizationHandler
b. IAuthorizationRequirement
c. IAuthorizationService
d. IAuthorizationPolicyProvider
答案:c
-
在以下代码中,对
Support动作的访问仅限于Support角色:[AllowAnonymous] public class HomeController : Controller { public IactionResult Index() { return View(); } [Authorize(Roles ="Support")] public IactionResult Support() { return View(); } }
a. 正确
b. 错误
答案:b
进一步阅读
要了解更多关于授权的信息,您可以参考docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction?view=aspnetcore-6.0。
2087



被折叠的 条评论
为什么被折叠?



