ASP.NET Core 中基于资源的授权

本文介绍如何授权用户访问应用资源。

在应用中, 资源 通常由 C# 类表示,该类包含集合中存储的数据,例如 byte[] 数组。 该类通常包含与资源相关的其他元数据,例如唯一的资源标识符、日期、作者、源信息和 UI 中显示的友好名称。 保存资源数据的集合通常从物理文件内容、云存储对象、内存中对象或数据库中的数据加载。

基于资源的授权在 ASP.NET Core应用中需要特别注意。 属性评估发生在数据绑定之前,在执行加载资源的任何方法之前。 使用 [Authorize] 特性的声明式授权不足以满足基于资源的授权要求。 相反,应用必须调用自定义授权方法,即称为 命令性授权的方法。

本文使用 Razor 组件示例,重点介绍 ASP.NET Core 3.1 或更高版本的 Blazor 授权方案。 有关适用于 ASP.NET Core的所有版本的 Razor Pages 和 MVC 指南,请参阅以下资源:

本文中的示例使用 C# 12 (.NET 8) 或更高版本中提供 的主要构造函数。 有关详细信息,请参阅声明类和结构的主要构造函数(C# 文档教程)和主要构造函数(C# 指南)。

示例应用

Blazor Web App本文的示例是BlazorWebAppAuthorization示例应用(dotnet/AspNetCore.Docs.SamplesGitHub 存储库)(如何下载)。 示例应用使用具有预配置文档对象的种子帐户来演示本文中的示例。 有关详细信息,请参阅示例的自述文件(README.md)。

Caution

此示例应用使用内存中数据库来存储用户信息,这不适合生产方案。 示例应用仅用于演示目的,不应用作生产应用的起点。

使用命令性授权

授权是以 IAuthorizationService 的形式实现的,该 IAuthorizationService 在应用启动时由 ASP.NET Core 框架 注册到服务集合中。 该服务通过Razor提供给组件和其他类:

@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

IAuthorizationService 有两个 AuthorizeAsync 方法重载。 其中一个重载接受资源和策略名称:

Task<AuthorizationResult> AuthorizeAsync(
    ClaimsPrincipal user, 
    object resource, 
    string policyName);

另一个重载接受资源和要评估的要求 (IAuthorizationRequirement) 集合:

Task<AuthorizationResult> AuthorizeAsync(
    ClaimsPrincipal user, 
    object resource,
    IEnumerable<IAuthorizationRequirement> requirements);

在下面的示例中,在 “创建基于资源的处理程序 ”部分中进行了完全解释,受保护的资源将加载到自定义 Document 对象中。 将调用 AuthorizeAsync 重载来确定是否允许当前用户根据“SameAuthorPolicy”授权策略访问文档。 如果 authorizationResult.Succeeded 是 true,则用户有权访问该文档,因为他们编写了该文档(Document.Author 与用户的 Name 匹配):

protected override async Task OnParametersSetAsync()
{
    var user = (await AuthStateProvider.GetAuthenticationStateAsync()).User;

    if (user.Identity is not null && user.Identity.IsAuthenticated)
    {
        var document = DocumentRepository.Find(DocumentId);

        ...

        var authorizationResult = await AuthorizationService
            .AuthorizeAsync(user, document, "SameAuthorPolicy");

        ...
    }
}

创建基于资源的处理程序

创建基于资源的授权处理程序类似于 创建普通要求处理程序。 创建自定义要求类并实现要求处理程序类。 有关创建要求类的详细信息,请参阅 ASP.NET Core中基于策略的授权。

使用以下用于演示的 Document 类:

namespace BlazorWebAppAuthorization.Models;

public class Document
{
    public string? Author { get; set; }

    public byte[]? Content { get; set; }

    public Guid ID { get; set; }

    public string? Title { get; set; }
}

处理程序类指定要求和资源类型。 以下示例演示了一个利用 SameAuthorRequirement 要求和 Document 资源的处理程序。

Policies/Handlers/DocumentAuthorizationHandler.cs:

using Microsoft.AspNetCore.Authorization;
using BlazorWebAppAuthorization.Models;

namespace BlazorWebAppAuthorization.Policies.Handlers;

