ASP.NET Core in Action 24 Authorization: Securing your application

24 Authorization: Securing your application‌
24 Authorization: 保护您的应用程序

This chapter covers

本章涵盖

• Using authorization to control who can use your app
使用授权来控制谁可以使用你的应用
• Using claims-based authorization with policies
将基于声明的授权与策略结合使用
• Creating custom policies to handle complex requirements
创建自定义策略以处理复杂要求
• Authorizing a request depending upon the resource being accessed
根据正在访问的资源授权请求
• Hiding elements from a Razor template that the user is unauthorized to access
隐藏用户无权访问的 Razor 模板中的元素

In chapter 23 I showed you how to add users to an ASP.NET Core application by adding authentication. With authentication, users can register and log in to your app using an email address and password. Whenever you add authentication to an app, you inevitably find you want to be able to restrict what some users can do. The process of determining whether a user can perform a given action on your app is called authorization.
在第 23 章中,我向您展示了如何通过添加身份验证将用户添加到 ASP.NET Core 应用程序。通过身份验证,用户可以使用电子邮件地址和密码注册和登录您的应用。每当向应用程序添加身份验证时,您都不可避免地会发现您希望能够限制某些用户可以执行的作。确定用户是否可以对您的应用执行给定作的过程称为授权。

On an e-commerce site, for example, you may have admin users who are allowed to add new products and change prices, sales users who are allowed to view completed orders, and customer users who are allowed only to place orders and buy products.
例如,在电子商务网站上,您可能拥有允许添加新产品和更改价格的管理员用户、允许查看已完成订单的销售用户以及仅允许下订单和购买产品的客户用户。

In this chapter I show how to use authorization in an app to control what your users can do. In section 24.1 I introduce authorization and put it in the context of a real-life scenario you’ve probably experienced: an airport. I describe the sequence of events, from checking in, to passing through security, to entering an airport lounge, and you’ll see how these relate to the authorization concepts in this chapter.
在本章中,我将介绍如何在应用程序中使用授权来控制用户可以执行的作。在 Section 24.1 中,我介绍了 Authorization 并将其置于您可能经历过的真实场景的上下文中:机场。我将介绍事件的顺序,从办理登机手续到通过安检,再到进入机场休息室,您将了解这些事件与本章中的授权概念有何关系。

In section 24.2 I show how authorization fits into an ASP.NET Core web application and how it relates to the ClaimsPrincipal class you saw in the previous chapter. You’ll see how to enforce the simplest level of authorization in an ASP.NET Core app, ensuring that only authenticated users can execute a Razor Page or MVC action. This chapter focuses on authorization in Razor Pages and Model-View- Controller (MVC) controllers; in chapter 25 you’ll learn how the same principles apply to minimal API applications.
在第 24.2 节中,我将展示授权如何适应 ASP.NET Core Web 应用程序,以及它与您在上一章中看到的 ClaimsPrincipal 类的关系。你将了解如何在 ASP.NET Core 应用程序中强制实施最简单的授权级别,确保只有经过身份验证的用户才能执行 Razor Page 或 MVC作。本章重点介绍 Razor Pages 和模型视图控制器 (MVC) 控制器中的授权;在第 25 章中,您将了解相同的原则如何应用于最小的 API 应用程序。

We’ll extend that approach in section 24.3 by adding the concept of policies. These let you set specific requirements for a given authenticated user, requiring that they have specific pieces of information to execute an action or Razor Page.
我们将在 Section 24.3 中通过添加 policies 的概念来扩展该方法。这些允许您为给定的经过身份验证的用户设置特定要求,要求他们具有执行作或 Razor 页面的特定信息。

You’ll use policies extensively in the ASP.NET Core authorization system, so in section 24.4 we’ll explore how to handle more complex scenarios. You’ll learn about authorization requirements and handlers, and how you can combine them to create specific policies that you can apply to your Razor Pages and actions.
您将在 ASP.NET Core 授权系统中广泛使用策略,因此在第 24.4 节中,我们将探讨如何处理更复杂的情况。你将了解授权要求和处理程序,以及如何将它们组合起来以创建可应用于 Razor Pages 和作的特定策略。

Sometimes whether a user is authorized depends on which resource or document they’re attempting to access. A resource is anything that you’re trying to protect, so it could be a document or a post in a social media app. For example, you may allow users to create documents or to read documents from other users, but to edit only documents that they created themselves. This type of authorization, where you need the details of the document to determine if the user is authorized, is called resource-based authorization, and it’s the focus of section 24.5.
有时,用户是否获得授权取决于他们尝试访问的资源或文档。资源是您尝试保护的任何内容,因此它可以是社交媒体应用程序中的文档或帖子。例如,您可以允许用户创建文档或读取其他用户的文档,但只能编辑他们自己创建的文档。这种类型的授权,您需要文档的详细信息来确定用户是否获得授权,称为基于资源的授权,这是 Section 24.5 的重点。

In the final section of this chapter I show how you can extend the resource-based authorization approach to your Razor view templates. This lets you modify the UI to hide elements that users aren’t authorized to interact with. In particular, you’ll see how to hide the Edit button when a user isn’t authorized to edit the entity.
在本章的最后一节中,我将介绍如何将基于资源的授权方法扩展到 Razor 视图模板。这样,您就可以修改 UI 以隐藏用户无权与之交互的元素。具体而言,您将了解如何在用户无权编辑实体时隐藏 Edit (编辑) 按钮。

We’ll start by looking more closely at the concept of authorization, how it differs from authentication, and how it relates to real-life concepts you might see in an airport.
首先,我们将更仔细地研究授权的概念,它与身份验证有何不同,以及它与您可能在机场看到的现实生活中的概念有何关系。

24.1 Introduction to authorization‌

24.1 授权简介

In this section I provide an introduction to authorization and discuss how it compares with authentication. I use the real- life example of an airport as a case study to illustrate how claims-based authorization works.
在本节中,我将介绍授权并讨论它与身份验证的比较。我使用机场的真实示例作为案例研究来说明基于索赔的授权是如何运作的。

For people who are new to web apps and security, authentication and authorization can be a little daunting. It certainly doesn’t help that the words look so similar! The two concepts are often used together, but they’re definitely distinct:
对于刚接触 Web 应用程序和安全性的人来说,身份验证和授权可能有点令人生畏。这些词看起来如此相似当然无济于事!这两个概念经常一起使用,但它们绝对是不同的:

• Authentication—The process of determining who made a request
身份验证 - 确定请求发出者的过程
• Authorization—The process of determining whether the requested action is allowed
授权 - 确定是否允许请求的作的过程

Typically, authentication occurs first so that you know who is making a request to your app. For traditional web apps, your app authenticates a request by checking the encrypted cookie that was set when the user logged in (as you saw in chapter 23). API applications typically use a header instead of a cookie for authentication, but the overall process is the same, as you’ll see in chapter 25.
通常,首先进行身份验证,以便您知道谁在向您的应用发出请求。对于传统的 Web 应用程序,您的应用程序通过检查用户登录时设置的加密 cookie 来验证请求(如第 23 章所示)。API 应用程序通常使用 Headers 而不是 cookie 进行身份验证,但整个过程是相同的,您将在第 25 章中看到。

Once a request is authenticated and you know who is making the request, you can determine whether they’re allowed to execute an action on your server. This process is called authorization and is the focus of this chapter.
在请求经过身份验证并且您知道谁在发出请求后,您可以确定是否允许他们在您的服务器上执行作。此过程称为 authorization,是本章的重点。

Before we dive into code and start looking at authorization in ASP.NET Core, I’ll put these concepts into a real-life scenario that I hope you’re familiar with: checking in at an airport. To enter an airport and board a plane, you must pass through several steps: an initial step to prove who you are (authentication) and subsequent steps that check whether you’re allowed to proceed (authorization). In simplified form, these might look like this:
在我们深入研究代码并开始研究 ASP.NET Core 中的授权之前,我将把这些概念放入一个我希望您熟悉的真实场景中:在机场办理登机手续。要进入机场并登机,您必须通过几个步骤:证明您的身份的初始步骤(身份验证)和检查您是否被允许继续的后续步骤(授权)。在简化形式中,这些可能如下所示:

  1. Show your passport at the check-in desk. Receive a boarding pass.
    在值机柜台出示您的护照。收到登机牌。
  2. Show your boarding pass to enter security. Pass through security.
    出示登机牌进入安检。通过安检。
  3. Show your frequent-flyer card to enter the airline lounge. Enter the lounge.
    出示您的常旅客卡进入航空公司休息室。进入休息室。
  4. Show your boarding pass to board the flight. Enter the airplane.
    出示您的登机牌登机。进入飞机。

Obviously, these steps, also shown in figure 24.1, will vary somewhat in real life (I don’t have a frequent-flyer card!), but we’ll go with them for now. Let’s explore each step a little further.
显然,这些步骤(如图 24.1 所示)在现实生活中会有所不同(我没有常旅客卡!),但我们现在就来介绍一下。让我们进一步探讨每个步骤。

alt text
alt text

Figure 24.1 When boarding a plane at an airport, you pass through several authorization steps. At each authorization step, you must present a claim in the form of a boarding pass or a frequent-flyer card. If you’re not authorized, access is denied.
图 24.1 在机场登机时,您需要完成几个授权步骤。在每个授权步骤中,您必须以登机牌或常旅客卡的形式出示索赔。如果您未获得授权,则访问将被拒绝。

When you arrive at the airport, the first thing you do is go to the check-in counter. Here, you can purchase a plane ticket, but to do so, you need to prove who you are by providing a passport; you authenticate yourself. If you’ve forgotten your passport, you can’t authenticate, and you can’t go any further.
当您到达机场时,您做的第一件事是前往值机柜台。在这里,您可以购买机票,但要购买机票,您需要通过提供护照来证明您的身份;您验证自己。如果您忘记了护照,则无法进行身份验证,也无法继续。

Once you’ve purchased your ticket, you’re issued a boarding pass, which says which flight you’re on. We’ll assume that it also includes a BoardingPassNumber. You can think of this number as an additional claim associated with your identity.
购买机票后,您将收到一张登机牌,上面写着您乘坐的航班。我们假设它还包括 BoardingPassNumber。您可以将此号码视为与您的身份关联的附加声明。

DEFINITION A claim is a piece of information about a user that consists of a type and an optional value.
定义:声明是有关用户的一条信息,由类型和可选值组成。

The next step is security. The security guards ask you to present your boarding pass for inspection, which they use to check that you have a flight and so are allowed deeper into the airport. This is an authorization process: you must have the required claim (a BoardingPassNumber) to proceed.
下一步是安全性。保安要求您出示登机牌以供检查,他们用它来检查您是否有航班,因此可以进入机场更深处。这是一个授权过程:您必须拥有所需的声明 (BoardingPassNumber) 才能继续。

If you don’t have a valid BoardingPassNumber, there are two possibilities for what happens next:
如果您没有有效的 BoardingPassNumber,则接下来有两种可能的情况:

• If you haven’t yet purchased a ticket—You’ll be directed back to the check-in desk, where you can authenticate and purchase a ticket. At that point, you can try to enter security again.
如果您尚未购买机票 - 您将被引导回值机柜台,在那里您可以进行身份验证和购买机票。此时,您可以尝试再次进入安检。

• If you have an invalid ticket—You won’t be allowed through security, and there’s nothing else you can do. If, for example, you show up with a boarding pass a week late for your flight, they probably won’t let you through. (Ask me how I know!)
如果您的机票无效 - 您将不被允许通过安检,并且您无能为力。例如,如果您的航班登机牌晚了一周,他们可能不会让您通过。(问我怎么知道的!)

Once you’re through security, you need to wait for your flight to start boarding, but unfortunately, there aren’t any seats free. Typical! Luckily, you’re a regular flyer, and you’ve notched up enough miles to achieve Gold frequent-flyer status, so you can use the airline lounge.
通过安检后,您需要等待航班开始登机,但不幸的是,没有任何空位。典型!幸运的是,您是一名普通乘客,并且您已经积累了足够的里程来获得金卡常旅客身份,因此您可以使用航空公司休息室。

You head to the lounge, where you’re asked to present your Gold frequent-flyer card to the attendant, and they let you in. This is another example of authorization. You must have a FrequentFlyerClass claim with a value of Gold to proceed.
您前往休息室,在那里您被要求向服务员出示您的黄金常旅客卡,他们让您进入。这是授权的另一个示例。您必须有一个价值为 Gold 的 FrequentFlyerClass 索赔才能继续。

NOTE You’ve used authorization twice so far in this scenario. Each time, you presented a claim to proceed. In the first case, the presence of any BoardingPassNumber was sufficient, whereas for the FrequentFlyerClass claim, you needed the specific value of Gold.
注意:到目前为止,在此方案中,你已使用两次授权。每次,您都会提出要继续的索赔。在第一种情况下,任何 BoardingPassNumber 的存在就足够了,而对于 FrequentFlyerClass 声明,您需要 Gold 的特定值。

When you’re boarding the airplane, you have one final authorization step, in which you must present the BoardingPassNumber claim again. You presented this claim earlier, but boarding the aircraft is a distinct action from entering security, so you have to present it again.
当您登机时,您有一个最后的授权步骤,在该步骤中,您必须再次提供 BoardingPassNumber 声明。您之前提交了此声明,但登机与进入安检是不同的作,因此您必须再次提交。

This whole scenario has lots of parallels with requests to a web app:
整个场景与对 Web 应用程序的请求有很多相似之处:

• Both processes start with authentication.
两个过程都从身份验证开始。
• You must prove who you are to retrieve the claims you need for authorization.
您必须证明您是谁才能检索授权所需的索赔。
• You use authorization to protect sensitive actions like entering security and the airline lounge.
您使用授权来保护敏感作,例如进入安检和航空公司休息室。

I’ll reuse this airport scenario throughout the chapter to build a simple web application that simulates the steps you take in an airport. We’ve covered the concept of authorization in general, so in the next section we’ll look at how authorization works in ASP.NET Core. We’ll start with the most basic level of authorization, ensuring that only authenticated users can execute an action, and look at what happens when you try to execute such an action.
我将在本章中重用这个机场场景来构建一个简单的 Web 应用程序,用于模拟您在机场中采取的步骤。我们已经大致介绍了授权的概念,因此在下一节中,我们将了解如何在 ASP.NET Core 中工作。我们将从最基本的授权级别开始,确保只有经过身份验证的用户才能执行作,并查看当您尝试执行此类作时会发生什么。

