ASP.NET Core中基于策略的授权Policy-based authorization in ASP.NET Core

实际上,基于角色的授权基于声明的授权都使用要求、要求处理程序和预配置的策略。Underneath the covers, role-based authorization and claims-based authorization use a requirement, a requirement handler, and a pre-configured policy. 这些构建基块支持在代码中使用授权评估表达式。These building blocks support the expression of authorization evaluations in code. 结果就是,授权结构更加丰富,可重复使用,并且可以测试。The result is a richer, reusable, testable authorization structure.

授权策略包含一个或多个要求。An authorization policy consists of one or more requirements. 授权策略包含一个或多个要求,并在 Startup.ConfigureServices 方法中作为授权服务配置的一部分注册:It's registered as part of the authorization service configuration, in the Startup.ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });
}

在前面的示例中,创建了一个“AtLeast21”策略。In the preceding example, an "AtLeast21" policy is created. 它只有一个要求—,即最低年龄,以参数的形式传递给要求。It has a single requirement—that of a minimum age, which is supplied as a parameter to the requirement.

IAuthorizationServiceIAuthorizationService

确定授权是否成功的主要服务是IAuthorizationService:The primary service that determines if authorization is successful is IAuthorizationService:

/// <summary>
/// Checks policy based permissions for a user
/// </summary>
public interface IAuthorizationService
{
    /// <summary>
    /// Checks if a user meets a specific set of requirements for the specified resource
    /// </summary>
    /// <param name="user">The user to evaluate the requirements against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="requirements">The requirements to evaluate.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// This value is <value>true</value> when the user fulfills the policy; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check 
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, 
                                     IEnumerable<IAuthorizationRequirement> requirements);

    /// <summary>
    /// Checks if a user meets a specific authorization policy
    /// </summary>
    /// <param name="user">The user to check the policy against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="policyName">The name of the policy to check against a specific 
    /// context.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// Returns a flag indicating whether the user, and optional resource has fulfilled 
    /// the policy.    
    /// <value>true</value> when the policy has been fulfilled; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(
                                ClaimsPrincipal user, object resource, string policyName);
}

前面的代码突出显示了IAuthorizationService的两种方法。The preceding code highlights the two methods of the IAuthorizationService.

IAuthorizationRequirement是无方法的标记服务, 以及用于跟踪授权是否成功的机制。IAuthorizationRequirement is a marker service with no methods, and the mechanism for tracking whether authorization is successful.

IAuthorizationHandler个负责检查是否满足要求:Each IAuthorizationHandler is responsible for checking if requirements are met:

/// <summary>
/// Classes implementing this interface are able to make a decision if authorization
/// is allowed.
/// </summary>
public interface IAuthorizationHandler
{
    /// <summary>
    /// Makes a decision if authorization is allowed.
    /// </summary>
    /// <param name="context">The authorization information.</param>
    Task HandleAsync(AuthorizationHandlerContext context);
}

AuthorizationHandlerContext类是处理程序用来标记是否已满足要求的类:The AuthorizationHandlerContext class is what the handler uses to mark whether requirements have been met:

 context.Succeed(requirement)

下面的代码显示授权服务的默认实现 (和批注批注) 的默认实现:The following code shows the simplified (and annotated with comments) default implementation of the authorization service:

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, 
             object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
    // Create a tracking context from the authorization inputs.
    var authContext = _contextFactory.CreateContext(requirements, user, resource);

    // By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
    var handlers = await _handlers.GetHandlersAsync(authContext);

    // Invoke all handlers.
    foreach (var handler in handlers)
    {
        await handler.HandleAsync(authContext);
    }

    // Check the context, by default success is when all requirements have been met.
    return _evaluator.Evaluate(authContext);
}

