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

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

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

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

查看或下载示例代码 (如何下载)。

创建一个使用授权保护用户数据的 ASP.NET Core 应用包含一个使用基于资源的授权的示例应用程序。

本文中的示例使用 C# 12 (.NET 8) 或更高版本中提供 的主要构造函数。 有关详细信息,请参阅声明类和结构的主要构造函数(C# 文档教程)和主要构造函数(C# 指南)。 随附于该文章、以早于 .NET 8 的 .NET 版本为目标的示例应用使用构造函数注入。

使用命令性授权

授权是以 IAuthorizationService 的形式实现的,该 IAuthorizationService 在应用启动时由 ASP.NET Core 框架 注册到服务集合中。 该服务通过 依赖项注入提供给类和页面处理程序。 以下页面处理程序还会注入一个文档存储库,开发人员在服务容器中创建并注册该存储库来管理文档操作:

public class DocumentModel(IAuthorizationService authorizationService,
    IDocumentRepository documentRepository) : PageModel
{
    private readonly IAuthorizationService _authorizationService;
    private readonly IDocumentRepository _documentRepository;

    public DocumentModel(IAuthorizationService authorizationService,
        IDocumentRepository documentRepository)
    {
        _authorizationService = authorizationService;
        _documentRepository = documentRepository;
    }

    ...
}

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

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

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

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

在以下示例中,安全资源将加载到自定义 Document 对象中。 调用 AuthorizeAsync 重载来确定当前用户是否可通过自定义“EditPolicy”授权策略编辑该文档。 如果 authorizationResult.Succeeded 为 true,则用户已获得该文档的访问授权,因为用户编写了该文档(Document.Author 与用户的 Name 匹配)。

注释

以下示例假定使用属性集成功进行身份验证 User 。

public async Task<IActionResult> OnGetAsync(Guid documentId)
{
    Document = _documentRepository.Find(documentId);

    ...

    var authorizationResult = await _authorizationService
            .AuthorizeAsync(User, Document, "EditPolicy");

    ...
}

创建基于资源的处理程序

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

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

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 { }

在前面的示例中,假设 SameAuthorRequirement 是更泛型的 SpecificAuthorRequirement 类的特例。 SpecificAuthorRequirement 类(未显示)包含一个表示作者姓名的 Name 属性。 Name 属性可以设置为当前用户。

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

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

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

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

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

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

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

操作要求

若要根据 CRUD(创建、读取、更新、删除)操作的结果做出决策,请使用 OperationAuthorizationRequirement 帮助程序类。 此类使你可以编写一个处理程序,而不必为每种操作类型分别编写一个类。 若要使用它,请提供一些操作名称:

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

使用 OperationAuthorizationRequirement 要求和 Document 资源实现处理程序,如下所示:

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

        return Task.CompletedTask;
    }
}

前面的处理程序使用资源、用户的标识和要求 Name 属性验证操作。

操作资源处理程序的挑战和禁止

本部分介绍如何处理挑战和禁止结果,以及挑战和禁止的差异。

授权失败但用户经过身份验证时,应用可以返回一个 ForbidResult通知身份验证中间件授权失败。 对于未经身份验证的用户,返回一个 ChallengeResult。 对于交互式浏览器客户端,可能适合将用户重定向到登录页面。

注释

以下示例假定使用属性集成功进行身份验证 User 。

var authorizationResult = await _authorizationService
    .AuthorizeAsync(User, Document, Operations.Read);

if (authorizationResult.Succeeded)
{
    return Page();
}
else if (User.Identity?.IsAuthenticated ?? false)
{
    return new ForbidResult();
}
else
{
    return new ChallengeResult();
}