24.2 Authorization in ASP.NET Core‌

24.2 ASP.NET Core 中的授权

In this section you’ll see how the authorization principles described in the previous section apply to an ASP.NET Core application. You’ll learn about the role of the [Authorize] attribute and AuthorizationMiddleware in authorizing requests to Razor Pages and MVC actions. Finally, you’ll learn about the process of preventing unauthenticated users from executing endpoints and what happens when users are unauthorized.‌
在本节中,您将了解上一节中描述的授权原则如何应用于 ASP.NET Core 应用程序。你将了解 [Authorize] 属性和 AuthorizationMiddleware 在授权对 Razor Pages 和 MVC作的请求中的作用。最后,您将了解防止未经身份验证的用户执行终端节点的过程,以及当用户未经授权时会发生什么。

The ASP.NET Core framework has authorization built in, so you can use it anywhere in your app, but it’s most common to apply authorization via the AuthorizationMiddleware. The AuthorizationMiddleware should be placed after both the routing middleware and the authentication middleware but before the endpoint middleware, as shown in figure 24.2.
ASP.NET Core 框架内置了授权,因此您可以在应用程序中的任何位置使用它,但最常见的是通过 AuthorizationMiddleware 应用授权。AuthorizationMiddleware 应该放在 routing 中间件和 authentication 中间件之后,但在 endpoint middleware 之前,如图 24.2 所示。

alt text

Figure 24.2 Authorization occurs after an endpoint has been selected and after the request is authenticated, but before the action method or Razor Page endpoint is executed.
图 24.2 在选择终结点之后和对请求进行身份验证之后,但在执行作方法或 Razor Page 终结点之前,进行授权。

NOTE Remember that in ASP.NET Core, an endpoint refers to the handler selected by the routing middleware, which generates a response when executed. It is typically a Razor Page, a web API controller action method, or a minimal API endpoint handler.
注意请记住,在 ASP.NET Core 中,终端节点是指路由中间件选择的处理程序,该处理程序在执行时生成响应。它通常是 Razor Page、Web API 控制器作方法或最小 API 端点处理程序。

With this configuration, the RoutingMiddleware selects an endpoint to execute based on the request’s URL, such as a Razor Page, as you saw in chapter 14. Metadata about the selected endpoint is available to all middleware that occurs after the routing middleware. This metadata includes details about any authorization requirements for the endpoint, and it’s typically attached by decorating an action or Razor Page with an [Authorize] attribute.
使用此配置,RoutingMiddleware 根据请求的 URL 选择要执行的终结点,例如 Razor Page,如第 14 章所示。有关所选终端节点的元数据可用于路由中间件之后出现的所有中间件。此元数据包括有关终结点的任何授权要求的详细信息,通常通过使用 [Authorize] 属性修饰作或 Razor 页面来附加。

The AuthenticationMiddleware deserializes the encrypted cookie (or bearer token for APIs) associated with the request to create a ClaimsPrincipal. This object is set as the HttpContext.User for the request, so all subsequent middleware can access this value. It contains all the Claims that were added to the cookie when the user authenticated.
AuthenticationMiddleware 反序列化与创建 ClaimsPrincipal 的请求关联的加密 Cookie(或 API 的持有者令牌)。此对象设置为请求的 HttpContext.User,因此所有后续中间件都可以访问此值。它包含在用户进行身份验证时添加到 Cookie 的所有声明。

NOTE Remember that the authentication middleware may be placed before the routing middleware when the authentication process is the same for all endpoints.
注意:请记住,当所有端点的身份验证过程都相同时,可以将身份验证中间件放在路由中间件之前。

Nevertheless, I prefer to place it as shown in figure 24.2, after the routing middleware, and always before the authorization middleware.
尽管如此,我更喜欢将它如图 24.2 所示,放在 routing middleware 之后,并且总是放在 authorization middleware 之前。

Now we come to the AuthorizationMiddleware. This middleware checks whether the selected endpoint has any authorization requirements, based on the metadata provided by the RoutingMiddleware. If the endpoint has authorization requirements, the AuthorizationMiddleware uses the HttpContext.User to determine whether the current request is authorized to execute the endpoint.
现在我们来看看 AuthorizationMiddleware。此中间件根据 RoutingMiddleware 提供的元数据检查所选端点是否具有任何授权要求。如果端点有授权要求,则 AuthorizationMiddleware 使用 HttpContext.User 来确定当前请求是否被授权执行端点。

If the request is authorized, the next middleware in the pipeline executes as normal. If the request is not authorized, the AuthorizationMiddleware short-circuits the middleware pipeline, and the endpoint middleware is never executed.
如果请求获得授权,则管道中的下一个中间件将正常执行。如果请求未获得授权,则 AuthorizationMiddleware 将使中间件管道短路,并且永远不会执行端点中间件。

NOTE The call to UseAuthorization() must always be placed after UseRouting() and UseAuthentication(), but before UseEndpoints(). WebApplication automatically adds all this middleware in the correct order, but if you override the position in the pipeline, such as by calling UseRouting(), you must make sure to maintain this overall order.
注意:对 UseAuthorization() 的调用必须始终放在 UseRouting() 和 UseAuthentication() 之后,但在 UseEndpoints() 之前。WebApplication 会自动以正确的顺序添加所有这些中间件,但是如果你覆盖管道中的位置,例如通过调用 UseRouting(),则必须确保保持这个整体顺序。

The AuthorizationMiddleware is responsible for applying authorization requirements and ensuring that only authorized users can execute protected endpoints. In section you’ll learn how to apply the simplest authorization requirement to an endpoint, and in section 24.2.2 you’ll see how the framework responds when a user is not authorized to execute an endpoint.
AuthorizationMiddleware 负责应用授权要求并确保只有授权用户才能执行受保护的端点。在部分您将学习如何将最简单的授权要求应用于 Endpoint,在 Section 24.2.2 中,您将看到当用户无权执行 Endpoint 时框架如何响应。

24.2.1 Preventing anonymous users from accessing your application‌

24.2.1 阻止匿名用户访问您的应用程序

When you think about authorization, you typically think about checking whether a particular user has permission to execute an endpoint. In ASP.NET Core you normally achieve this by checking whether a user has a given claim.
在考虑授权时,通常会考虑检查特定用户是否具有执行终端节点的权限。在 ASP.NET Core 中,通常通过检查用户是否具有给定的声明来实现此目的。

There’s an even more basic level of authorization we haven’t considered yet: allowing only authenticated users to execute an endpoint. This is even simpler than the claims scenario (which we’ll come to later), as there are only two possibilities:
还有一个更基本的授权级别我们还没有考虑:只允许经过身份验证的用户执行端点。这甚至比 claims 场景(我们稍后会介绍)还要简单,因为只有两种可能性:

• The user is authenticated—The action executes as normal.
用户已通过 AUTHENTICATED -作将正常执行。
• The user is unauthenticated—The user can’t execute the endpoint.
用户未经身份验证 - 用户无法执行端点。

You can achieve this basic level of authorization by using the [Authorize] attribute, which you saw in chapter 22 when we discussed authorization filters. You can apply this attribute to your actions and Razor Pages, as shown in the following listing, to restrict them to authenticated (logged-in) users only. If an unauthenticated user tries to execute an action or Razor Page protected with the [Authorize] attribute, they’ll be redirected to the login page.
您可以使用 [Authorize] 属性来实现此基本级别的授权,您在第 22 章讨论授权过滤器时看到了该属性。可以将此属性应用于作和 Razor Pages,如下面的清单所示,以将它们限制为仅经过身份验证(已登录)的用户。如果未经身份验证的用户尝试执行受 [Authorize] 属性保护的作或 Razor 页面,他们将被重定向到登录页面。

Listing 24.1 Applying [Authorize] to an action
清单 24.1 将 [Authorize] 应用于作

public class RecipeApiController : ControllerBase
{
    public IActionResult List() ❶
{
return Ok();
}
[Authorize] ❷
public IActionResult View() ❸
{
return Ok();
}
}

❶ This action can be executed by anyone, even when not logged in.
任何人都可以执行此作,即使未登录。
❷ Applies [Authorize] to individual actions, whole controllers, or Razor Pages
将 [授权] 应用于单个作、整个控制器或 Razor 页面
❸ This action can be executed only by authenticated users.
此作只能由经过身份验证的用户执行。

Applying the [Authorize] attribute to an endpoint attaches metadata to it, indicating that only authenticated users may access the endpoint. As you saw in figure 24.2, this metadata is made available to the AuthorizationMiddleware when an endpoint is selected by the RoutingMiddleware.
将 [Authorize] 属性应用于终端节点会将元数据附加到该终端节点,指示只有经过身份验证的用户才能访问该终端节点。如图 24.2 所示,当 RoutingMiddleware 选择端点时,此元数据可供 AuthorizationMiddleware 使用。

You can apply the [Authorize] attribute at the action scope, controller scope, Razor Page scope, or globally, as you saw in chapter 21. Any action or Razor Page that has the [Authorize] attribute applied in this way can be executed only by an authenticated user. Unauthenticated users will be redirected to the login page.
可以在作范围、控制器范围、Razor Page 范围或全局应用 [Authorize] 属性,如第 21 章所示。以这种方式应用了 [Authorize] 属性的任何作或 Razor 页面只能由经过身份验证的用户执行。未经身份验证的用户将被重定向到登录页面。

TIP There are several ways to apply the [Authorize] attribute globally. You can read about the options and when to choose which option on my blog: http://mng.bz/opQp.
提示:有几种方法可以全局应用 [Authorize] 属性。您可以在我的博客上阅读有关选项以及何时选择哪个选项的信息:http://mng.bz/opQp

Sometimes, especially when you apply the [Authorize] attribute globally, you might need to poke holes in this authorization requirement. If you apply the [Authorize] attribute globally, any unauthenticated requests are redirected to the login page for your app. But if the [Authorize] attribute is global, when the login page tries to load, you’ll be unauthenticated and redirected to the login page again. And now you’re stuck in an infinite redirect loop.
有时,尤其是在全局应用 [Authorize] 属性时,可能需要在此授权要求中戳漏洞。如果全局应用 [Authorize] 属性,则任何未经身份验证的请求都将重定向到应用的登录页面。但是,如果 [Authorize] 属性是全局属性,则当登录页尝试加载时,你将被取消身份验证并再次重定向到登录页。现在你陷入了一个无限的重定向循环中。

To get around this, you can direct specific endpoints to ignore the [Authorize] attribute by applying the [AllowAnonymous] attribute to an action or Razor Page, as shown in the next listing. This allows unauthenticated users to execute the action, so you can avoid the redirect loop that would otherwise result.
若要解决此问题,可以通过将 [AllowAnonymous] 属性应用于作或 Razor 页面,指示特定终结点忽略 [Authorize] 属性,如下一个列表所示。这允许未经身份验证的用户执行作,因此您可以避免否则会导致的重定向循环。

Listing 24.2 Applying [AllowAnonymous] to allow unauthenticated access
清单 24.2 应用 [AllowAnonymous] 以允许未经身份验证的访问

[Authorize] ❶
public class AccountController : ControllerBase
{
public IActionResult ManageAccount() ❷
{
return Ok();
}
[AllowAnonymous] ❸
public IActionResult Login() ❹
{
return Ok();
}
}

❶ Applied at the controller scope, so the user must be authenticated for all actions on the controller
在控制器范围内应用,因此用户必须对控制器上的所有作进行身份验证
❷ Only authenticated users may execute ManageAccount.
只有经过身份验证的用户才能执行 ManageAccount。
❸ [AllowAnonymous] overrides [Authorize] to allow unauthenticated users.
[AllowAnonymous] 覆盖 [Authorize] 以允许未经身份验证的用户。
❹ Login can be executed by anonymous users.
匿名用户可以执行登录。

WARNING If you apply the [Authorize] attribute globally, be sure to add the [AllowAnonymous] attribute to your login actions, error actions, password reset actions, and any other actions that you need unauthenticated users to execute. If you’re using the default Identity UI described in chapter 23, this is already configured for you.
警告:如果全局应用 [Authorize] 属性,请确保将 [AllowAnonymous] 属性添加到登录作、错误作、密码重置作以及需要未经身份验证的用户执行的任何其他作。如果您使用的是第 23 章中描述的默认身份 UI,则已为您配置了此 UI。

If an unauthenticated user attempts to execute an action protected by the [Authorize] attribute, traditional web apps redirect them to the login page. But what about APIs that don’t have a user interface? And what about more complex scenarios, where a user is logged in but doesn’t have the necessary claims to execute an action? In section we’ll look at how the ASP.NET Core authentication services handle all this for you.
如果未经身份验证的用户尝试执行受 [Authorize] 属性保护的作,则传统 Web 应用程序会将其重定向到登录页面。但是没有用户界面的 API 呢?对于更复杂的情况,即用户已登录但没有执行作所需的声明,该怎么办?在部分我们将了解 ASP.NET Core 身份验证服务如何为您处理所有这些。

24.2.2 Handling unauthorized requests‌

24.2.2 处理未经授权的请求

In the previous section you saw how to apply the [Authorize] attribute to an action to ensure that only authenticated users can execute it. In section 24.3 we’ll look at more complex examples that require you to also have a specific claim. In both cases, you must meet one or more authorization requirements (for example, you must be authenticated) to execute the action.‌
在上一节中,您了解了如何将 [Authorize] 属性应用于作,以确保只有经过身份验证的用户才能执行该作。在 Section 24.3 中,我们将查看更复杂的示例,这些示例要求您也有一个特定的声明。在这两种情况下,您都必须满足一个或多个授权要求(例如,您必须经过身份验证)才能执行作。