public class DocumentAuthorizationHandler :
    AuthorizationHandler<SameAuthorRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        SameAuthorRequirement requirement, 
        Document resource)
    {
        if (context.User.Identity?.Name == resource.Author)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

public class SameAuthorRequirement : IAuthorizationRequirement { }

在 Program.cs 中注册要求和处理程序:

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("SameAuthorPolicy", policy =>
        policy.Requirements.Add(new SameAuthorRequirement()));

builder.Services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

在 Startup.ConfigureServices 中注册要求和处理程序:

services.AddAuthorization(options =>
{
    options.AddPolicy("SameAuthorPolicy", policy =>
        policy.Requirements.Add(new SameAuthorRequirement()));
});

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

有关创建授权策略的详细信息,请参阅 ASP.NET Core 中基于策略的授权。

以下 AccessDocument 组件调用 AuthorizeAsync 重载,以确定是否允许当前用户基于“SameAuthorPolicy”授权策略查看文档。 如果 authorizationResult.Succeeded 为 true,则用户已获得该文档的访问授权,因为用户编写了该文档(Document.Author 与用户的 Name 匹配)。

Pages/AccessDocument.razor:

@page "/access-document/{documentId}"
@using Microsoft.AspNetCore.Authorization
@using BlazorWebAppAuthorization.Data
@inject AuthenticationStateProvider AuthStateProvider
@inject IAuthorizationService AuthorizationService
@inject IDocumentRepository DocumentRepository

<h1>Access Document</h1>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
        <p>@message</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized to access this page.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    private string? message;

    [Parameter]
    public string? DocumentId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        var user = (await AuthStateProvider.GetAuthenticationStateAsync()).User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            var document = DocumentRepository.Find(DocumentId);

            if (document == null)
            {
                message = "Document not found.";
                return;
            }

            var authorizationResult = await AuthorizationService
                .AuthorizeAsync(user, document, "SameAuthorPolicy");

            message = authorizationResult.Succeeded
                ? $"You are authorized for document {DocumentId}."
                : $"You are NOT authorized for document {DocumentId}.";
        }
    }
}

在 示例应用中,应用的每个用户都有权访问他们创作的种子文档。

操作要求

若要根据 CRUD(创建、读取、更新、删除)操作的结果做出决策,请使用 OperationAuthorizationRequirement 帮助程序类。 借助帮助程序类,可以为每个操作类型编写单个处理程序而不是单个类。 以下 Operations 类建立所有四种 CRUD 操作类型:

using Microsoft.AspNetCore.Authorization.Infrastructure;

public static class Operations
{
    public static readonly OperationAuthorizationRequirement Create =
        new() { Name = nameof(Create) };
    public static readonly OperationAuthorizationRequirement Delete =
        new() { Name = nameof(Delete) };
    public static readonly OperationAuthorizationRequirement Read =
        new() { Name = nameof(Read) };
    public static readonly OperationAuthorizationRequirement Update =
        new() { Name = nameof(Update) };
}

以下 DocumentAuthorizationCrudHandler 授权处理程序根据资源、在某些情况下还根据用户的身份(角色)以及要求的 Name 属性来验证该操作:

  • 所有用户都可以阅读文档。
  • 只有角色中的 Admin 用户可以创建和更新文档。
  • 只有具有 SuperUser 角色的用户才能删除文档。

Policies/Handlers/DocumentAuthorizationCrudHandler.cs:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using BlazorWebAppAuthorization.Models;

namespace BlazorWebAppAuthorization.Policies.Handlers;

public class DocumentAuthorizationCrudHandler :
    AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OperationAuthorizationRequirement requirement, 
        Document resource)
    {
        if (requirement.Name == Operations.Create.Name &&
            context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == Operations.Delete.Name &&
            context.User.IsInRole("SuperUser"))
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == Operations.Read.Name)
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == Operations.Update.Name &&
            context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

在应用中注册服务的位置:

builder.Services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationCrudHandler>();

调用含操作的 AuthorizeAsync 重载,以返回授权结果。

若要获得 创建 文档的授权,请执行:

var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Create);

若要获得 读取 文档的授权,请执行以下操作:

var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Read);

若要授权 删除 文档:

var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Delete);

若要获得 更新 文档的授权,

var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Update);

在示例应用的AccessDocumentCrud页面中:

  • Leela (leela@contoso.com) 作为 Admin 和 SuperUser,可以对资源执行完整 CRUD 操作。
  • Harry (harry@contoso.com) 仅作为 Admin,可以创建、读取和更新资源。
  • Sarah (sarah@contoso.com) 仅作为 SuperUser,可以删除和读取资源。