ASP.NET Core 中的基於政策的授權

ASP.NET Core 授權政策是一組由框架評估的授權要求,用以決定使用者是否被允許存取某個資源。 原則可以使用名稱註冊,並套用至需要該原則的資源。

本文說明:

  • 如何預設要求使用者已認證。
  • 如何建立需求。
  • 如何註冊與適用保單。
  • 如何選擇命名、預設及備用政策。
  • 用於單一及多重需求評估的授權處理程序。
  • 如何評估單一保單中的多項要求。

具名原則可使用於 [Authorize(Policy = "...")](Razor 元件、頁面和控制器)或 RequireAuthorization(...)(端點),而架構會使用處理常式來評估原則背後的需求。 IAuthorizationPolicyProvider(ASP.NET Core 文件中的自訂授權政策提供者)會動態產生政策,而非在應用程式啟動時註冊。

基於角色的授權 與 基於聲明的授權 使用需求、需求處理器及預先設定的授權政策。 這些元件支援在程式碼中進行授權評估的表達。

本文使用Razor元件範例,並聚焦於 ASP.NET Core 3.1或更新版本的Blazor授權情境。 關於Razor適用於所有 ASP.NET Core 版本的頁面與 MVC 指引,請閱讀本文後參考以下資源:

本文中的一些範例(ASP.NET Core 8.0 或更新版本)使用主要建構子,這些建構子可在 C# 12(.NET 8)或更新版本中提供。 欲了解更多資訊,請參閱「宣告主建構子關於類別與結構」(C# 文件教學)及主建構子(C# 指南)。

要求全域使用者認證

對於伺服器端應用程式,大多數或所有端點都需要驗證,請設定一個需要已認證使用者的備援政策。 這種預設安全的做法保護了新加入且未指定授權元資料的端點。

備用策略(Fallback policy)而非預設策略,適用於未指定授權元資料的端點。 當端點使用 [Authorize] 或 RequireAuthorization() 且未指定原則名稱時,會套用預設原則。 完整政策選擇規則請參閱 預設與備用政策 章節。

var requireAuthPolicy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .Build();

builder.Services.AddAuthorizationBuilder()
    .SetFallbackPolicy(requireAuthPolicy);
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

在 Startup.ConfigureServices 中:

services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

對於刻意設為公開的端點,請套用 [AllowAnonymous] 或呼叫 AllowAnonymous()。

備援政策適用於授權中介軟體處理的請求。 例如:

  • 如果請求未對應到任何端點,且授權中介軟體會針對該請求執行,則會套用後援原則。
  • 靜態檔案中介軟體在授權中介軟體之前提供的靜態檔案不受備援政策保護。
  • 公開端點可以依賴靜態資產,且這些資產必須允許匿名存取。

欲了解更多資訊,請參閱 ASP.NET Core 及伺服器端Blazor應用程式授權模式中的靜態檔案。 Blazor WebAssembly 應用程式不支援伺服器端備援授權政策。 關於Blazor WebAssembly授權模式,請參見 Secure ASP.NET CoreBlazor WebAssembly。

預設與備用政策

授權中介軟體將端點的授權元資料整合成政策。 下表說明了元資料如何決定所使用的政策:

授權元資料 政策或行為
None 若已設定,則會使用 AuthorizationOptions.FallbackPolicy。 預設情況下,備援政策是 null,因此不需要授權。
沒有原則名稱的 [Authorize] 或 RequireAuthorization() 除非端點同時具有明確的 AuthorizationOptions.DefaultPolicy 執行個體,否則會使用 AuthorizationPolicy。 預設政策要求有已認證的使用者。
[Authorize(Policy = "{POLICY NAME}")] 或 RequireAuthorization("{POLICY NAME}") 使用的是具名原則。
[Authorize(Roles = "{ROLES}")] 政策會根據指定的角色建立。 此授權宣告不會新增預設策略。
RequireAuthorization(policy)搭配一個 AuthorizationPolicy 執行個體 採用明確政策。 若有明確的政策元資料,裸授權資料與僅認證方案授權資料不會新增預設政策。
[Authorize(AuthenticationSchemes = "{SCHEME}")] 沒有原則名稱或角色 採用指定的認證方案。 除非端點有明確的 AuthorizationPolicy 執行個體,否則也會使用預設策略。
多屬性 [Authorize] 或策略選擇 RequireAuthorization(...) 呼叫 所選的具名原則、角色、驗證配置和明示原則會合併。 裸授權資料僅在沒有明確 AuthorizationPolicy 實例存在時加入預設策略。 合併原則中的所有條件都必須通過。 備用策略則未被使用。
[AllowAnonymous] 或 AllowAnonymous() 授權中介軟體不會對該端點強制執行授權失敗。 驗證仍然可以執行並填入 HttpContext.User。

