Skip to content

掌握 ASP.NET Core 的灵活授权的声明变换

Published: at 12:00 AM

掌握 ASP.NET Core 的灵活授权的声明变换

摘录

声明基授权机制是 ASP.NET Core 中现代授权的核心。但是,你的身份提供者 (IDP) 发出的访问令牌可能并不总是完全符合你的应用程序的内部授权需求。解决方案是什么?声明变换。

原文 Master Claims Transformation for Flexible ASP.NET Core AuthorizationMilan Jovanović 发表。


声明基授权机制是 ASP.NET Core 中现代授权的核心。但是,你的身份提供者 (IDP) 发出的访问令牌可能并不总是完全符合你的应用程序的内部授权需求。

Microsoft Entra ID(之前的 Azure AD)或 Auth0 这样的外部 IDP 可能有它们自己的声明架构,或者可能不直接发出你的应用程序对其授权逻辑所需的所有声明。

解决方案是什么?声明变换。

声明变换允许你在应用程序使用它们进行授权之前修改声明。

在今天的问题中,我们将:

声明变换是如何工作的?

人们说一张图片胜过千言万语。在软件工程中,我们有称为 UML 图的东西可以用来画出一个图景。

这里有一个展示声明变换流程的 序列图

  1. 用户使用身份提供者进行身份验证
  2. 用户调用后端 API 并提供访问令牌
  3. 后端 API 执行声明变换和授权
  4. 如果用户被正确授权,后端 API 返回响应

声明变换序列图。

让我们看看如何在 ASP.NET Core 中实现这一点。

简单的声明变换

声明可以从受信任的身份提供者发出的任何用户或身份数据中创建。声明是一个表示主题身份的名称-值对,而不是主题可以执行的操作。

在 ASP.NET Core 中 声明变换的核心是 IClaimsTransformation 接口。

它暴露了一个用于变换声明的方法:

public interface IClaimsTransformation
{
    Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal);
}

这里是使用 IClaimsTransformation 添加自定义声明的简单示例:

internal static class CustomClaims
{
    internal const string CardType = "card_type";
}

internal sealed class CustomClaimsTransformation : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        if (principal.HasClaim(claim => claim.Type == CustomClaims.CardType))
        {
            return Task.FromResult(principal);
        }

        ClaimsIdentity claimsIdentity = new ClaimsIdentity();

        claimsIdentity.AddClaim(new Claim(CustomClaims.CardType, "platinum"));

        principal.AddIdentity(claimsIdentity);

        return Task.FromResult(principal);
    }
}

CustomClaimsTransformation 类应该注册为服务:

builder.Services.AddTransient<IClaimsTransformation, CustomClaimsTransformation>();

最后,你可以定义一个使用此声明的自定义授权策略:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy(
        "HasPlatinumCard",
        builder => builder
            .RequireAuthenticatedUser()
            .RequireClaim(CustomClaims.CardType, "platinum"));
});

使用 IClaimsTransformation 时你应该知道几个问题:

使用声明变换实现 RBAC

基于角色的访问控制 (RBAC) 是一种授权模型,其中权限被分配给角色,角色被授予用户。声明变换有助于平滑地实现 RBAC。通过添加角色声明和可能的权限声明,可以简化你应用程序中的授权逻辑。另一个好处是你可以保持访问令牌更小并且不包含任何角色或权限声明。

让我们考虑一个场景,你的应用程序在细粒度级别上管理资源,但你的身份提供者只提供了如 RegisteredMember 这样的粗粒度角色。你可以使用声明变换将 Member 角色映射到特定的细粒度权限,如 SubmitOrderPurchaseTicket

这里是一个更复杂的 CustomClaimsTransformation 实现。我们发送一个 GetUserPermissionsQuery 数据库查询并获得 PermissionsResponse 返回。PermissionsResponse 包含用户的权限,这些权限被添加为自定义声明。

internal sealed class CustomClaimsTransformation(
    IServiceProvider serviceProvider)
    : IClaimsTransformation
{
    public async Task<ClaimsPrincipal> TransformAsync(
        ClaimsPrincipal principal)
    {
        if (principal.HasClaim(c => c.Type == CustomClaims.Sub ||
                                    c.Type == CustomClaims.Permission))
        {
            return principal;
        }

        using IServiceScope scope = serviceProvider.CreateScope();

        ISender sender = scope.ServiceProvider.GetRequiredService<ISender>();

        string identityId = principal.GetIdentityId();

        Result<PermissionsResponse> result = await sender.Send(
            new GetUserPermissionsQuery(identityId));

        if (result.IsFailure)
        {
            throw new ClaimsAuthorizationException(
                nameof(GetUserPermissionsQuery), result.Error);
        }

        var claimsIdentity = new ClaimsIdentity();

        claimsIdentity.AddClaim(
            new Claim(CustomClaims.Sub, result.Value.UserId.ToString()));

        foreach (string permission in result.Value.Permissions)
        {
            claimsIdentity.AddClaim(
                new Claim(CustomClaims.Permission, permission));
        }

        principal.AddIdentity(claimsIdentity);

        return principal;
    }
}

既然 ClaimsPrincipal 包含作为自定义声明的权限,你可以做一些有趣的事情。例如,你可以实现一个基于权限的 AuthorizationHandler

internal sealed class PermissionAuthorizationHandler
    : AuthorizationHandler<PermissionRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PermissionRequirement requirement)
    {
        HashSet<string> permissions = context.User.GetPermissions();

        if (permissions.Contains(requirement.Permission))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

要点

声明变换是一种优雅的方式,可以弥补身份提供者提供的声明和你的 ASP.NET Core 应用程序需求之间的差距。IClaimsTransformation 接口使你能够自定义当前 ClaimsPrincipal 的声明。无论你需要添加角色、映射外部组到内部权限,还是从用户档案中提取额外信息,声明变换都提供了这样做的灵活性。

然而,使用声明变换时要考虑几个关键点:

如果你想看到 ASP.NET Core 中的 RBAC 完整实现,请查看这个身份验证和授权播放列表