If the user meets the authorization requirements, the request passes unimpeded through the AuthorizationMiddleware, and the endpoint is executed in the EndpointMiddleware. If they don’t meet the requirements for the selected endpoint, the AuthorizationMiddleware will short-circuit the request. Depending on why the request failed authorization, the AuthorizationMiddleware generates one of two different types of responses, as shown in figure 24.3:
如果用户满足授权要求,则请求通过 AuthorizationMiddleware 畅通无阻,端点在 EndpointMiddleware 中执行。如果它们不满足所选终端节点的要求,则 AuthorizationMiddleware 将使请求短路。根据请求授权失败的原因, AuthorizationMiddleware 生成两种不同类型的响应之一,如图 24.3 所示:

• Challenge—This response indicates that the user was not authorized to execute the action because they weren’t yet logged in.
• 质询 - 此响应表示用户由于尚未登录而无权执行作。

• Forbid—This response indicates that the user was logged in but didn’t meet the requirements to execute the action. They didn’t have a required claim, for example.
• 禁止 - 此响应表示用户已登录,但不符合执行作的要求。例如,他们没有必需的索赔。

alt text

Figure 24.3 The three types of response to an authorization attempt. In the left example, the request contains an authentication cookie, so the user is authenticated in the AuthenticationMiddleware. The AuthorizationMiddleware confirms that the authenticated user can access the selected endpoint, so the endpoint is executed. In the center example, the request is not authenticated, so the Authorization- Middleware generates a challenge response. In the right example, the request is authenticated, but the user does not have permission to execute the endpoint, so a forbid response is generated.
图 24.3 对授权尝试的三种响应类型。在左侧示例中,请求包含一个身份验证 cookie,因此在 AuthenticationMiddleware 中对用户进行身份验证。AuthorizationMiddleware 确认经过身份验证的用户可以访问选定的终端节点,因此将执行终端节点。在中间的示例中,请求未经过身份验证,因此 Authorization- Middleware 生成质询响应。在正确的示例中,请求已经过身份验证,但用户没有执行终端节点的权限,因此会生成 forbid 响应。

NOTE If you apply the [Authorize] attribute in basic form, as you did in section 24.2.1, you will generate only challenge responses. In this case, a challenge response will be generated for unauthenticated users, but authenticated users will always be authorized.
注意:如果以基本形式应用 [Authorize] 属性,就像在第 24.2.1 节中所做的那样,您将仅生成质询响应。在这种情况下,将为未经身份验证的用户生成质询响应,但经过身份验证的用户将始终获得授权。

The exact HTTP response generated by a challenge or forbid response typically depends on the type of application you’re building and so the type of authentication your application uses: a traditional web application with Razor Pages, or an API application.
质询或禁止响应生成的确切 HTTP 响应通常取决于要构建的应用程序类型,因此应用程序使用的身份验证类型:具有 Razor Pages 的传统 Web 应用程序或 API 应用程序。

For traditional web apps using cookie authentication, such as when you use ASP.NET Core Identity, as in chapter 23, the challenge and forbid responses generate an HTTP redirect to a page in your application. A challenge response indicates the user isn’t yet authenticated, so they’re redirected to the login page for the app. After logging in, they can attempt to execute the protected resource again. A forbid response means the request was from a user that already logged in, but they’re still not allowed to execute the action.
对于使用 Cookie 身份验证的传统 Web 应用程序,例如当您使用 ASP.NET Core Identity 时(如第 23 章所示),challenge 和 forbid 响应会生成指向应用程序中页面的 HTTP 重定向。质询响应指示用户尚未通过身份验证,因此他们将被重定向到应用程序的登录页面。登录后,他们可以尝试再次执行受保护的资源。禁止响应表示请求来自已登录的用户,但仍不允许他们执行该作。

Consequently, the user is redirected to a “forbidden” or “access denied” web page, as shown in figure 24.4, which informs them they can’t execute the action or Razor Page.
因此,用户将被重定向到 “forbidden” 或 “access denied” 网页,如图 24.4 所示,该网页通知他们无法执行作或 Razor Page。

alt text

Figure 24.4 A forbid response in traditional web apps using cookie authentication. If you don’t have permission to execute a Razor Page and you’re already logged in, you’ll be redirected to an “access denied” page.
图 24.4 传统 Web 应用程序中使用 cookie 身份验证的 forbid 响应。如果您没有执行 Razor 页面的权限,并且您已经登录,您将被重定向到“拒绝访问”页面。

The preceding behavior is standard for traditional web apps, but API apps typically use a different approach to authentication, as you’ll see in chapter 25. Instead of logging in and using the API directly, you’d typically log in to a third- party application that provides a token to the client-sidesingle-page application (SPA) or mobile app. The client-side app sends this token when it makes a request to your API.
上述行为是传统 Web 应用程序的标准行为,但 API 应用程序通常使用不同的身份验证方法,如第 25 章所示。您通常不会直接登录并使用 API,而是登录到向客户端提供令牌的第三方应用程序单页应用程序 (SPA) 或移动应用程序。客户端应用程序在向 API 发出请求时发送此令牌。

Authenticating a request for an API app is essentially identical to a traditional web app that uses cookies, as you’ll see in chapter 25; AuthenticationMiddleware deserializes the credentials to create the ClaimsPrincipal. The difference is in how an API handles authorization failures.
对 API 应用程序的请求进行身份验证与使用 cookie 的传统 Web 应用程序基本相同,如第 25 章所示;AuthenticationMiddleware 反序列化凭据以创建 ClaimsPrincipal。区别在于 API 处理授权失败的方式。

When an API app generates a challenge response, it returns a 401 Unauthorized error response to the caller. Similarly, when the app generates a forbid response, it returns a 403 Forbidden response. The traditional web app essentially handled these errors by automatically redirecting unauthorized users to the login or “access denied” page, but the API app doesn’t do this. It’s up to the client-side SPA or mobile app to detect these errors and handle them as appropriate.
当 API 应用生成质询响应时,它会向调用方返回 401 Unauthorized 错误响应。同样,当应用程序生成 forbid 响应时,它会返回 403 Forbidden 响应。传统的 Web 应用程序基本上是通过自动将未经授权的用户重定向到登录或 “access denied” 页面来处理这些错误,但 API 应用程序不会这样做。客户端 SPA 或移动应用程序负责检测这些错误并根据需要处理它们。

TIP This difference in authorization behavior is one of the reasons I generally recommend creating separate apps for your APIs and Razor pages apps; it’s possible to have both in the same app, but the configuration is often more complex.
提示:授权行为的这种差异是我通常建议为您的 API 和 Razor 页面应用程序创建单独应用程序的原因之一;可以在同一个应用程序中同时拥有两者,但配置通常更复杂。

The different behavior between traditional web apps and SPAs can be confusing initially, but you generally don’t need to worry about that too much in practice. Whether you’re building an API app or a traditional MVC web app, the authorization code in your app looks the same in both cases.
传统 Web 应用程序和 SPA 之间的不同行为最初可能会令人困惑,但在实践中,您通常不需要太担心这一点。无论您是构建 API 应用程序还是传统的 MVC Web 应用程序,应用程序中的授权代码在这两种情况下看起来都相同。

Apply [Authorize] attributes to your endpoints, and let the framework take care of the differences for you.
将 [Authorize] 属性应用于您的终端节点,让框架为您处理差异。

NOTE In chapter 23 you saw how to configure ASP.NET Core Identity in a Razor Pages app. This chapter assumes that you’re building a Razor Pages app too, but the chapter is equally applicable if you’re building an API, as you’ll see in chapter 25. Authorization policies are applied in the same way, whichever style of app you’re building. Only the final response of unauthorized requests differs.
注意:在第 23 章中,你了解了如何在 Razor Pages 应用中配置 ASP.NET 核心标识。本章假定你也在构建 Razor Pages 应用,但如果你正在构建 API,则本章同样适用,如第 25 章所示。无论您正在构建哪种风格的应用程序,授权策略都以相同的方式应用。只有未授权请求的最终响应不同。

You’ve seen how to apply the most basic authorization requirement—restricting an endpoint to authenticated users —but most apps need something more subtle than this all-or- nothing approach. Consider the airport scenario from section 24.1. Being authenticated (having a passport) isn’t enough to get you through security. Instead, you also need a specific claim: BoardingPassNumber. In the next section we’ll look at how you can implement a similar requirement in ASP.NET Core.‌
您已经了解了如何应用最基本的授权要求,即将终端节点限制为经过身份验证的用户,但大多数应用程序需要比这种全有或全无方法更微妙的东西。考虑 24.1 节中的机场场景。通过身份验证(拥有护照)不足以让您通过安检。相反,您还需要一个特定的声明:BoardingPassNumber。在下一节中,我们将了解如何在 ASP.NET Core 中实现类似的要求。

24.3 Using policies for claims- based authorization‌

24.3 使用策略进行基于声明的授权

In the previous section, you saw how to require that users be logged in to access an endpoint. In this section you’ll see how to apply additional requirements. You’ll learn to use authorization policies to perform claims-based authorization to require that a logged-in user have the required claims to execute a given endpoint.
在上一节中,您了解了如何要求用户登录才能访问终端节点。在本节中,您将了解如何应用其他要求。您将学习如何使用授权策略来执行基于声明的授权,以要求登录用户具有执行给定终端节点所需的声明。

In chapter 23 you saw that authentication in ASP.NET Core centers on a ClaimsPrincipal object, which represents the user. This object has a collection of claims that contain pieces of information about the user, such as their name, email, and date of birth.
在第 23 章中,您看到 ASP.NET Core 中的身份验证以表示用户的 ClaimsPrincipal 对象为中心。此对象具有一组声明,其中包含有关用户的信息片段,例如其姓名、电子邮件和出生日期。

You can use this information to customize the app for each user, by displaying a welcome message addressing the user by name, for example, but you can also use claims for authorization. For example, you might authorize a user only if they have a specific claim (such as BoardingPassNumber) or if a claim has a specific value (FrequentFlyerClass claim with the value Gold).
您可以使用此信息为每个用户自定义应用程序,例如,通过显示按名称称呼用户的欢迎消息,但您也可以使用声明进行授权。例如,仅当用户具有特定声明(如 BoardingPassNumber)或声明具有特定值(值为 Gold 的 FrequentFlyerClass 声明)时,你才可以授权用户。

In ASP.NET Core the rules that define whether a user is authorized are encapsulated in a policy.
在 ASP.NET Core 中,定义用户是否获得授权的规则封装在策略中。

DEFINITION A policy defines the requirements you must meet for a request to be authorized.
定义:策略定义要授权请求必须满足的要求。

Policies can be applied to an endpoint using the [Authorize] attribute, similar to the way you saw in section 24.2.1. This listing shows a Razor Page PageModel that represents the first authorization step in the airport scenario. The AirportSecurity.cshtml Razor Page is protected by an [Authorize] attribute, but you’ve also provided a policy name, "CanEnterSecurity", as shown in the following listing.
可以使用 [Authorize] 属性将策略应用于终端节点,类似于您在第 24.2.1 节中看到的方式。此列表显示了一个 Razor Page PageModel,它表示 airport 场景中的第一个授权步骤。AirportSecurity.cshtml Razor 页面受 [Authorize] 属性保护,但你还提供了策略名称“CanEnterSecurity”,如以下列表所示。

Listing 24.3 Applying an authorization policy to a Razor Page
清单 24.3 将授权策略应用于 Razor 页面

[Authorize("CanEnterSecurity")] ❶
public class AirportSecurityModel : PageModel
{
public void OnGet() ❷
{
}
}

❶ Applying the “CanEnterSecurity” policy using [Authorize]
使用 [Authorize]应用“CanEnterSecurity”策略

❷ Only users that satisfy the “CanEnterSecurity” policy can execute the Razor Page.
只有满足“CanEnterSecurity”策略的用户才能执行 Razor 页面。

If a user attempts to execute the AirportSecurity.cshtml Razor Page, the authorization middleware verifies whether the user satisfies the policy’s requirements (we’ll look at the policy itself shortly). This gives one of three possible outcomes:
如果用户尝试执行 AirportSecurity.cshtml Razor 页面,授权中间件会验证用户是否满足策略的要求(我们稍后将介绍策略本身)。这将给出以下三种可能的结果之一:

• The user satisfies the policy—The middleware pipeline continues, and the EndpointMiddleware executes the Razor Page as normal.
用户满足策略 - 中间件管道继续,并且 EndpointMiddleware 照常执行 Razor Page。

• The user is unauthenticated—The user is redirected to the login page.
用户未通过 IF - 用户被重定向到登录页面。

• The user is authenticated but doesn’t satisfy the policy—The user is redirected to a “forbidden” or “access denied” page.
用户已通过身份验证,但不满足策略 - 用户被重定向到“禁止”或“拒绝访问”页面。

These three outcomes correlate with real-life outcomes you might expect when trying to pass through security at the airport:
这三种结果与您在尝试通过机场安检时可能预期的现实结果相关:

• You have a valid boarding pass—You can enter security as normal.
您持有有效的登机牌 - 您可以正常进入安检。

• You don’t have a boarding pass—You’re redirected to purchase a ticket.
您没有登机牌 - 您将被重定向到购买机票。

• Your boarding pass is invalid (you turned up a day late, for example)—You’re blocked from entering.
您的登机牌无效(例如,您迟到一天)- 您被阻止进入。

Listing 24.3 shows how you can apply a policy to a Razor Page using the [Authorize] attribute, but you still need to define the CanEnterSecurity policy.
清单 24.3 显示了如何使用 [Authorize] 属性将策略应用于 Razor 页面,但您仍然需要定义 CanEnterSecurity 策略。

You add policies to an ASP.NET Core application in Program.cs, as shown in listing 24.4. First, you add the authorization services and return an AuthorizationBuilder object using AddAuthorizationBuilder(). You can then add policies to the builder by calling AddPolicy(). You define the policy itself by calling methods in a lambda method on a AuthorizationPolicyBuilder (called policyBuilder here).
您可以在 Program.cs 中将策略添加到 ASP.NET Core 应用程序,如清单 24.4 所示。首先,添加授权服务并使用 AddAuthorizationBuilder() 返回 AuthorizationBuilder 对象。然后,您可以通过调用 AddPolicy() 将策略添加到生成器中。您可以通过在 AuthorizationPolicyBuilder(此处称为 policyBuilder)上调用 lambda 方法中的方法来定义策略本身。