備用政策不會與命名或預設政策結合。 對於前表所示的宣告,只有當端點的授權元資料未產生授權政策時,才會選擇該宣告。 例如,使用 [Authorize] 預設策略取代備援策略,並 [Authorize(Policy = "{POLICY NAME}")] 使用命名策略而非備援策略。

透過 IAuthorizationRequirementData 以端點中繼資料形式提供的需求,會在政策選擇後新增。 若無其他授權元資料產生政策,則在設定備援政策時,這些需求會與備援政策合併。

在 ASP.NET Core 8.0 到 10.0 版本中,實作了 IAuthorizationRequirementData 的屬性僅會套用至 Minimal API 和路由端點。 關於版本與主機模型的支援,請參見 帶有「IAuthorizationRequirementData」的自訂授權政策。

需求與原則登錄

授權政策包含一個或多個 需求,政策用以評估目前使用者主體的授權。 需求會實作 IAuthorizationRequirement,這是空的標記介面。

當需求不包含資料或沒有屬性(參數)時,它會作為空標記,觸發相應的 授權處理程序 (IAuthorizationHandler),以處理授權(本文稍後會詳細說明)。 由於此時處理器完全依賴 HTTP 上下文、使用者聲明或後端資料來判斷使用者是否符合需求,因此需求類別本身不需要內部資料或參數。 該要求僅指定框架要評估哪一項規則。

例如,考慮以下最低年齡要求(MinimumAgeRequirement),僅以標記類別實作:

public class MinimumAgeRequirement : IAuthorizationRequirement { }

前述需求用於建立一個政策,確認使用者已超過處理程序檢查的特定年齡。 An AuthorizationHandler<MinimumAgeRequirement> 檢查 AuthorizationHandlerContext.User。 如果使用者的出生日期聲明顯示其年齡超過某個年齡,該要求就成功了。 需求物件在此情況下不需要任何屬性(參數)。 下一個範例展示了完整實作最低年齡要求,該要求包含設定最低年齡參數。

請考慮以下 MinimumAgeRequirement 需求,描述一個單一參數——最低年齡,以評估使用者授權:

using Microsoft.AspNetCore.Authorization;

namespace BlazorWebAppAuthorization.Policies.Requirements;

public class MinimumAgeRequirement(int minimumAge) : IAuthorizationRequirement
{
    public int MinimumAge { get; } = minimumAge;
}
using Microsoft.AspNetCore.Authorization;

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public MinimumAgeRequirement(int minimumAge) =>
        MinimumAge = minimumAge;

    public int MinimumAge { get; }
}
using Microsoft.AspNetCore.Authorization;

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

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

在應用程式 Program 檔案中,透過呼叫 AuthorizationBuilder.AddPolicy來註冊一個政策,作為授權服務設定的一部分。 以下範例 AtLeast21 建立了一份僅有最低年齡要求的保單,並將最低年齡設定為21歲。

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("AtLeast21", policy => 
        policy.Requirements.Add(new MinimumAgeRequirement(21)));

在應用程式 Program 檔案中,透過呼叫 AuthorizationBuilder.AddPolicy來註冊一個政策,作為授權服務設定的一部分。 以下範例會建立具有單一最低年齡需求的 AtLeast21 原則,並將其最低年齡設定為 21 歲:

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

在授權服務配置Startup.ConfigureServicesStartup.cs中,透過呼叫AuthorizationBuilder.AddPolicy來註冊一個政策。 以下範例會建立具有單一最低年齡需求的 AtLeast21 原則,並將其最低年齡設定為 21 歲:

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