下面的代码演示了一个ConfigureServices典型的:The following code shows a typical ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    // Add all of your handlers to DI.
    services.AddSingleton<IAuthorizationHandler, MyHandler1>();
    // MyHandler2, ...

    services.AddSingleton<IAuthorizationHandler, MyHandlerN>();

    // Configure your policies
    services.AddAuthorization(options =>
          options.AddPolicy("Something",
          policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")));


    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

使用IAuthorizationService[Authorize(Policy = "Something")]进行授权。Use IAuthorizationService or [Authorize(Policy = "Something")] for authorization.

将策略应用到 MVC 控制器Applying policies to MVC controllers

如果使用 Razor Pages, 请参阅本文档中的将策略应用于 Razor PagesIf you're using Razor Pages, see Applying policies to Razor Pages in this document.

策略通过使用[Authorize]具有策略名称的属性应用到控制器。Policies are applied to controllers by using the [Authorize] attribute with the policy name. 例如:For example:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseController : Controller
{
    public IActionResult Index() => View();
}

将策略应用到 Razor PagesApplying policies to Razor Pages

通过将[Authorize]属性与策略名称一起使用, 将策略应用到 Razor Pages。Policies are applied to Razor Pages by using the [Authorize] attribute with the policy name. 例如:For example:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseModel : PageModel
{
}

还可以通过使用授权约定, 将策略应用到 Razor Pages。Policies can also be applied to Razor Pages by using an authorization convention.

要求Requirements

授权要求是一个可供策略用来评估当前用户主体的数据参数的集合。An authorization requirement is a collection of data parameters that a policy can use to evaluate the current user principal. 在我们的“AtLeast21”策略中,此要求是单个参数—最低年龄。In our "AtLeast21" policy, the requirement is a single parameter—the minimum age. 要求可以实现 IAuthorizationRequirement,这是一个空标记接口。A requirement implements IAuthorizationRequirement, which is an empty marker interface. 参数化的最低年龄要求可以按如下方式实现:A parameterized minimum age requirement could be implemented as follows:

using Microsoft.AspNetCore.Authorization;

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

如果授权策略包含多个授权要求, 则所有要求必须通过, 才能成功进行策略评估。If an authorization policy contains multiple authorization requirements, all requirements must pass in order for the policy evaluation to succeed. 换句话说, 添加到单个授权策略中的多个授权要求将分别处理。In other words, multiple authorization requirements added to a single authorization policy are treated on an AND basis.

备注

要求不需要具有数据或属性。A requirement doesn't need to have data or properties.

授权处理程序Authorization handlers

授权处理程序负责评估要求的属性。An authorization handler is responsible for the evaluation of a requirement's properties. 授权处理程序会针对提供的AuthorizationHandlerContext 来评估要求,确定是否允许访问。The authorization handler evaluates the requirements against a provided AuthorizationHandlerContext to determine if access is allowed.

一项要求可以有多个处理程序A requirement can have multiple handlers. 处理程序可以继承 AuthorizationHandler<,其中的 TRequirement 是需处理的要求。A handler may inherit AuthorizationHandler<TRequirement>, where TRequirement is the requirement to be handled. 另外,一个处理程序也可以通过实现 IAuthorizationHandler 来处理多个类型的要求。Alternatively, a handler may implement IAuthorizationHandler to handle more than one type of requirement.

为一个要求使用处理程序Use a handler for one requirement

下面是一对一关系的示例,其中的单个最低年龄要求处理程序使用单个要求:The following is an example of a one-to-one relationship in which a minimum age handler utilizes a single requirement:

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   MinimumAgeRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth &&
                                        c.Issuer == "http://contoso.com"))
        {
            //TODO: Use the following if targeting a version of
            //.NET Framework older than 4.6:
            //      return Task.FromResult(0);
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(
            context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth && 
                                        c.Issuer == "http://contoso.com").Value);

        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