Listing 24.4 Adding an authorization policy using AuthorizationPolicyBuilder
清单 24.4 使用 AuthorizationPolicyBuilder 添加授权策略

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorizationBuilder() ❶
.AddPolicy( ❷
"CanEnterSecurity", ❸
policyBuilder => policyBuilder ❹
.RequireClaim("BoardingPassNumber")); ❹
});
// Additional configuration

❶ Calls AddAuthorizationBuilder to add the required authorization services
调用 AddAuthorizationBuilder 以添加所需的授权服务
❷ Adds a new policy
添加新策略
❸ Provides a name for the policy
为策略提供名称
❹ Defines the policy requirements using AuthorizationPolicyBuilder
使用 AuthorizationPolicyBuilder 定义策略要求

When you call AddPolicy you provide a name for the policy, which should match the value you use in your [Authorize] attributes, and you define the requirements of the policy. In this example, you have a single simple requirement: the user must have a claim of type BoardingPassNumber. If a user has this claim, whatever its value, the policy is satisfied, and the user will be authorized.
调用 AddPolicy 时,您需要为策略提供一个名称,该名称应与您在 [Authorize] 属性中使用的值匹配,并定义策略的要求。在此示例中,您有一个简单的要求:用户必须具有 BoardingPassNumber 类型的声明。如果用户具有此声明,则无论其值如何,都满足策略,并且用户将获得授权。

NOTE A claim is information about the user, as a key-value pair. A policy defines the requirements for successful authorization. A policy may require that a user have a given claim, or it may specify more complex requirements, as you’ll see shortly.
注意:声明是有关用户的信息,以键值对的形式。策略定义成功授权的要求。策略可能要求用户具有给定的声明,或者它可能指定更复杂的要求,您很快就会看到。

AuthorizationPolicyBuilder contains several methods for creating simple policies like this, as shown in table 24.1. For example, an overload of the RequireClaim() method lets you specify a specific value that a claim must have. The following would let you create a policy where the "BoardingPassNumber" claim must have a value of "A1234":
AuthorizationPolicyBuilder包含几种用于创建此类简单策略的方法,如表 24.1 所示。例如,RequireClaim() 方法的重载允许您指定声明必须具有的特定值。下面将允许你创建一个策略,其中 “BoardingPassNumber” 声明的值必须为“A1234”:

policyBuilder => policyBuilder.RequireClaim("BoardingPassNumber", "A1234");

Table 24.1 Simple policy builder methods on AuthorizationPolicyBuilder
表 24.1 AuthorizationPolicyBuilder 上的简单策略生成器方法

Method Policy behavior
RequireAuthenticatedUser() The required user must be authenticated. Creates a policy similar to the default [Authorize] attribute, where you don’t set a policy.
RequireClaim(claim, values) The user must have the specified claim. If provided, the claim must be one of the specified values.
RequireUsername(username) The user must have the specified username.
RequireAssertion(function) Executes the provided lambda function, which returns a bool, indicating whether the policy was satisfied.

Role-based authorization vs. claims-based authorization
基于角色的授权与基于声明的授权

If you look at all of the methods available on the AuthorizationPolicyBuilder type using IntelliSense, you might notice that there’s a method I didn’t mention in table 24.1: RequireRole(). This is a remnant of the role-based approach to authorization used in previous versions of ASP.NET, and I don’t recommend using it.
如果您使用 IntelliSense 查看 AuthorizationPolicyBuilder 类型上的所有可用方法,您可能会注意到我在表 24.1 中没有提到的方法:RequireRole()。这是 ASP.NET 早期版本中使用的基于角色的授权方法的残余部分,我不建议使用它。

Before Microsoft adopted the claims-based authorization used by ASP.NET, role-based authorization was the norm. Users were assigned to one or more roles, such as Administrator or Manager, and authorization involved checking whether the current user was in the required role.‌
在 Microsoft 采用 ASP.NET 使用的基于声明的授权之前,基于角色的授权是常态。将用户分配给一个或多个角色,例如 Administrator 或 Manager,授权涉及检查当前用户是否位于所需的角色中。

This role-based approach to authorization is possible in ASP.NET Core, but it’s used primarily for legacy compatibility reasons. Claims-based authorization is the suggested approach. Unless you’re porting a legacy app that uses roles, I suggest that you embrace claims-based authorization and leave those roles behind.
这种基于角色的授权方法在 ASP.NET Core 中是可能的,但它主要用于旧版兼容性原因。基于声明的授权是建议的方法。除非您要移植使用角色的旧应用程序,否则我建议您采用基于声明的授权,并将这些角色抛在脑后。

Note that the fact that you’re using claims-based permissions doesn’t mean you need to get rid of roles entirely, but you should use roles as a basis for assigning claims to a user rather than authorize that a user belongs to one or more roles.
请注意,使用基于声明的权限这一事实并不意味着需要完全删除角色,但应使用角色作为将声明分配给用户的基础,而不是授权用户属于一个或多个角色。

You can use these methods to build simple policies that can handle basic situations, but often you’ll need something more complicated. What if you want to create a policy that enforces that only users over the age of 18 can execute an endpoint?
您可以使用这些方法来构建可以处理基本情况的简单策略,但通常需要更复杂的策略。如果要创建一个策略,强制要求只有 18 岁以上的用户才能执行终端节点,该怎么办?

The DateOfBirth claim provides the information you need, but there’s no single correct value, so you couldn’t use the RequireClaim() method. You could use the RequireAssertion() method and provide a function that calculates the age from the DateOfBirth claim, but that could get messy pretty quickly.
DateOfBirth 声明提供所需的信息,但没有单个正确的值,因此无法使用 RequireClaim() 方法。您可以使用 RequireAssertion() 方法并提供一个函数,该函数根据 DateOfBirth 声明计算年龄,但这很快就会变得混乱。

For more complex policies that can’t be easily defined using the RequireClaim() method, I recommend that you take a different approach and create a custom policy, as you’ll see in the following section.‌
对于无法使用 RequireClaim() 方法轻松定义的更复杂的策略,我建议您采用不同的方法并创建自定义策略,如下一节所示。

27.4 Creating custom policies for authorization‌

27.4 创建自定义授权策略

You’ve already seen how to create a policy by requiring a specific claim or requiring a specific claim with a specific value, but often the requirements will be more complex than that. In this section you’ll learn how to create custom authorization requirements and handlers. You’ll also see how to configure authorization requirements where there are multiple ways to satisfy a policy, any of which are valid.
您已经了解了如何通过要求特定声明或要求具有特定值的特定声明来创建策略,但要求通常比这更复杂。在本节中,您将了解如何创建自定义授权要求和处理程序。您还将了解如何配置授权要求,其中有多种方法可以满足策略,其中任何一种都是有效的。

Let’s return to the airport example. You’ve already configured the policy for passing through security, and now you’re going to configure the policy that controls whether you’re authorized to enter the airline lounge.
让我们回到机场的例子。您已经配置了通过安检的策略,现在您将配置控制您是否有权进入航空公司休息室的策略。

As you saw in figure 24.1, you’re allowed to enter the lounge if you have a FrequentFlyerClass claim with a value of Gold. If this was the only requirement, you could use AuthorizationPolicyBuilder to create a policy like this:
如图 24.1 所示,如果您有价值为 Gold 的 FrequentFlyerClass 索赔,则可以进入休息室。如果这是唯一的要求,则可以使用 AuthorizationPolicyBuilder 创建如下所示的策略:

options.AddPolicy("CanAccessLounge", policyBuilder => policyBuilder.RequireClaim("FrequentFlyerClass", "Gold"));

But what if the requirements are more complicated? For example, suppose you can enter the lounge if you’re at least 18 years old (as calculated from the DateOfBirth claim) and you’re one of the following:
但是,如果要求更复杂呢?例如,假设您至少年满 18 岁(根据 DateOfBirth 声明计算)并且您是以下之一,则可以进入休息室:

• You’re a Gold-class frequent flyer (have a FrequentFlyerClass claim with value "Gold")
您是金卡级常旅客(有价值为“金卡”的 FrequentFlyerClass 索赔)
• You’re an employee of the airline (have an EmployeeNumber claim).
您是航空公司的员工(有 EmployeeNumber 索赔)。

If you’ve ever been banned from the lounge (you have an IsBannedFromLounge claim), you won’t be allowed in, even if you satisfy the other requirements.
如果您曾经被禁止进入休息室(您有 IsBannedFromLounge 索赔),即使您满足其他要求,也不会被允许进入。

There’s no way of achieving this complex set of requirements with the basic use of AuthorizationPolicyBuilder you’ve seen so far. Luckily, these methods are a wrapper around a set of building blocks that you can combine to achieve the desired policy.
到目前为止,您所看到的 AuthorizationPolicyBuilder 的基本用法无法实现这组复杂的要求。幸运的是,这些方法是一组构建块的包装器,您可以组合这些构建块来实现所需的策略。

24.4.1 Requirements and handlers: The building blocks of a policy‌

24.4.1 要求和处理程序:策略的构建块

Every policy in ASP.NET Core consists of one or more requirements, and every requirement can have one or more handlers. For the airport lounge example, you have a single policy ("CanAccessLounge"), two requirements (MinimumAgeRequirement and AllowedInLoungeRequirement), and several handlers, as shown in figure 24.5.
ASP.NET Core 中的每个策略都包含一个或多个要求,每个要求可以有一个或多个处理程序。对于机场休息室示例,您有一个策略(“CanAccessLounge”)、两个要求(MinimumAgeRequirement 和 AllowedInLoungeRequirement)和多个处理程序,如图 24.5 所示。

alt text

Figure 24.5 A policy can have many requirements, and every requirement can have many handlers. By combining multiple requirements in a policy and providing multiple handler implementations, you can create complex authorization policies that meet any of your business requirements.
图 24.5 一个策略可以有很多需求,每个需求可以有很多处理程序。通过在策略中组合多个要求并提供多个处理程序实施,您可以创建满足任何业务需求的复杂授权策略。

For a policy to be satisfied, a user must fulfill all the requirements. If the user fails any of the requirements, the authorize middleware won’t allow the protected endpoint to be executed. In this example, a user must be allowed to access the lounge and must be over 18 years old.
要满足策略,用户必须满足所有要求。如果用户不符合任何要求,则 authorize 中间件将不允许执行受保护的终端节点。在此示例中,必须允许用户访问休息室,并且必须年满 18 岁。

Each requirement can have one or more handlers, which will confirm that the requirement has been satisfied. For example, as shown in figure 24.5, AllowedInLoungeRequirement has two handlers that can satisfy the requirement:
每个要求都可以有一个或多个处理程序,这些处理程序将确认已满足要求。例如,如图 24.5 所示,AllowedInLoungeRequirement 有两个可以满足要求的处理程序:

• FrequentFlyerHandler
• IsAirlineEmployeeHandler

If the user satisfies either of these handlers, AllowedInLoungeRequirement is satisfied. You don’t need all handlers for a requirement to be satisfied; you need only one.
如果用户满足其中任一处理程序,则满足 AllowedInLoungeRequirement。您不需要满足所有处理程序即可满足需求;你只需要一个。

NOTE Figure 24.5 shows a third handler,BannedFromLoungeHandler, which I’ll cover in section 24.4.2. It’s slightly different in that it can fail a requirement but not satisfy it.
注意:图 24.5 显示了第三个处理程序 BannedFromLoungeHandler,我将在 24.4.2 节中介绍。它略有不同,因为它可能不符合要求,但不能满足它。

You can use requirements and handlers to achieve most any combination of behavior you need for a policy. By combining handlers for a requirement, you can validate conditions using a logical OR: if any of the handlers is satisfied, the requirement is satisfied. By combining requirements, you create a logical AND: all the requirements must be satisfied for the policy to be satisfied, as shown in figure 24.6.‌
您可以使用要求和处理程序来实现策略所需的大多数行为组合。通过组合需求的处理程序,您可以使用逻辑 OR 验证条件:如果满足任何处理程序,则满足需求。通过组合需求,您可以创建一个逻辑 AND:必须满足所有要求才能满足策略,如图 24.6 所示。

alt text

Figure 24.6 For a policy to be satisfied, every requirement must be satisfied. A requirement is satisfied if any of the handlers is satisfied.
图 24.6 要满足策略,必须满足所有要求。如果满足任何处理程序,则满足要求。

TIP You can add multiple policies to a Razor Page or action method by applying the [Authorize] attribute multiple times, as in [Authorize ("Policy1"), Authorize("Policy2")]. All policies must be satisfied for the request to be authorized.
提示:可以通过多次应用 [Authorize] 属性,将多个策略添加到 Razor 页面或作方法,如 [Authorize (“Policy1”), Authorize(“Policy2”)] 中所示。必须满足所有策略,请求才能获得授权。

I’ve highlighted requirements and handlers that will make up your "CanAccessLounge" policy, so in the next section you’ll build each of the components and apply them to the airport sample app.
我已经重点介绍了构成“CanAccessLounge”策略的要求和处理程序,因此在下一节中,您将构建每个组件并将它们应用于 airport 示例应用程序。

24.4.2 Creating a policy with a custom requirement and handler‌

24.4.2 创建具有自定义要求和处理程序的策略

You’ve seen all the pieces that make up a custom authorization policy, so in this section we’ll explore the implementation of the "CanAccessLounge" policy.
您已经了解了构成自定义授权策略的所有部分,因此在本节中,我们将探讨 “CanAccessLounge” 策略的实现。

CREATING AN IAUTHORIZATIONREQUIREMENT TO REPRESENT A REQUIREMENT
创建 IAUTHORIZATIONREQUIREMENT 以表示要求

As you’ve seen, a custom policy can have multiple requirements, but what is a requirement in code terms? Authorization requirements in ASP.NET Core are any class that implements the IAuthorizationRequirement interface. This is a blank marker interface, which you can apply to any class to indicate that it represents a requirement.
如您所见,自定义策略可以有多个要求,但代码术语中的要求是什么?ASP.NET Core 中的授权要求是实现 IAuthorizationRequirement 接口的任何类。这是一个空白标记接口,您可以将其应用于任何类,以指示它表示要求。