若授權政策包含多項授權要求,所有要求必須通過,政策評估才會成功。 換句話說,新增至單一授權策略的多個授權要求會以 AND 條件共同處理。

將政策套用到 Razor 元件

使用帶有政策名稱的Razor屬性對[Authorize]元件套用政策:

@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "CustomerServiceMember")]

若套用多個 政策,所有 政策必須通過後才會授予存取權限:

@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "CustomerServiceMember")]
@attribute [Authorize(Policy = "HumanResourcesMember")]

將原則套用至端點

使用具有原則名稱的 RequireAuthorization,將原則套用至端點。 例如:

app.MapGet("/helloworld", () => "Hello World!")
    .RequireAuthorization("AtLeast21");

在 MVC 和 Razor Pages 應用程式中套用政策

關於如何在 Pages 和 MVC 應用程式中套用政策 Razor 的指引,請參閱以下資源:

授權服務介面(IAuthorizationService)

IAuthorizationService 主要負責判定在呼叫 IAuthorizationService.AuthorizeAsync 多載時,授權是否成功。

  • AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements): 檢查使用者是否符合特定資源的授權要求。
  • AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName):檢查使用者是否符合指定資源的特定授權政策。

如果某資源不是政策評估所需的,則 null 會傳給該資源。

前述方法回傳 AuthorizationResult 包裹於 Task的 。

每個 IAuthorizationHandler 都負責透過 IAuthorizationHandler.HandleAsync 檢查是否符合需求。 該 AuthorizationHandlerContext 類別包含實作所 IAuthorizationHandler 使用的授權資訊。 IAuthorizationRequirement 是一個沒有方法的標記介面,作為追蹤授權是否成功的機制。 當以 AuthorizationHandlerContext.Succeed 呼叫 IAuthorizationRequirement 時,即符合該原則:

context.Succeed(requirement);

授權處理器

授權處理器負責評估要求之屬性。 授權處理者會根據所提供的 AuthorizationHandlerContext 評估需求,以判斷access是否被允許。

需求可以有多個 處理程式。 處理常式可以繼承 AuthorizationHandler<TRequirement>,其中 TRequirement 是要處理的需求。 或者,處理常式可以直接實作 IAuthorizationHandler 以處理多種類型的需求。

針對一項需求使用一個處理常式

下列範例顯示一對一關聯性,其中最低年齡處理常式會處理單一需求:

using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using BlazorWebAppAuthorization.Policies.Requirements;

namespace BlazorWebAppAuthorization.Policies.Handlers;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = 
            context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);

        if (dateOfBirthClaim is null)
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
        var calculatedAge = DateTime.Today.Year - dateOfBirth.Year;

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

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

        return Task.CompletedTask;
    }
}
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = 
            context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);

        if (dateOfBirthClaim is null)
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
        var calculatedAge = DateTime.Today.Year - dateOfBirth.Year;

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

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

        return Task.CompletedTask;
    }
}
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
        {
            // 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).Value);

        var calculatedAge = DateTime.Today.Year - dateOfBirth.Year;

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

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

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

前述代碼決定現任使用者負責人是否有出生日期申報。 當宣告遺失時,即無法進行授權,在此情況下會傳回已完成的工作。 當聲明存在時,會計算使用者的年齡。 如果使用者符合需求所定義的最低年齡,則會將授權視為成功。 當授權成功時,context.Succeed 會以符合的需求作為唯一參數來叫用。

針對多個需求使用一個處理常式

下列範例顯示一對多關聯性,其中權限處理常式可以處理三種不同類型的需求:

using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using BlazorWebAppAuthorization.Policies.Requirements;

namespace BlazorWebAppAuthorization.Policies.Handlers;

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);
                }
            }
        }

        return Task.CompletedTask;
    }

    private static bool IsOwner(ClaimsPrincipal user, object? resource)
    {
        // Code omitted for brevity
        return true;
    }

    private static bool IsSponsor(ClaimsPrincipal user, object? resource)
    {
        // Code omitted for brevity
        return true;
    }
}
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;

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);
                }
            }
        }

        // 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 — 這個屬性包含未標示為成功的需求。 對於 ReadPermission 需求,使用者必須是擁有者或贊助者才能access所請求的資源。 對於EditPermission或DeletePermission的要求,他們必須是所有者才能存取所請求的資源。