前面的代码确定当前的用户主体是否有一个由已知的受信任颁发者颁发的出生日期声明。The preceding code determines if the current user principal has a date of birth claim which has been issued by a known and trusted Issuer. 缺少声明时,无法进行授权,这种情况下会返回已完成的任务。Authorization can't occur when the claim is missing, in which case a completed task is returned. 存在声明时,会计算用户的年龄。When a claim is present, the user's age is calculated. 如果用户满足此要求所定义的最低年龄,则可以认为授权成功。If the user meets the minimum age defined by the requirement, authorization is deemed successful. 授权成功后,会调用 context.Succeed,使用满足的要求作为其唯一参数。When authorization is successful, context.Succeed is invoked with the satisfied requirement as its sole parameter.

为多个要求使用处理程序Use a handler for multiple requirements

下面是一个一对多关系的示例, 其中权限处理程序可以处理三种不同类型的要求:The following is an example of a one-to-many relationship in which a permission handler can handle three different types of requirements:

using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class PermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadPermission)
            {
                if (IsOwner(context.User, context.Resource) ||
                    IsSponsor(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditPermission ||
                     requirement is DeletePermission)
            {
                if (IsOwner(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }

    private bool IsOwner(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }

    private bool IsSponsor(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }
}

前面的代码遍历PendingRequirements—属性, 该属性包含未标记为成功的要求。The preceding code traverses PendingRequirements—a property containing requirements not marked as successful. ReadPermission对于要求, 用户必须是所有者或主办方才能访问请求的资源。For a ReadPermission requirement, the user must be either an owner or a sponsor to access the requested resource. 对于EditPermissionDeletePermission要求, 该用户必须是访问所请求资源的所有者。In the case of an EditPermission or DeletePermission requirement, he or she must be an owner to access the requested resource.

处理程序注册Handler registration

处理程序是在配置期间在服务集合中注册的。Handlers are registered in the services collection during configuration. 例如:For example:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });

    services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
}

前面的代码通过MinimumAgeHandler调用services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();注册为单一实例。The preceding code registers MinimumAgeHandler as a singleton by invoking services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();. 可以使用任何内置服务生存期来注册处理程序。Handlers can be registered using any of the built-in service lifetimes.

处理程序应返回什么?What should a handler return?

请注意,Handle处理程序示例中的 方法不返回值。Note that the Handle method in the handler example returns no value. 如何表明状态是成功还是失败?How is a status of either success or failure indicated?

  • 处理程序通过调用 context.Succeed(IAuthorizationRequirement requirement) 并传递已成功验证的要求来表示成功。A handler indicates success by calling context.Succeed(IAuthorizationRequirement requirement), passing the requirement that has been successfully validated.

  • 处理程序通常不需要处理失败,因为同一要求的其他处理程序可能会成功。A handler doesn't need to handle failures generally, as other handlers for the same requirement may succeed.

  • 若要保证故障, 即使其他要求处理程序成功, context.Fail请调用。To guarantee failure, even if other requirement handlers succeed, call context.Fail.

如果处理程序调用context.Succeedcontext.Fail, 则仍将调用所有其他处理程序。If a handler calls context.Succeed or context.Fail, all other handlers are still called. 这允许要求产生副作用, 如日志记录, 即使另一个处理程序已成功验证或失败, 也会发生这种情况。This allows requirements to produce side effects, such as logging, which takes place even if another handler has successfully validated or failed a requirement. 设置为 false 时,InvokeHandlersAfterFailure 属性 (在 ASP.NET Core 1.1 及更高版本中提供)会在已调用 context.Fail 的情况下不执行处理程序。When set to false, the InvokeHandlersAfterFailure property (available in ASP.NET Core 1.1 and later) short-circuits the execution of handlers when context.Fail is called. InvokeHandlersAfterFailure 默认为 true,这种情况下会调用所有处理程序。InvokeHandlersAfterFailure defaults to true, in which case all handlers are called.

备注

即使身份验证失败, 也会调用授权处理程序。Authorization handlers are called even if authentication fails.

为什么需要对一项要求使用多个处理程序?Why would I want multiple handlers for a requirement?