If the interface doesn’t have any members, you might be wondering what the requirement class needs to look like. Typically, they’re simple plain old CLR object (POCO) classes. The following listing shows AllowedInLoungeRequirement, which is about as simple as a requirement can get. It has no properties or methods; it implements the required IAuthorizationRequirement interface.
如果接口没有任何成员,您可能想知道 requirement 类需要是什么样子。通常,它们是简单的普通旧 CLR 对象 (POCO) 类。下面的清单显示了 AllowedInLoungeRequirement,这与要求所能获得的最低要求差不多。它没有属性或方法;它实现所需的 IAuthorizationRequirement 接口。

Listing 24.5 AllowedInLoungeRequirement

public class AllowedInLoungeRequirement
    : IAuthorizationRequirement { } ❶

❶ The interface identifies the class as an authorization requirement.
接口将类标识为授权要求。

This is the simplest form of requirement, but it’s also common to have one or two properties that make the requirement more generalized. For example, instead of creating the highly specific MustBe18YearsOldRequirement, you could create a parameterized MinimumAgeRequirement, as shown in the following listing. By providing the minimum age as a parameter to the requirement, you can reuse the requirement for other policies with different minimum-age requirements.
这是最简单的要求形式,但通常具有一两个使要求更通用的属性。例如,您可以创建参数化的 MinimumAgeRequirement,而不是创建高度具体的 MustBe18YearsOldRequirement,如下面的清单所示。通过提供最低年龄作为要求的参数,您可以将该要求重新用于具有不同最低年龄要求的其他保单。

Listing 24.6 The parameterized MinimumAgeRequirement
清单 24.6 参数化的 MinimumAgeRequirement

public class MinimumAgeRequirement : IAuthorizationRequirement ❶
{
public MinimumAgeRequirement(int minimumAge) ❷
{
MinimumAge = minimumAge;
}
public int MinimumAge { get; } ❸
}

❶ The interface identifies the class as an authorization requirement.
接口将类标识为授权要求。

❷ The minimum age is provided when the requirement is created.
创建要求时提供最低年龄。

❸ Handlers can use the exposed minimum age to determine whether the requirement is satisfied.
处理程序可以使用公开的最小年龄来确定是否满足要求。

The requirements are the easy part. They represent each of the components of the policy that must be satisfied for the policy to be satisfied overall. Note that requirements are meant to be lightweight objects that can be created “manually.” So while you can have constructor parameters, as shown in listing 24.6, you can’t use dependency injection (DI) here. That’s not as limiting as it sounds, because your handlers can use DI.
要求是最简单的部分。它们表示策略的每个组成部分,必须满足这些组成部分才能使策略总体上得到满足。请注意,需求是可以 “手动” 创建的轻量级对象。因此,虽然你可以有构造函数参数,如清单 24.6 所示,但你不能在这里使用依赖注入 (DI)。这并不像听起来那么限制,因为你的处理程序可以使用 DI。

CREATING A POLICY WITH MULTIPLE REQUIREMENTS
创建具有多个要求的策略

You’ve created the two requirements, so now you can configure the "CanAccessLounge" policy to use them. You configure your policies as you did before, in Program.cs.
您已经创建了这两个要求,因此现在可以配置 “CanAccessLounge” 策略来使用它们。您可以像以前一样在 Program.cs 中配置策略。

Listing 24.7 shows how to do this by creating an instance of each requirement and passing them to AuthorizationPolicyBuilder. The authorization handlers use these requirement objects when attempting to authorize the policy.
清单 24.7 展示了如何通过创建每个需求的实例并将它们传递给AuthorizationPolicyBuilder来做到这一点。授权处理程序在尝试授权策略时使用这些要求对象。

Listing 24.7 Creating an authorization policy with multiple requirements
清单 24.7 创建具有多个需求的授权策略

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.services.AddAuthorization(options =>
{ ❶
options.AddPolicy( ❶
"CanEnterSecurity", ❶
policyBuilder => policyBuilder ❶
.RequireClaim(Claims.BoardingPassNumber)); ❶
options.AddPolicy( ❷
"CanAccessLounge", ❷
policyBuilder => policyBuilder.AddRequirements( ❸
new MinimumAgeRequirement(18), ❸
new AllowedInLoungeRequirement() ❸
));
});
// Additional configuration

❶ Adds the previous simple policy for passing through security
新增之前简单的通过安检策略

❷ Adds a new policy for the airport lounge, called CanAccessLounge
为机场贵宾室添加一项名为 CanAccessLounge 的新政策
❸ Adds an instance of each IAuthorizationRequirement object
添加每个 IAuthorizationRequirement 对象的实例

You now have a policy called "CanAccessLounge" with two requirements, so you can apply it to a Razor Page or action method using the [Authorize] attribute, in exactly the same way you did for the "CanEnterSecurity" policy:
现在,您有一个名为“CanAccessLounge”的策略,其中包含两个要求,因此您可以使用 [Authorize] 属性将其应用于 Razor 页面或作方法,其方式与对“CanEnterSecurity”策略执行的作完全相同:

[Authorize("CanAccessLounge")]
public class AirportLoungeModel : PageModel
{
public void OnGet() { }
}

When a request is routed to the AirportLounge.cshtml Razor Page, the authorize middleware executes the authorization policy and each of the requirements is inspected. But you saw earlier that the requirements are purely data; they indicate what needs to be fulfilled, but they don’t describe how that has to happen. For that, you need to write some handlers.
将请求路由到 AirportLounge.cshtml Razor 页面时,authorize 中间件将执行授权策略并检查每个要求。但您之前看到,这些要求纯粹是数据;它们指出了需要满足什么,但没有描述必须如何实现。为此,您需要编写一些处理程序。

CREATING AUTHORIZATION HANDLERS TO SATISFY YOUR REQUIREMENTS
创建授权处理程序以满足您的要求

Authorization handlers contain the logic of how a specific IAuthorizationRequirement can be satisfied. When executed, a handler can do one of three things:
授权处理程序包含如何满足特定 IAuthorizationRequirement 的逻辑。执行时,处理程序可以执行以下三项作之一:

• Mark the requirement handling as a success.
将需求处理标记为成功。

• Do nothing.
什么都不做。

• Explicitly fail the requirement.
明确不符合要求。

Handlers should implement AuthorizationHandler, where T is the type of requirement they handle. For example, the following listing shows a handler for AllowedInLoungeRequirement that checks whether the user has a claim called FrequentFlyerClass with a value of Gold.
处理程序应实现 AuthorizationHandler,其中 T 是它们处理的需求类型。例如,下面的清单显示了 AllowedInLoungeRequirement 的处理程序,该处理程序检查用户是否具有名为 FrequentFlyerClass 且值为 Gold 的声明。

Listing 24.8 FrequentFlyerHandler for AllowedInLoungeRequirement
清单 24.8 AllowedInLoungeRequirement 的 FrequentFlyerHandler

public class FrequentFlyerHandler :
AuthorizationHandler<AllowedInLoungeRequirement> ❶
{
protected override Task HandleRequirementAsync( ❷
AuthorizationHandlerContext context, ❸
AllowedInLoungeRequirement requirement) ❹
{
if(context.User.HasClaim("FrequentFlyerClass", "Gold")) ❺
{
context.Succeed(requirement); ❻
}
return Task.CompletedTask; ❼
}
}

❶ The handler implements AuthorizationHandler<T>.
处理程序实现 AuthorizationHandler<T>

❷ You must override the abstract HandleRequirementAsync method.
您必须重写抽象 HandleRequirementAsync 方法。

❸ The context contains details such as the ClaimsPrincipal user object.
上下文包含诸如 ClaimsPrincipal 用户对象之类的详细信息。

❹ The requirement instance to handle
要处理的要求实例

❺ Checks whether the user has the Frequent-FlyerClass claim with the Gold value
检查用户是否具有值为 Gold 的Frequent-FlyerClass 声明

❻ If the user had the necessary claim, marks the requirement as satisfied by calling Succeed
如果用户具有必要的声明,则通过调用 Succeed将要求标记为满足

❼ If the requirement wasn’t satisfied, does nothing
如果未满足要求,则不执行任何作

This handler is functionally equivalent to the simple RequireClaim() handler you saw at the start of section 24.4, but using the requirement and handler approach instead.
这个处理程序在功能上等同于你在 24.4 节开头看到的简单RequireClaim()处理程序,但使用的是需求和处理程序方法。

When a request is routed to the AirportLounge.cshtml Razor Page, the authorization middleware sees the [Authorize] attribute on the endpoint with the "CanAccessLounge" policy. It loops through all the requirements in the policy and all the handlers for each requirement, calling the HandleRequirementAsync method for each.‌
当请求路由到 AirportLounge.cshtml Razor 页面时,授权中间件会在具有“CanAccessLounge”策略的终结点上看到 [Authorize] 属性。它循环访问策略中的所有要求和每个要求的所有处理程序,并为每个要求调用 HandleRequirementAsync 方法。

The authorization middleware passes the current AuthorizationHandlerContext and the requirement to be checked to each handler. The current ClaimsPrincipal being authorized is exposed on the context as the User property. In listing 24.8, FrequentFlyerHandler uses the context to check for a claim called FrequentFlyerClass with the Gold value, and if it exists, indicates that the user is allowed to enter the airline lounge by calling Succeed().
授权中间件将当前 AuthorizationHandlerContext 和要检查的要求传递给每个处理程序。当前被授权的 ClaimsPrincipal 在上下文中作为 User 属性公开。在列表 24.8 中,FrequentFlyerHandler 使用上下文来检查名为 FrequentFlyerClass 且值为 Gold 的声明,如果存在,则表示允许用户通过调用 Succeed() 进入航空公司休息室。

NOTE Handlers mark a requirement as being satisfied by calling context .Succeed() and passing the requirement as an argument.
注意处理程序通过调用 context 将需求标记为满足。Succeed() 并将需求作为参数传递。

It’s important to note the behavior when the user doesn’t have the claim. FrequentFlyerHandler doesn’t do anything in this case; it returns a completed Task to satisfy the method signature.
请务必注意用户没有声明时的行为。在这种情况下,FrequentFlyerHandler 不执行任何作;它返回一个已完成的 Task 以满足方法签名。

NOTE Remember that if any of the handlers associated with a requirement passes, the requirement is a success. Only one of the handlers must succeed for the requirement to be satisfied.
注意:请记住,如果与要求关联的任何处理程序通过,则要求成功。只有一个处理程序必须成功才能满足要求。

This behavior, whereby you either call context.Succeed() or do nothing, is typical for authorization handlers. The following listing shows the implementation of IsAirlineEmployeeHandler, which uses a similar claim check to determine whether the requirement is satisfied.
此行为,即调用 context.Succeed() 或不执行任何作,是授权处理程序的典型特征。下面的清单显示了 IsAirlineEmployeeHandler 的实现,它使用类似的声明检查来确定是否满足要求。

Listing 24.9 IsAirlineEmployeeHandler

public class IsAirlineEmployeeHandler :
AuthorizationHandler<AllowedInLoungeRequirement> ❶
{
protected override Task HandleRequirementAsync( ❷
AuthorizationHandlerContext context, ❷
AllowedInLoungeRequirement requirement) ❷
{
if(context.User.HasClaim(c => c.Type == "EmployeeNumber")) ❸
{
context.Succeed(requirement); ❹
}
return Task.CompletedTask; ❺
}
}

❶ The handler implements AuthorizationHandler<T>.
处理程序实现 AuthorizationHandler<T>

❷ You must override the abstract HandleRequirementAsync method.
您必须覆盖抽象的 HandleRequirementAsync 方法。

❸ Checks whether the user has the EmployeeNumber claim
检查用户是否具有 EmployeeNumber 声明

❹ If the user has the necessary claim, marks the requirement as satisfied by calling Succeed
如果用户具有必要的声明,则通过调用 Succeed 将需求标记为满足

❺ If the requirement wasn’t satisfied, does nothing
如果不满足要求,则不执行任何作

I’ve left the implementation of MinimumAgeHandler for MinimumAgeRequirement as an exercise for the reader, as it’s similar to the handlers you have already seen. You can find an example implementation in the code samples for the chapter.
我将 MinimumAgeRequirement 的 MinimumAgeHandler 的实现留给读者作为练习,因为它类似于您已经看到的处理程序。您可以在本章的代码示例中找到示例实现。

TIP It’s possible to write generic handlers that can be used with multiple requirements, but I suggest sticking to handling a single requirement. If you need to extract some common functionality, move it to an external service, and call that from both handlers.
提示:可以编写可用于多个需求的通用处理程序,但我建议坚持处理单个需求。如果需要提取一些常用功能,请将其移动到外部服务,然后从两个处理程序中调用它。

This pattern of authorization handler is common, but in some cases, instead of checking for a success condition, you might want to check for a failure condition. In the airport example, you don’t want to authorize someone who was previously banned from the lounge, even if they would otherwise be allowed to enter.
这种授权处理程序模式很常见,但在某些情况下,您可能希望检查失败条件,而不是检查成功条件。在 airport 示例中,您不希望授权之前被禁止进入贵宾室的人,即使他们本来可以进入。

You can handle this scenario by using the context.Fail() method exposed on the context, as shown in the following listing. Calling Fail() in a handler always causes the requirement, and hence the whole policy, to fail. You should use it only when you want to guarantee failure, even if other handlers indicate success.
您可以使用上下文处理此方案。Fail() 方法,如下面的清单所示。在处理程序中调用 Fail() 总是会导致需求失败,从而导致整个策略失败。仅当您希望保证失败时,才应使用它,即使其他处理程序指示成功。

Listing 24.10 Calling context.Fail() in a handler to fail the requirement
清单 24.10 调用context.Fail()使需求失败

public class BannedFromLoungeHandler :
AuthorizationHandler<AllowedInLoungeRequirement> ❶
{
protected override Task HandleRequirementAsync( ❷
AuthorizationHandlerContext context, ❷
AllowedInLoungeRequirement requirement) ❷
{
if(context.User.HasClaim(c => c.Type == "IsBannedFromLounge")) ❸
{
context.Fail(); ❹
}
return Task.CompletedTask; ❺
}
}

❶ The handler implements AuthorizationHandler<T>.
处理程序实现 AuthorizationHandler<T>