處理者註冊

在設定期間,將處理常式註冊在服務集合中。 以下範例將最小年齡處理常式MinimumAgeHandler註冊為單例服務,但處理常式可使用任何內建的 服務存留期來註冊:

builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

可以將需求和處理常式組合成實作 IAuthorizationRequirement 和 IAuthorizationHandler 的單一類別。 此組合會在處理常式與需求之間建立緊密結合,而且只建議用於簡單需求和處理常式。 建立同時實作這兩個介面的類別後,就不需要在服務容器中註冊處理常式,因為內建的 PassThroughAuthorizationHandler 讓需求得以自行處理。

請參閱 ASP.NET Core AssertionRequirement 類別的實作,其中示範了 AssertionRequirement 同時作為需求與處理常式,並且全部封裝在單一完全自含的類別中。 框架 AssertionRequirement 的 API 允許你使用內嵌 lambda 表達式來驗證存取,而不必寫出獨立的樣板式需求和處理器類別。

Note

指向 .NET 參考來源的文件連結通常會載入儲存庫的預設分支,該分支代表下一版 .NET 版本的當前開發進度。 若要為特定發行版本選取標籤,請使用 切換分支或標籤 下拉式選單。 如需詳細資訊,請參閱如何選取 ASP.NET Core 原始程式碼 (dotnet/AspNetCore.Docs #26205) 的版本標籤。

處理常式應該傳回什麼?

Handle中的 方法不會傳回任何值。 如何指示成功或失敗的狀態?

  • 處理常式藉由呼叫 context.Succeed,並傳遞已成功驗證的需求(IAuthorizationRequirement)來表示成功。

  • 處理器通常不需要處理失敗情況,因為針對相同需求的其他處理器可能會成功。

  • 若要保證失敗,即使其他需求處理程序成功,也請呼叫 context.Fail。

如果處理常式呼叫 context.Succeed 或 context.Fail,則仍會呼叫所有其他處理常式。 這可讓需求產生副作用,例如記錄日誌;即使另一個處理常式已成功驗證某項需求,或在某項需求上驗證失敗,這些副作用仍會發生。 當設為 false 時,呼叫 InvokeHandlersAfterFailure 時,context.Fail 屬性會中止處理程序的執行。 InvokeHandlersAfterFailure 預設為 true,在此情況下會呼叫所有處理常式。

Note

即使驗證失敗,仍會調用授權處理常式。 此外,處理器可以任意順序執行,因此 不必 依賴呼叫處理器的順序。

為什麼我會想要多個處理常式來滿足一個需求?

如果您想要以 OR 為基礎進行評估,請針對單一需求實作多個處理程式。 舉例來說,假設Contoso公司有只能用感應卡開啟的門。 如果你把鑰匙卡留在家裡,接待員列印了一個臨時貼紙,併為您打開門。 在這種情況下,應用程式只有一個需求,但有多個處理器,每個處理器都檢視單一需求。

以下為範例實作:

  • BuildingEntryRequirement 是進入建築物的必要條件。
  • BadgeEntryHandler (個人持有徽章)與 TemporaryStickerHandler (個人持有臨時貼紙)是獨立的處理員,各自檢視單一需求。

BuildingEntryRequirement.cs:

using Microsoft.AspNetCore.Authorization;

namespace BlazorWebAppAuthorization.Policies.Requirements;

public class BuildingEntryRequirement : IAuthorizationRequirement { }

BadgeEntryHandler.cs:

using Microsoft.AspNetCore.Authorization;
using BlazorWebAppAuthorization.Policies.Requirements;

namespace BlazorWebAppAuthorization.Policies.Handlers;

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

        return Task.CompletedTask;
    }
}

TemporaryStickerHandler.cs:

using Microsoft.AspNetCore.Authorization;
using BlazorWebAppAuthorization.Policies.Requirements;