在需要以 OR 逻辑为基础进行评估的情况下,可以针对单个要求实现多个处理程序。In cases where you want evaluation to be on an OR basis, implement multiple handlers for a single requirement. 例如,Microsoft 的门只能使用门禁卡打开。For example, Microsoft has doors which only open with key cards. 如果你将门禁卡丢在家中,可以要求前台打印一张临时标签来开门。If you leave your key card at home, the receptionist prints a temporary sticker and opens the door for you. 在这种情况下,只有一个要求,即 BuildingEntry,但有多个处理程序,每个处理程序针对单个要求进行检查。In this scenario, you'd have a single requirement, BuildingEntry, but multiple handlers, each one examining a single requirement.

BuildingEntryRequirement.csBuildingEntryRequirement.cs

using Microsoft.AspNetCore.Authorization;

public class BuildingEntryRequirement : IAuthorizationRequirement
{
}

BadgeEntryHandler.csBadgeEntryHandler.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "BadgeId" &&
                                       c.Issuer == "http://microsoftsecurity"))
        {
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

TemporaryStickerHandler.csTemporaryStickerHandler.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 
                                                   BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "TemporaryBadgeId" &&
                                       c.Issuer == "https://microsoftsecurity"))
        {
            // We'd also check the expiration date on the sticker.
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

请确保这两个处理程序已注册Ensure that both handlers are registered. 当策略评估 BuildingEntryRequirement 时,如果有一个处理程序成功,则策略评估成功。If either handler succeeds when a policy evaluates the BuildingEntryRequirement, the policy evaluation succeeds.

使用 func 来实现策略Using a func to fulfill a policy

有些情况下,策略很容易用代码实现。There may be situations in which fulfilling a policy is simple to express in code. 可以在通过 Func<AuthorizationHandlerContext, bool> 策略生成器配置策略时提供 RequireAssertionIt's possible to supply a Func<AuthorizationHandlerContext, bool> when configuring your policy with the RequireAssertion policy builder.

例如,上一个 BadgeEntryHandler 可以重写,如下所示:For example, the previous BadgeEntryHandler could be rewritten as follows:

services.AddAuthorization(options =>
{
     options.AddPolicy("BadgeEntry", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                (c.Type == "BadgeId" ||
                 c.Type == "TemporaryBadgeId") &&
                 c.Issuer == "https://microsoftsecurity")));
});

访问处理程序中的 MVC 请求上下文Accessing MVC request context in handlers

在授权处理程序中实现的 HandleRequirementAsync 方法有两个参数:AuthorizationHandlerContext 以及你正在处理的 TRequirementThe HandleRequirementAsync method you implement in an authorization handler has two parameters: an AuthorizationHandlerContext and the TRequirement you are handling. MVC 或 Jabbr 之类的框架可以自由地将任何对象添加到 Resource 中的 AuthorizationHandlerContext 属性,以便传递额外信息。Frameworks such as MVC or Jabbr are free to add any object to the Resource property on the AuthorizationHandlerContext to pass extra information.

例如,MVC 在 属性中传递 AuthorizationFilterContextResource 实例。For example, MVC passes an instance of AuthorizationFilterContext in the Resource property. 可以通过此属性访问 HttpContextRouteData 以及 MVC 和 Razor 页面提供的所有其他内容。This property provides access to HttpContext, RouteData, and everything else provided by MVC and Razor Pages.

Resource 属性的使用取决于框架。The use of the Resource property is framework specific. 使用 Resource 属性中的信息时,授权策略就会局限于特定的框架。Using information in the Resource property limits your authorization policies to particular frameworks. Resource InvalidCastException使用关键字强制转换属性, 然后确认强制转换已成功, 以确保在其他框架上运行时代码不会崩溃: isYou should cast the Resource property using the is keyword, and then confirm the cast has succeeded to ensure your code doesn't crash with an InvalidCastException when run on other frameworks:

// Requires the following import:
//     using Microsoft.AspNetCore.Mvc.Filters;
if (context.Resource is AuthorizationFilterContext mvcContext)
{
    // Examine MVC-specific things like routing data.
}