❷ You must override the abstract HandleRequirementAsync method.
您必须重写抽象的 HandleRequirementAsync 方法。

❸ Checks whether the user has the IsBannedFromLounge claim
检查用户是否具有 IsBannedFromLounge 声明

❹ If the user has the claim, fails the requirement by calling Fail. The whole policy fails.
如果用户具有声明,则通过调用 Fail 来满足要求。整个策略失败了。

❺ If the claim wasn’t found, does nothing
如果未找到索赔,则不执行任何作

In most cases, your handlers will either call Succeed() or will do nothing, but the Fail() method is useful when you need a kill switch to guarantee that a requirement won’t be satisfied.
在大多数情况下,处理程序将调用 Succeed() 或不执行任何作,但当你需要一个终止开关来保证不会满足要求时,Fail() 方法非常有用。

NOTE Whether a handler calls Succeed(), Fail(), or neither, the authorization system always executes all the handlers for a requirement and all the requirements for a policy, so you can be sure your handlers will always be called.
注意:无论处理程序是调用 Succeed()、Fail(),还是两者都不调用,授权系统始终执行要求的所有处理程序和策略的所有要求,因此您可以确保始终调用处理程序。

The final step to complete your authorization implementation for the app is to register the authorization handlers with the DI container, as shown in the following listing.
完成应用程序的授权实现的最后一步是向 DI 容器注册授权处理程序,如下面的清单所示。

Listing 24.11 Registering the authorization handlers with the DI container
Listing 24.11 向 DI 容器注册授权处理程序

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(options =>
options.AddPolicy(
"CanEnterSecurity",
policyBuilder => policyBuilder
.RequireClaim(Claims.BoardingPassNumber));
options.AddPolicy(
"CanAccessLounge",
policyBuilder => policyBuilder.AddRequirements(
new MinimumAgeRequirement(18),
new AllowedInLoungeRequirement()
));
});
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
services.AddSingleton<IAuthorizationHandler, FrequentFlyerHandler>();
services.AddSingleton<IAuthorizationHandler, BannedFromLoungeHandler>();
services.AddSingleton<IAuthorizationHandler, IsAirlineEmployeeHandler>();
// Additional configuration

For this app, the handlers don’t have any constructor dependencies, so I’ve registered them as singletons with the container. If your handlers have scoped or transient dependencies (the EF Core DbContext, for example), you might want to register them as scoped instead, as appropriate.
对于此应用程序,处理程序没有任何构造函数依赖项,因此我已将它们注册为容器中的单例。如果处理程序具有范围或暂时性依赖项(例如 EF Core DbContext),则可能需要根据需要将它们注册为范围依赖项。

NOTE Services are registered with a lifetime of transient, scoped, or singleton, as discussed in chapter 9.
注意:服务注册的生命周期为 transient、scoped 或 singleton,如第 9 章所述。

You can combine the concepts of policies, requirements, and handlers in many ways to achieve your goals for authorization in your application. The example in this section, although contrived, demonstrates the components you need to apply authorization declaratively at the action method or Razor Page level by creating policies and applying the [Authorize] attribute as appropriate.
您可以通过多种方式组合策略、要求和处理程序的概念,以实现应用程序中的授权目标。本部分中的示例虽然是人为的,但演示了通过创建策略并根据需要应用 [Authorize] 属性,在作方法或 Razor 页面级别以声明方式应用授权所需的组件。

As well as applying the [Authorize] attribute explicitly to actions and Razor Pages, you can configure it globally, so that a policy is applied to every endpoint in your application. Additionally, for Razor Pages you can apply different authorization policies to different folders. You can read more about applying authorization policies using conventions in Microsoft’s “Razor Pages authorization conventions in ASP.NET Core” documentation: http://mng.bz/nMm2.
除了将 [Authorize] 属性显式应用于作和 Razor Pages 之外,还可以全局配置它,以便将策略应用于应用程序中的每个终结点。此外,对于 Razor Pages,可以将不同的授权策略应用于不同的文件夹。您可以在 Microsoft 的“ASP.NET Core 中的 Razor Pages 授权约定”文档中阅读有关使用约定应用授权策略的更多信息:http://mng.bz/nMm2

There’s one area, however, where the [Authorize] attribute falls short: resource-based authorization. The [Authorize] attribute attaches metadata to an endpoint, so the authorization middleware can authorize the user before an endpoint is executed. But what if you need to authorize the action from within the endpoint?
但是,[Authorize] 属性在一个方面存在不足:基于资源的授权。[Authorize] 属性将元数据附加到终结点,以便授权中间件可以在执行终结点之前对用户进行授权。但是,如果您需要从终端节点内部授权作,该怎么办?

This is common when you’re applying authorization at the document or resource level. If users are allowed to edit only documents they created, you need to load the document before you can tell whether they’re allowed to edit it! This isn’t easy with the declarative [Authorize] attribute approach, so you must often use an alternative, imperative approach. In the next section you’ll see how to apply this resource-based authorization in a Razor Page handler.‌
当您在文档或资源级别应用授权时,这种情况很常见。如果仅允许用户编辑他们创建的文档,则需要先加载文档,然后才能判断是否允许他们编辑该文档!使用声明性 [Authorize] 属性方法,这并不容易,因此您必须经常使用替代的命令式方法。在下一部分中,你将了解如何在 Razor Page 处理程序中应用此基于资源的授权。

24.5 Controlling access with resource-based authorization‌

24.5 使用基于资源的授权控制访问

In this section you’ll learn about resource-based authorization. This is used when you need to know details about the resource being protected to determine whether a user is authorized. You’ll learn how to apply authorization policies manually using the IAuthorizationService and how to create resource-based AuthorizationHandlers.
在本节中,您将了解基于资源的授权。当您需要了解有关受保护资源的详细信息以确定用户是否获得授权时,可以使用此方法。您将了解如何使用 IAuthorizationService 手动应用授权策略,以及如何创建基于资源的 AuthorizationHandlers。

Resource-based authorization is a common problem for applications, especially when you have users who can create or edit some sort of document. Consider the recipe application you worked on in chapter 23. This app lets users create, view, and edit recipes.
基于资源的授权是应用程序的常见问题,尤其是当您拥有可以创建或编辑某种文档的用户时。考虑您在第 23 章中处理的配方应用程序。此应用程序允许用户创建、查看和编辑配方。

Up to this point, everyone can create new recipes, and anyone can edit any recipe, even if they haven’t logged in. Now you want to add some additional behavior:
到目前为止,每个人都可以创建新配方,任何人都可以编辑任何配方,即使他们尚未登录。现在,您需要添加一些其他行为:

• Only authenticated users should be able to create new recipes.
只有经过身份验证的用户才能创建新配方。

• You can edit only the recipes you created.
您只能编辑您创建的配方。

You’ve already seen how to achieve the first of these requirements: decorate the Create .cshtml Razor Page with an [Authorize] attribute and don’t specify a policy, as shown in the following listing. This will force the user to authenticate before they can create a new recipe.
你已了解如何实现这些要求中的第一个:使用 [Authorize] 属性装饰 Create .cshtml Razor 页面,并且不指定策略,如下面的列表所示。这将强制用户在创建新配方之前进行身份验证。

Listing 24.12 Adding AuthorizeAttribute to the Create.cshtml Razor Page
列表 24.12 将 AuthorizeAttribute 添加到 Create.cshtml Razor 页面

[Authorize] ❶
public class CreateModel : PageModel
{[BindProperty]
public CreateRecipeCommand Input { get; set; }
public void OnGet() ❷
{ ❷
Input = new CreateRecipeCommand(); ❷
} ❷
public async Task<IActionResult> OnPost() ❷
{ ❷
// Method body not shown for brevity ❷
} ❷
}

❶ Users must be authenticated to execute the Create.cshtml Razor Page.
用户必须经过身份验证才能执行 Create.cshtml Razor 页面。
❷ All page handlers are protected. You can apply [Authorize] only to the PageModel, not handlers.
所有页面处理程序都受到保护。只能将 [Authorize] 应用于 PageModel,而不能应用于处理程序。

TIP As with all filters, you can apply the [Authorize] attribute only to the Razor Page, not to individual page handlers. The attribute applies to all page handlers in the Razor Page.
提示:与所有筛选器一样,只能将 [Authorize] 属性应用于 Razor 页面,而不能应用于单个页面处理程序。该属性适用于 Razor 页面中的所有页面处理程序。

Adding the [Authorize] attribute fulfills your first requirement, but unfortunately, with the techniques you’ve seen so far, you have no way to fulfill the second. You could apply a policy that either permits or denies a user the ability to edit all recipes, but there’s currently no easy way to restrict this so that a user can only edit their own recipes.
添加 [Authorize] 属性可以满足第一个要求,但遗憾的是,使用你目前看到的技术,无法满足第二个要求。您可以应用一个策略来允许或拒绝用户编辑所有配方,但目前没有简单的方法来限制这一点,以便用户只能编辑自己的配方。

To find out who created the Recipe, you must first load it from the database. Only then can you attempt to authorize the user, taking the specific recipe (resource) into account. The following listing shows a partially implemented page handler for how this might look, where authorization occurs partway through the method, after the Recipe object has been loaded.
要找出 Recipe 的创建者,您必须先从数据库中加载它。只有这样,您才能尝试授权用户,同时考虑特定的配方(资源)。下面的清单显示了一个部分实现的页面处理程序,其中授权发生在方法的中途,在 Recipe 对象加载之后。

Listing 24.13 The Edit.cshtml page must load the Recipe
清单 24.13 Edit.cshtml 页面必须加载 Recipe

public IActionResult OnGet(int id) ❶
{
var recipe = _service.GetRecipe(id); ❷
var createdById = recipe.CreatedById; ❷
// Authorize user based on createdById ❸
if(isAuthorized) ❹
{ ❹
return View(recipe); ❹
} ❹
}

❶ The id of the recipe to edit is provided by model binding.
要编辑的配方的 id 由模型绑定提供。
❷ You must load the Recipe from the database before you know who created it.
您必须先从数据库中加载 Recipe,然后才能知道谁创建了它。
❸ You must authorize the current user to verify that they’re allowed to edit this specific Recipe.
您必须授权当前用户验证是否允许他们编辑此特定配方。
❹ The action method can continue only if the user was authorized.
只有在用户获得授权的情况下,作方法才能继续。

You need access to the resource (in this case, the Recipe entity) to perform the authorization, so the declarative [Authorize] attribute can’t help you. In section 24.5.1 you’ll see the approach you need to take to handle these situations and to apply authorization inside your endpoints.
您需要访问资源(在本例中为 Recipe 实体)才能执行授权,因此声明性 [Authorize] 属性无法为您提供帮助。在 Section 24.5.1 中,您将看到处理这些情况并在 endpoints 内应用授权所需采用的方法。

WARNING Be careful when exposing the integer ID of your entities in the URL, as in listing 24.13. Users will be able to edit every entity by modifying the ID in the URL to access a different entity. Be sure to apply authorization checks, or you could expose a security vulnerability called insecure direct object reference (IDOR). You can read more about IDOR at http://mng.bz/QPnG.
警告:在 URL 中公开实体的整数 ID 时要小心,如清单 24.13 所示。用户将能够通过修改 URL 中的 ID 来编辑每个实体,以访问不同的实体。请务必应用授权检查,否则可能会暴露称为不安全直接对象引用 (IDOR) 的安全漏洞。您可以在 http://mng.bz/QPnG 上阅读有关 IDOR 的更多信息。

24.5.1 Manually authorizing requests with IAuthorizationService‌

24.5.1 使用 IAuthorizationService 手动授权请求

All of the approaches to authorization so far have been declarative. You apply the [Authorize] attribute, with or without a policy name, and you let the framework take care of performing the authorization itself.
到目前为止,所有授权方法都是声明性的。您可以应用 [Authorize] 属性(无论是否具有策略名称),并让框架自行执行授权。

For this recipe-editing example, you need to use imperative authorization, so you can authorize the user after you’ve loaded the Recipe from the database. Instead of applying a marker saying “Authorize this method,” you need to write some of the authorization code yourself.
对于此配方编辑示例,您需要使用命令式授权,以便您可以在从数据库加载配方后授权用户。您需要自己编写一些授权代码,而不是应用“Authorize this method”标记。

DEFINITION Declarative and imperative are two different styles of programming. Declarative programming describes what you’re trying to achieve and lets the framework figure out how to achieve it. Imperative programming describes how to achieve something by providing each of the steps needed.‌
定义:声明式和命令式是两种不同的编程风格。声明式编程描述了您要实现的目标,并让框架弄清楚如何实现它。命令式编程描述了如何通过提供所需的每个步骤来实现某些目标。

ASP.NET Core exposes IAuthorizationService, which you can inject into any of your services or endpoints for imperative authorization. The following listing shows how you could update the Edit.cshtml Razor Page (shown partially in listing 24.13) to use the IAuthorizationService to verify whether the action is allowed to continue execution.
ASP.NET Core 公开了 IAuthorizationService,您可以将其注入到任何服务或终端节点中,以实现命令式授权。以下列表显示了如何更新 Edit.cshtml Razor 页面(部分显示在列表 24.13 中),以使用 IAuthorizationService 来验证是否允许作继续执行。

Listing 24.14 Using IAuthorizationService for resource-based authorization
清单 24.14 使用 IAuthorizationService 进行基于资源的授权

[Authorize] ❶
public class EditModel : PageModel
{
[BindProperty]
public Recipe Recipe { get; set; }
private readonly RecipeService _service;
private readonly IAuthorizationService _authService; ❷
public EditModel(
RecipeService service,
IAuthorizationService authService) ❷
{
_service = service;
_authService = authService; ❷
}
public async Task<IActionResult> OnGet(int id)
{
Recipe = _service.GetRecipe(id); ❸
AuthorizationResult authResult = await _authService ❹
.AuthorizeAsync(User, Recipe, "CanManageRecipe"); ❹
if (!authResult.Succeeded) ❺
{ ❺
return new ForbidResult(); ❺
} ❺
return Page(); ❻
}
}