namespace BlazorWebAppAuthorization.Policies.Handlers;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => 
            c.Type == "TemporaryBadgeId" &&
            c.Issuer == "https://contososecurity"))
        {
            // Code to check expiration date omitted for brevity.
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

BuildingEntryRequirement.cs:

using Microsoft.AspNetCore.Authorization;

public class BuildingEntryRequirement : IAuthorizationRequirement
{
}

BadgeEntryHandler.cs:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;

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

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

TemporaryStickerHandler.cs:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;

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

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

請確定這兩個處理程式 都已註冊。 如果任一處理常式在原則評估 BuildingEntryRequirement 時成功,則原則評估成功。

使用 Func 來滿足原則

在某些情況下,使用 Func<AuthorizationHandlerContext, bool> 原則建構器設定原則時,可透過 RequireAssertion 委派以程式碼簡單表達如何滿足該原則。 例如,前述 BadgeEntryHandler 可以重寫如下:

    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
            (c.Type == "BadgeId" || c.Type == "TemporaryBadgeId")
            && c.Issuer == "https://contososecurity")));
});

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

透過外部服務範例授權

透過外部服務範例(dotnet/AspNetCore.Docs.SamplesGitHub 倉庫)授權說明如何透過外部授權服務實作額外的授權需求。 該解決方案Contoso.API的專案是使用 Microsoft Entra ID 來保護的。 Contoso.Security.API project 的額外授權檢查會返回一個有效負載,說明 Contoso.API 客戶端應用程式是否能呼叫 GetWeather API。

設定範例

以下示範需在命令殼層中使用 NSwag(Swagger/OpenAPI) 或 cURL。

在 Contoso.Security.API 專案中,將 AllowedClients 預留位置({CLIENT ID})設為任意測試 GUID 值(例如 00001111-aaaa-2222-bbbb-3333cccc4444):

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AllowedClients": [
    "{CLIENT ID (FOR THE CLIENT CALLING CONTOSO.API)}"
  ]
}

在開啟給 Contoso.API 專案的指令殼中,使用 dotnet user-jwts 來產生一個存取權杖,並標 appid 示客戶端應用程式的 ID 聲明,該 ID 是在前一步建立的(例如, 00001111-aaaa-2222-bbbb-3333cccc4444)。

dotnet user-jwts create --claim appid={GUID}

範例:

dotnet user-jwts create --claim appid=00001111-aaaa-2222-bbbb-3333cccc4444

輸出會在命令提示字元中的「Token:」之後產生令牌:

New JWT saved with ID '{JWT ID}'.
Name: {USER}
Custom Claims: [appid=00001111-aaaa-2222-bbbb-3333cccc4444]

Token: {TOKEN}

將標記的值({TOKEN} 佔位符出現在上面的輸出中)存放起來,以便稍後使用。

你可以在網路上的 JWT 解碼器中解碼該權杖,例如使用 jwt.ms 來查看其內容,即可看到其中包含帶有用戶端應用程式 ID 的 appid 宣告:

{
  "alg": "HS256",
  "typ": "JWT"
}.{
  "unique_name": "{USER}",
  "sub": "{USER}",
  "jti": "14ed7729",
  "appid": "{CLIENT ID}",
  "aud": [
    "https://localhost:7250",
    "http://localhost:7251"
  ],
  "nbf": 1780660887,
  "exp": 1788609687,
  "iat": 1780660888,
  "iss": "dotnet-user-jwts"
}.[Signature]

再次使用不正確的用戶端 ID(appid)值執行該命令:

dotnet user-jwts create --claim appid=aaaabbbb-0000-cccc-1111-dddd2222eeee

把第二個代幣的價值放在一旁。

在 Visual Studio 中,或在命令殼層中使用 Contoso.API 命令,啟動 Contoso.Security.API 和 dotnet watch 這兩個專案:

dotnet watch

在專案Contoso.API的 https://localhost:7250/swagger/index.html Swagger 介面中(),選擇授權按鈕。

在 「可用授權: Bearer 」視窗中,輸入存取權杖。 選取 [授權] 按鈕。 關閉 可用授權 視窗。

在 default 下,選取 端點的 /WeatherForecast 按鈕。 選取 [試用] 按鈕。 選取 [執行] 按鈕。

在 回應>伺服器回應>回應主體 下方顯示的輸出會顯示由 Contoso.API 專案傳回的天氣預報 JSON。

對產生的存取權杖執行同樣步驟,該權杖是用無效的用戶端應用程式 ID 產生的。 回覆是 403 - 禁止。

其他資源