❶ Only authenticated users should be allowed to edit recipes.
只允许经过身份验证的用户编辑配方。
❷ IAuthorizationService is injected into the class constructor using DI.
使用 DI 将 IAuthorizationService 注入到类构造函数中。
❸ Loads the Recipe from the database
从数据库加载配方
❹ Calls IAuthorizationService, providing ClaimsPrinicipal, resource, and the policy name
调用 IAuthorizationService,提供 ClaimsPrinicipal、资源和策略名称
❺ If authorization failed, returns a Forbidden result
如果授权失败,则返回 Forbidden 结果
❻ If authorization was successful, continues displaying the Razor Page
如果授权成功,则继续显示 Razor 页面

IAuthorizationService exposes an AuthorizeAsync method, which requires three things to authorize the request:
IAuthorizationService 公开了一个 AuthorizeAsync 方法,该方法需要三项内容来授权请求:

• The ClaimsPrincipal user object, exposed on the PageModel as User
ClaimsPrincipal 用户对象,在 PageModel 上作为 User 公开

• The resource being authorized: Recipe
正在授权的资源:Recipe

• The policy to evaluate: "CanManageRecipe"
要评估的策略:“CanManageRecipe”

The authorization attempt returns an AuthorizationResult object, which indicates whether the attempt was successful via the Succeeded property. If the attempt wasn’t successful, you should return a new ForbidResult, which is converted to an HTTP 403 Forbidden response or redirects the user to the “access denied” page, depending on whether you’re building a traditional web app or an API app.‌‌‌
授权尝试返回一个 AuthorizationResult 对象,该对象通过 Succeeded 属性指示尝试是否成功。如果尝试不成功,您应该返回一个新的 ForbidResult,该结果将转换为 HTTP 403 Forbidden 响应或将用户重定向到“access denied”页面,具体取决于您是构建传统的 Web 应用程序还是 API 应用程序。

NOTE As mentioned in section 24.2.2, which type of response is generated depends on which authentication services are configured. The default Identity configuration, used by Razor Pages, generates redirects. API apps typically generate HTTP 401 and 403 responses instead.
注意:如第 24.2.2 节所述,生成的响应类型取决于配置的身份验证服务。Razor Pages 使用的默认标识配置会生成重定向。API 应用程序通常会生成 HTTP 401 和 403 响应。

You’ve configured the imperative authorization in the Edit.cshtml Razor Page itself, but you still need to define the "CanManageRecipe" policy that you use to authorize the user. This is the same process as for declarative authorization, so you have to do the following:
你已在 Edit.cshtml Razor 页面本身中配置了命令性授权,但仍需要定义用于授权用户的“CanManageRecipe”策略。此过程与声明式授权的过程相同,因此您必须执行以下作:

• Create a policy in Program.cs by calling AddAuthorization().
通过调用 AddAuthorization() 在 Program.cs 中创建策略。
• Define one or more requirements for the policy.
定义策略的一个或多个要求。
• Define one or more handlers for each requirement.
为每个要求定义一个或多个处理程序。
• Register the handlers in the DI container.
在 DI 容器中注册处理程序。

With the exception of the handler, these steps are identical to the declarative authorization approach with the [Authorize] attribute, so I run through them only briefly here.
除了处理程序之外,这些步骤与具有 [Authorize] 属性的声明性授权方法相同,因此我在这里只简要介绍一下它们。

First, you can create a simple IAuthorizationRequirement. As with many requirements, this contains no data and simply implements the marker interface:
首先,您可以创建一个简单的 IAuthorizationRequirement。与许多要求一样,这不包含任何数据,只实现 marker 接口:

public class IsRecipeOwnerRequirement : IAuthorizationRequirement { }

Defining the policy in Program.cs is similarly simple, as you have only a single requirement. Note that there’s nothing resource-specific in any of this code so far:
在 Program.cs 中定义策略同样简单,因为您只有一个需求。请注意,到目前为止,此代码中没有任何特定于资源的内容:

builder.Services.AddAuthorization(options => { options.AddPolicy("CanManageRecipe", policyBuilder =>
policyBuilder.AddRequirements(new IsRecipeOwnerRequirement()));
});

You’re halfway there. All you need to do now is create an authorization handler for IsRecipeOwnerRequirement and register it with the DI container.
你已经成功了一半。现在,您需要做的就是为 IsRecipeOwnerRequirement 创建一个授权处理程序,并将其注册到 DI 容器中。

24.5.2 Creating a resource-based AuthorizationHandler‌

24.5.2 创建基于资源的 AuthorizationHandler

Resource-based authorization handlers are essentially the same as the authorization handler implementations you saw in section 24.4.2. The only difference is that the handler also has access to the resource being authorized.
基于资源的授权处理程序本质上与您在 Section 24.4.2 中看到的授权处理程序实现相同。唯一的区别是处理程序还可以访问被授权的资源。

To create a resource-based handler, you should derive from the AuthorizationHandler<TRequirement, TResource> base class, where TRequirement is the type of requirement to handle and TResource is the type of resource that you provide when calling IAuthorizationService. Compare this with the AuthorizationHandler<T> class you implemented previously, where you specified only the requirement.
若要创建基于资源的处理程序,应从AuthorizationHandler<TRequirement, TResource> 基类派生,其中 TRequirement 是要处理的要求类型,TResource 是调用 IAuthorizationService 时提供的资源类型。将此类与您之前实现的类进行比较,在AuthorizationHandler<T> 该类中,您只指定了要求。

The next listing shows the handler implementation for your recipe application. You can see that you’ve specified the requirement as IsRecipeOwnerRequirement and the resource as Recipe, and you have implemented the HandleRequirementAsync method.
下一个清单显示了配方应用程序的处理程序实现。您可以看到,您已将要求指定为 IsRecipeOwnerRequirement,将资源指定为 Recipe,并且已实现 HandleRequirementAsync 方法。

Listing 24.15 IsRecipeOwnerHandler for resource-based authorization
列表 24.15 IsRecipeOwnerHandler 用于基于资源的授权

public class IsRecipeOwnerHandler :
AuthorizationHandler<IsRecipeOwnerRequirement, Recipe> ❶
{
private readonly UserManager<ApplicationUser> _userManager; ❷
public IsRecipeOwnerHandler( ❷
UserManager<ApplicationUser> userManager) ❷
{ ❷
_userManager = userManager; ❷
} ❷
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
IsRecipeOwnerRequirement requirement,
Recipe resource) ❸
{
var appUser = await _userManager.GetUserAsync(context.User);
if(appUser == null) ❹
{
return;
}
if(resource.CreatedById == appUser.Id) ❺
{
context.Succeed(requirement); ❻
}
}
}

❶ Implements the necessary base class, specifying the requirement and resource type
实现必要的基类,指定需求和资源类型
❷ Injects an instance of the UserManager<T> class using DI
使用 DI 注入 UserManager<T> 类的实例
❸ As well as the context and requirement, you’re provided the resource instance.
除了上下文和需求外,还为您提供资源实例。
❹ If you aren’t authenticated, appUser will be null.
如果您未通过身份验证,appUser 将为 null。
❺ Checks whether the current user created the Recipe by checking the CreatedById property
通过检查CreatedById 属性来检查当前用户是否创建了配方
❻ If the user created the document, Succeeds the requirement; otherwise, does nothing
如果用户创建了文档,则 Succeeds the requirement;否则,不执行任何作

This handler is slightly more complicated than the examples you’ve seen previously, primarily because you’re using an additional service, UserManager<>, to load the ApplicationUser entity based on ClaimsPrincipal from the request.
此处理程序比您之前看到的示例稍微复杂一些,主要是因为您正在使用附加服务UserManager<> 来根据请求中的 ClaimsPrincipal 加载 ApplicationUser 实体。

NOTE In practice, the ClaimsPrincipal will likely already have the Id added as a claim, making the extra step unnecessary in this case. This example shows the general pattern if you need to use dependency-injected services.
注意:实际上,ClaimsPrincipal 可能已将 Id 添加为声明,因此在这种情况下不需要额外的步骤。此示例显示了需要使用 dependency-injected 服务时的一般模式。

The other significant difference is that the HandleRequirementAsync method has provided the Recipe resource as a method argument. This is the same object you provided when calling AuthorizeAsync on IAuthorizationService. You can use this resource to verify whether the current user created it. If so, you Succeed() the requirement; otherwise, you do nothing.
另一个显著区别是 HandleRequirementAsync 方法已将 Recipe 资源作为方法参数提供。这与您在 IAuthorizationService 上调用 AuthorizeAsync 时提供的对象相同。您可以使用此资源来验证它是否为当前用户创建。如果是这样,则 Succeed() 要求;否则,您什么都不做。

The final task is adding IsRecipeOwnerHandler to the DI container. Your handler uses an additional dependency, UserManager<>, that uses EF Core, so you should register the handler as a scoped service:
最后一项任务是将 IsRecipeOwnerHandler 添加到 DI 容器中。处理程序使用使用 EF Core 的附加依赖项 UserManager<>,因此应将处理程序注册为范围服务:

services.AddScoped<IAuthorizationHandler, IsRecipeOwnerHandler>();

TIP If you’re wondering how to know whether you register a handler as scoped or a singleton, think back to chapter 9.
提示:如果您想知道如何知道将处理程序注册为 scoped 还是 singleton,请回想第 9 章。

Essentially, if you have scoped dependencies, you must register the handler as scoped; otherwise, singleton is fine.
本质上,如果你有 scoped 依赖项,则必须将处理程序注册为 scoped;否则,Singleton 就可以了。

With everything hooked up, you can take the application for a spin. If you try to edit a recipe you didn’t create by clicking the Edit button on the recipe, you’ll either be redirected to the login page (if you hadn’t yet authenticated) or see an “access denied” page, as shown in figure 24.7.
连接好所有内容后,您可以试用该应用程序。如果您尝试通过单击配方上的 Edit 按钮来编辑不是创建的配方,您将被重定向到登录页面(如果您尚未进行身份验证)或看到 “access denied” 页面,如图 24.7 所示。

alt text

Figure 24.7 If you’re logged in but not authorized to edit a recipe, you’ll be redirected to an “Access Denied” page. If you’re not logged in, you’ll be redirected to the Login page.
图 24.7 如果您已登录但无权编辑配方,您将被重定向到“Access Denied”页面。如果您尚未登录,您将被重定向到 Login (登录页面)。

By using resource-based authorization, you’re able to enact more fine-grained authorization requirements that you can apply at the level of an individual document or resource.
通过使用基于资源的授权,您可以制定更精细的授权要求,这些要求可以应用于单个文档或资源级别。

Instead of being able to authorize only that a user can edit any recipe, you can authorize whether a user can edit this recipe.
您可以授权用户是否可以编辑此配方,而不是仅授权用户可以编辑任何配方。

All the authorization techniques you’ve seen so far have focused on server-side checks. Both the [Authorize] attribute and resource-based authorization approaches focus on stopping users from executing a protected endpoint on the server. This is important from a security point of view, but there’s another aspect you should consider: the user experience when they don’t have permission.
到目前为止,您看到的所有授权技术都集中在服务器端检查上。[Authorize] 属性和基于资源的授权方法都侧重于阻止用户在服务器上执行受保护的终结点。从安全角度来看,这很重要,但您应该考虑另一个方面:用户没有权限时的体验。

You’ve protected the code executing on the server, but arguably the Edit button should never have been visible to the user if they weren’t going to be allowed to edit the recipe! In the next section we’ll look at how you can conditionally hide the Edit button by using resource-based authorization in your view models.‌
您已经保护了在服务器上执行的代码,但可以说,如果不允许用户编辑配方,则 Edit (编辑) 按钮永远不应该对用户可见!在下一节中,我们将了解如何在视图模型中使用基于资源的授权来有条件地隐藏 Edit 按钮。

Resource-based authorization versus business-logic checks
基于资源的授权与业务逻辑检查

The value proposition of using the ASP.NET Core framework’s resource- based authorization approach isn’t always clear compared with using simple, manual, business-logic based checks (as in listing 24.13). Using IAuthorizationService and the authorization infrastructure adds an explicit dependency on the ASP.NET Core framework that you may not want to use if you’re performing authorization checks in your domain model services.
与使用简单的、手动的、基于业务逻辑的检查(如清单 24.13 所示)相比,使用 ASP.NET Core 框架基于资源的授权方法的价值主张并不总是很清楚。使用 IAuthorizationService 和授权基础结构会添加对 ASP.NET Core 框架的显式依赖项,如果您在域模型服务中执行授权检查,则可能不想使用该框架。

This is a valid concern without an easy answer. I tend to favor simple business-logic checks inside the domain, without relying on the framework’s authorization infrastructure, to make my domain easier to test and framework-independent. But doing so loses some of the benefits of such a framework:
这是一个合理的担忧,没有一个简单的答案。我倾向于在域内进行简单的业务逻辑检查,而不依赖框架的授权基础设施,以使我的域更易于测试且独立于框架。但这样做会失去这种框架的一些好处:

• The IAuthorizationService uses declarative policies, even though you are calling the authorization framework imperatively.
IAuthorizationService 使用声明性策略,即使您以命令方式调用授权框架也是如此。

• You can decouple the need to authorize an action from the actual requirements.
您可以将授权作的需要与实际要求分离。

• You can easily rely on peripheral services and properties of the request, which may be harder (or undesirable) with business logic checks.
您可以轻松依赖请求的外围服务和属性,这对于业务逻辑检查可能更难(或不可取)。

You can achieve these benefits in business-logic checks, but that typically requires creating a lot of infrastructure too, so you lose a lot of the benefits of keeping things simple. Which approach is best will depend on the specifics of your application design, and there may well be cases for using both.
你可以在业务逻辑检查中实现这些好处,但这通常需要创建大量的基础设施,所以你会失去很多保持简单的好处。哪种方法最好,将取决于应用程序设计的具体情况,并且很可能会出现同时使用这两种方法的情况。

For example, one possible approach is to use the basic [Authorize] attribute as described in section 24.2.1 to prevent anonymous access to your APIs, potentially with simple, coarse policies applied to your APIs. You would then rely on “manual” business-logic checks against the ClaimsPrincipal in your domain as required. This may reduce a lot of the complexity and indirection associated with the ASP.NET Core authorization system.
例如,一种可能的方法是使用第 24.2.1 节中所述的基本 [Authorize] 属性来防止匿名访问您的 API,可能会使用简单、粗略的策略应用于您的 API。然后,您可以根据需要对域中的 ClaimsPrincipal 进行“手动”业务逻辑检查。这可能会降低与 ASP.NET Core 授权系统相关的许多复杂性和间接性。

24.6 Hiding HTML elements from unauthorized users‌

24.6 对未经授权的用户隐藏 HTML 元素

All the authorization code you’ve seen so far has revolved around protecting endpoints on the server side, rather than modifying the UI for users. This is important and should be the starting point whenever you add authorization to an app.
到目前为止,您看到的所有授权代码都围绕着保护服务器端的端点,而不是为用户修改 UI。这很重要,每当您向应用程序添加授权时,都应该从此作为起点。

WARNING Malicious users can easily circumvent your UI, so it’s important to always authorize your endpoints on the server, never on the client alone.
警告:恶意用户可以轻松绕过您的 UI,因此请务必始终在服务器上授权您的终端节点,而不是仅在客户端上授权。

From a user-experience point of view, however, it’s not friendly to have buttons or links that look like they’re available but present an “access denied” page when they’re clicked. A better experience would be for the links to be disabled or not visible at all.
然而,从用户体验的角度来看,让按钮或链接看起来可用但在点击时显示 “拒绝访问” 页面并不友好。更好的体验是禁用链接或根本不显示链接。

You can achieve this in several ways in your own Razor templates. In this section I’m going to show you how to add an additional property to the PageModel, called CanEditRecipe, which the Razor view template will use to change the rendered HTML.
您可以在自己的 Razor 模板中通过多种方式实现此目的。在本节中,我将向您展示如何向 PageModel 添加一个名为 CanEditRecipe 的附加属性,Razor 视图模板将使用该属性来更改呈现的 HTML。

TIP An alternative approach would be to inject IAuthorizationService directly into the view template using the @inject directive, as you saw in chapter 9, but you should generally prefer to keep logic like this in the page handler.
提示:另一种方法是使用 @inject 指令将 IAuthorizationService 直接注入视图模板中,如第 9 章所示,但您通常更愿意在页面处理程序中保留这样的逻辑。

When you’re finished, the rendered HTML looks unchanged for recipes you created, but the Edit button will be hidden when viewing a recipe someone else created, as shown in figure 24.8.
完成后,您创建的配方的渲染 HTML 看起来没有变化,但是在查看其他人创建的配方时,Edit (编辑) 按钮将被隐藏,如图 24.8 所示。

alt text

Figure 24.8 Although the HTML will appear unchanged for recipes you created, the Edit button is hidden when you view recipes created by a different user.
图 24.8 虽然您创建的配方的 HTML 看起来不变,但当您查看其他用户创建的配方时,Edit(编辑)按钮会隐藏。

Listing 24.16 shows the PageModel for the View.cshtml Razor Page, which is used to render the recipe page shown in figure 24.8. As you’ve already seen for resource-based authorization, you can use the IAuthorizationService to determine whether the current user has permission to edit the Recipe by calling AuthorizeAsync.
列表 24.16 显示了 View.cshtml Razor 页面的 PageModel,它用于呈现图 24.8 中所示的配方页面。正如您已经看到的基于资源的授权,您可以使用 IAuthorizationService 通过调用 AuthorizeAsync 来确定当前用户是否有权编辑配方。

You can then set this value as an additional property on the PageModel, called CanEditRecipe.
然后,您可以将此值设置为 PageModel 上的附加属性,称为 CanEditRecipe。

Listing 24.16 Setting the CanEditRecipe property in the View.cshtml Razor Page
列表 24.16 在 View.cshtml Razor 页面中设置 CanEditRecipe 属性

public class ViewModel : PageModel
{
public Recipe Recipe { get; set; }
public bool CanEditRecipe { get; set; } ❶
private readonly RecipeService _service;
private readonly IAuthorizationService _authService;
public ViewModel(
RecipeService service,
IAuthorizationService authService)
{
_service = service;
_authService = authService;
}
public async Task<IActionResult> OnGetAsync(int id)
{
Recipe = _service.GetRecipe(id); ❷
AuthorizationResult isAuthorised = await _authService ❸
.AuthorizeAsync(User, recipe, "CanManageRecipe"); ❸
CanEditRecipe = isAuthorised.Succeeded; ❹
return Page();
}
}

❶ The CanEditRecipe property will be used to control whether the Edit button is rendered.
CanEditRecipe 属性将用于控制是否呈现 Edit 按钮。

❷ Loads the Recipe resource for use with IAuthorizationService
加载 Recipe 资源以用于 IAuthorizationService

❸ Verifies whether the user is authorized to edit the Recipe
验证用户是否有权编辑Recipe

❹ Sets the CanEditRecipe property on the PageModel as appropriate
根据需要在 PageModel 上设置 CanEditRecipe 属性

Instead of blocking execution of the Razor Page (as you did previously in the Edit.cshtml page handler), use the result of the call to AuthorizeAsync to set the CanEditRecipe value on the PageModel. You can then make a simple change to the View.chstml Razor template, adding an if clause around the rendering of the Edit link:
不要阻止 Razor 页面的执行(就像之前在 Edit.cshtml 页面处理程序中所做的那样),而是使用对 AuthorizeAsync 的调用结果在 PageModel 上设置 CanEditRecipe 值。然后,您可以对 View.chstml Razor 模板进行简单的更改,在 Edit 链接的呈现周围添加 if 子句:

@if(Model.CanEditRecipe)
{
<a asp-page="Edit" asp-route-id="@Model.Id" class="btn btn-primary">Edit</a>
}

This ensures that only users who will be able to execute the Edit.cshtml Razor Page can see the link to that page.
这可确保只有能够执行 Edit.cshtml Razor 页面的用户才能看到指向该页面的链接。

WARNING The if clause means that the Edit link will not be displayed unless the current user created the recipe, but you should never rely on client-side security alone. It’s important to keep the server-side authorization check in your Edit.cshtml page handler to protect against any direct access attempts. Even if a malicious user circumvents your UI, the server-side authorization ensures that your application is secure.
警告:if 子句表示除非当前用户创建了配方,否则不会显示 Edit 链接,但您绝不应仅依赖客户端安全性。请务必在 Edit.cshtml 页面处理程序中保留服务器端授权检查,以防止任何直接访问尝试。即使恶意用户绕过了您的 UI,服务器端授权也可以确保您的应用程序是安全的。

With that final change, you’ve finished adding authorization to the recipe application. Anonymous users can browse the recipes created by others, but they must log in to create new recipes. Additionally, authenticated users can edit only the recipes that they created, and they won’t see an Edit link for other people’s recipes.
完成最后的更改后,您已完成向配方应用程序添加授权。匿名用户可以浏览其他人创建的配方,但他们必须登录才能创建新配方。此外,经过身份验证的用户只能编辑他们创建的配方,并且不会看到其他人的配方的 Edit (编辑) 链接。

Authorization is a key aspect of most apps, so it’s important to bear it in mind from an early point. Although it’s possible to add authorization later, as you did with the recipe app, it’s normally preferable to consider authorization sooner rather than later in the app’s development.
授权是大多数应用程序的关键方面,因此尽早牢记这一点非常重要。尽管可以稍后添加授权,就像您对配方应用程序所做的那样,但通常最好在应用程序开发中尽早考虑授权。

In chapters 23 and 24 we focused on authentication and authorization for traditional web applications using Razor. In chapter 25 we’ll look at API applications, how authentication works with tokens, and how to add authorization policies to minimal APIs.
在第 23 章和第 24 章中,我们重点介绍了使用 Razor 对传统 Web 应用程序的身份验证和授权。在第 25 章中,我们将介绍 API 应用程序、身份验证如何与令牌配合使用,以及如何将授权策略添加到最小的 API。

27.7 Summary

27.7 总结

Authentication is the process of determining who a user is. It’s distinct from authorization, the process of determining what a user can do. Authentication typically occurs before authorization.
身份验证是确定用户身份的过程。它与 authorization 不同,授权是确定用户可以做什么的过程。身份验证通常在授权之前进行。

You can use the authorization services in any part of your application, but it’s typically applied using the AuthorizationMiddleware by calling UseAuthorization(). This should be placed after the calls to UseRouting() and UseAuthentication(), and before the call to UseEndpoints() for correct operation.
您可以在应用程序的任何部分使用授权服务,但通常通过调用 UseAuthorization() 使用 AuthorizationMiddleware 来应用授权服务。这应该放在调用 UseRouting() 和 UseAuthentication() 之后,以及调用 UseEndpoints() 之前,以便正确作。

You can protect Razor Pages and MVC actions by applying the [Authorize] attribute. The routing middleware records the presence of the attribute as metadata with the selected endpoint. The authorization middleware uses this metadata to determine how to authorize the request.
可以通过应用 [Authorize] 属性来保护 Razor Pages 和 MVC作。路由中间件将属性的存在记录为所选终端节点的元数据。授权中间件使用此元数据来确定如何授权请求。

The simplest form of authorization requires that a user be authenticated before executing an action. You can achieve this by applying the [Authorize] attribute to a Razor Page, action, controller, or globally. You can also apply attributes conventionally to a subset of Razor Pages.
最简单的授权形式要求在执行作之前对用户进行身份验证。可以通过将 [Authorize] 属性应用于 Razor 页面、作、控制器或全局来实现此目的。您还可以按惯例将属性应用于 Razor Pages 的子集。

Claims-based authorization uses the current user’s claims to determine whether they’re authorized to execute an action. You define the claims needed to execute an action in a policy.
基于声明的授权使用当前用户的声明来确定他们是否有权执行作。您可以定义在策略中执行作所需的声明。

Policies have a name and are configured in Program.cs as part of the call to AddAuthorization() in ConfigureServices. You define the policy using AddPolicy(), passing in a name and a lambda that defines the claims needed.
策略有一个名称,并在 Program.cs 中作为对 ConfigureServices 中 AddAuthorization() 的调用的一部分进行配置。您可以使用 AddPolicy() 定义策略,传入定义所需声明的名称和 lambda。

You can apply a policy to an action or Razor Page by specifying the policy in the authorize attribute; for example, [Authorize("CanAccessLounge")]. This policy will be used by the AuthorizationMiddleware to determine whether the user is allowed to execute the selected endpoint.
您可以通过在 authorize 属性中指定策略,将策略应用于作或 Razor 页面;例如,[Authorize(“CanAccessLounge”)]。AuthorizationMiddleware 将使用此策略来确定是否允许用户执行所选端点。

In a Razor Pages app, if an unauthenticated user attempts to execute a protected action, they’ll be redirected to the login page for your app. If they’re already authenticated but don’t have the required claims, they’ll be shown an “access denied” page instead.
在 Razor Pages 应用中,如果未经身份验证的用户尝试执行受保护的作,他们将被重定向到应用的登录页面。如果他们已经通过身份验证但没有所需的声明,则会显示“access denied”页面。

For complex authorization policies, you can build a custom policy. A custom policy consists of one or more requirements, and a requirement can have one or more handlers. You can combine requirements and handlers to create policies of arbitrary complexity.
对于复杂的授权策略,您可以构建自定义策略。自定义策略由一个或多个要求组成,一个要求可以有一个或多个处理程序。您可以组合需求和处理程序来创建任意复杂度的策略。

For a policy to be authorized, every requirement must be satisfied. For a requirement to be satisfied, one or more of the associated handlers must indicate success, and none must indicate explicit failure.
要授权策略,必须满足所有要求。要满足要求,一个或多个关联的处理程序必须指示成功,并且没有处理程序必须指示显式失败。

AuthorizationHandler<T> contains the logic that determines whether a requirement is satisfied. For example, if a requirement requires that users be over 18, the handler could look for a DateOfBirth claim and calculate the user’s age.
AuthorizationHandler<T> 包含确定是否满足要求的逻辑。例如,如果要求要求用户年满 18 岁,则处理程序可以查找 DateOfBirth 声明并计算用户的年龄。

Handlers can mark a requirement as satisfied by calling context.Succeed (requirement). If a handler can’t satisfy the requirement, it shouldn’t call anything on the context, as a different handler could call Succeed() and satisfy the requirement.
处理程序可以通过调用 context 将需求标记为满足。成功 (要求)。如果处理程序无法满足要求,则它不应在上下文中调用任何内容,因为其他处理程序可以调用 Succeed() 并满足要求。

If a handler calls context.Fail(), the requirement fails, even if a different handler marked it as a success using Succeed(). Use this method only if you want to override any calls to Succeed() from other handlers to ensure that the authorization policy will fail authorization.
如果处理程序调用 context.Fail(),则要求失败,即使其他处理程序使用 Succeed() 将其标记为成功也是如此。仅当您想要覆盖其他处理程序对 Succeed() 的任何调用以确保授权策略授权失败时,才使用此方法。

Resource-based authorization uses details of the resource being protected to determine whether the current user is authorized. For example, if a user is allowed to edit only their own documents, you need to know the author of the document before you can determine whether they’re authorized.
基于资源的授权使用受保护资源的详细信息来确定当前用户是否获得授权。例如,如果只允许用户编辑自己的文档,则需要先知道文档的作者,然后才能确定他们是否获得授权。

Resource-based authorization uses the same policy, requirements, and handler system as before. Instead of applying authorization with the [Authorize] attribute, you must manually call IAuthorizationService and provide the resource you’re protecting.
基于资源的授权使用与以前相同的策略、要求和处理程序系统。您必须手动调用 IAuthorizationService 并提供您正在保护的资源,而不是使用 [Authorize] 属性应用授权。

You can modify the user interface to account for user authorization by adding additional properties to your PageModel. If a user isn’t authorized to execute an action, you can remove or disable the link to that action method in the UI. You should always authorize on the server, even if you’ve removed links from the UI.
您可以通过向 PageModel 添加其他属性来修改用户界面以考虑用户授权。如果用户无权执行作,您可以在 UI 中删除或禁用指向该作方法的链接。您应该始终在服务器上授权,即使您已从 UI 中删除了链接。

Leave a Reply

Your email address will not be published. Required fields are marked *