伺服器端 ASP.NET Core Blazor 其他安全性情節

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .NET 8 版本

此文章說明如何針對其他安全性情節設定伺服器端 Blazor,包括如何將權杖傳遞至 Blazor 應用程式。

注意

本文中的程式碼範例採用 可為 Null 的參考型別 (NRT) 和 .NET 編譯器 Null 狀態靜態分析,這在 .NET 6 或更新版本的 ASP.NET Core 中受到支援。 以 ASP.NET Core 5.0 或更早版本為目標時,請從此文章範例的 string?TodoItem[]?WeatherForecast[]?IEnumerable<GitHubBranch>? 類型中移除 Null 類型指定 (?)。

將權杖傳遞至伺服器端 Blazor 應用程式

在伺服器端 Razor 應用程式中 Blazor 元件外部可用的權杖,可以使用此節中所述的方法傳遞至元件。 此節中的範例主要介紹如何將存取權杖、重新整理權杖和防止偽造要求 (XSRF) 權杖傳遞至 Blazor 應用程式,但此方法適用於其他 HTTP 內容狀態。

注意

在元件 POST 至 Identity 或其他需要驗證之端點的情節下,將 XSRF 權杖傳遞至 Razor 元件會很有用。 如果您的應用程式只需要存取權杖和重新整理權杖,您可以從下列範例中移除 XSRF 權杖程式碼。

使用一般 Razor Pages 或 MVC 應用程式來驗證應用程式。 將權杖佈建並儲存至驗證 cookie。

Program 檔案中:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});

Startup.cs 中:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});

Startup.cs 中:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});

您可以選擇性地使用 options.Scope.Add("{SCOPE}"); 來新增其他範圍,其中預留位置 {SCOPE} 是要新增的其他範圍。

定義可在 Blazor 應用程式中用來解析來自相依性插入 (DI) 之權杖的限定範圍權杖提供者服務。

TokenProvider.cs

public class TokenProvider
{
    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
    public string? XsrfToken { get; set; }
}

Program 檔案中,新增下列項目的服務:

  • IHttpClientFactory:用於使用存取權杖從伺服器 API 取得天氣資料的 WeatherForecastService 類別。
  • TokenProvider:保留存取權杖和重新整理權杖。
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();

Startup.csStartup.ConfigureServices 中,新增下列項目的服務:

  • IHttpClientFactory:用於使用存取權杖從伺服器 API 取得天氣資料的 WeatherForecastService 類別。
  • TokenProvider:保留存取權杖和重新整理權杖。
services.AddHttpClient();
services.AddScoped<TokenProvider>();

定義類別,以使用存取權杖和重新整理權杖來傳入初始應用程式狀態。

InitialApplicationState.cs

public class InitialApplicationState
{
    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
    public string? XsrfToken { get; set; }
}

Pages/_Host.cshtml 檔案中,建立 InitialApplicationState 的執行個體,並將它當做參數傳遞給應用程式:

Pages/_Layout.cshtml 檔案中,建立 InitialApplicationState 的執行個體,並將它當做參數傳遞給應用程式:

Pages/_Host.cshtml 檔案中,建立 InitialApplicationState 的執行個體,並將它當做參數傳遞給應用程式:

@using Microsoft.AspNetCore.Authentication
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf

...

@{
    var tokens = new InitialApplicationState
    {
        AccessToken = await HttpContext.GetTokenAsync("access_token"),
        RefreshToken = await HttpContext.GetTokenAsync("refresh_token"),
        XsrfToken = Xsrf.GetAndStoreTokens(HttpContext).RequestToken
    };
}

<component ... param-InitialState="tokens" ... />

App 元件 (App.razor) 中,解析服務並使用參數的資料將其初始化:

@inject TokenProvider TokenProvider

...

@code {
    [Parameter]
    public InitialApplicationState? InitialState { get; set; }

    protected override Task OnInitializedAsync()
    {
        TokenProvider.AccessToken = InitialState?.AccessToken;
        TokenProvider.RefreshToken = InitialState?.RefreshToken;
        TokenProvider.XsrfToken = InitialState?.XsrfToken;

        return base.OnInitializedAsync();
    }
}

注意

將初始狀態指派給上述範例中之 TokenProvider 的替代方法是將資料複製到 OnInitializedAsync 內的限定範圍服務,以便在整個應用程式中使用。

Microsoft.AspNet.WebApi.Client NuGet 封裝的應用程式新增封裝參考。

注意

如需將套件新增至 .NET 應用程式的指引,請參閱在套件取用工作流程 (NuGet 文件)安裝及管理套件底下的文章。 在 NuGet.org 確認正確的套件版本。

在提出安全 API 要求的服務中,插入權杖提供者並擷取 API 要求的權杖:

WeatherForecastService.cs

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class WeatherForecastService
{
    private readonly HttpClient http;
    private readonly TokenProvider tokenProvider;

    public WeatherForecastService(IHttpClientFactory clientFactory, 
        TokenProvider tokenProvider)
    {
        http = clientFactory.CreateClient();
        this.tokenProvider = tokenProvider;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        var token = tokenProvider.AccessToken;
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://localhost:5003/WeatherForecast");
        request.Headers.Add("Authorization", $"Bearer {token}");
        var response = await http.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ?? 
            Array.Empty<WeatherForecast>();
    }
}

針對傳遞至元件的 XSRF 權杖,請插入 TokenProvider 並將 XSRF 權杖新增至 POST 要求。 下列範例會將權杖新增至登出端點 POST。 下列範例的情節是登出端點 (Areas/Identity/Pages/Account/Logout.cshtml已在應用程式中產生架構) 不會指定 IgnoreAntiforgeryTokenAttribute (@attribute [IgnoreAntiforgeryToken]),因為它除了執行必須保護的正常登出作業之外,還會執行一些動作。 端點需要有效的 XSRF 權杖才能成功處理要求。

在向已授權的使用者顯示 [登出] 按鈕的元件中:

@inject TokenProvider TokenProvider

...

<AuthorizeView>
    <Authorized>
        <form action="/Identity/Account/Logout?returnUrl=%2F" method="post">
            <button class="nav-link btn btn-link" type="submit">Logout</button>
            <input name="__RequestVerificationToken" type="hidden" 
                value="@TokenProvider.XsrfToken">
        </form>
    </Authorized>
    <NotAuthorized>
        ...
    </NotAuthorized>
</AuthorizeView>

設定驗證配置

對於使用多個驗證中介軟體因而具有多個驗證配置的應用程式,可以在 Program 檔案的端點設定中明確設定 Blazor 所使用的配置。 下列範例會設定 OpenID Connect (OIDC) 配置:

對於使用多個驗證中介軟體因而具有多個驗證配置的應用程式,可以在 Startup.cs 的端點設定中明確設定 Blazor 所使用的配置。 下列範例會設定 OpenID Connect (OIDC) 配置:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;

...

app.MapRazorComponents<App>().RequireAuthorization(
    new AuthorizeAttribute
    {
        AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
    })
    .AddInteractiveServerRenderMode();
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

...

app.MapBlazorHub().RequireAuthorization(
    new AuthorizeAttribute 
    {
        AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
    });

對於使用多個驗證中介軟體因而具有多個驗證配置的應用程式,可以在 Startup.Configure 的端點設定中明確設定 Blazor 所使用的配置。 下列範例會設定 Microsoft Entra ID 配置:

endpoints.MapBlazorHub().RequireAuthorization(
    new AuthorizeAttribute 
    {
        AuthenticationSchemes = AzureADDefaults.AuthenticationScheme
    });

使用 OpenID Connect (OIDC) v2.0 端點

在 ASP.NET Core 5.0 之前的版本中,驗證程式庫和 Blazor 範本會使用 OpenID Connect (OIDC) v1.0 端點。 若要在 ASP.NET Core 5.0 之前的版本中使用 v2.0 端點,請在 OpenIdConnectOptions 中設定 OpenIdConnectOptions.Authority 選項:

services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, 
    options =>
    {
        options.Authority += "/v2.0";
    }

或者,您可以在應用程式設定 (appsettings.json) 檔案中進行設定:

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/common/oauth2/v2.0/",
    ...
  }
}

如果將區段附加到授權單位不適合應用程式的 OIDC 提供者 (例如使用非 ME 識別碼的提供者),請直接設定 Authority 屬性。 使用 Authority 索引鍵在 OpenIdConnectOptions 或應用程式設定檔案中設定屬性。

程式碼變更

  • 識別碼權杖中的宣告清單會針對 v2.0 端點有所變更。 變更的 Microsoft 文件已淘汰,但識別碼權杖參考中提供了與識別碼權杖中的宣告有關的指引。

  • 由於資源已在 v2.0 端點的範圍 URI 中指定,因此請移除 OpenIdConnectOptions 中的 OpenIdConnectOptions.Resource 屬性設定:

    services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => 
        {
            ...
            options.Resource = "...";    // REMOVE THIS LINE
            ...
        }
    

應用程式識別碼 URI

  • 使用 v2.0 端點時,API 會定義 App ID URI,用來代表 API 的唯一識別碼。
  • 所有範圍都包含 App ID URI 作為前置詞,而且 v2.0 端點會以 App ID URI 作為對象發出存取權杖。
  • 使用 V2.0 端點時,伺服器 API 中所設定的用戶端識別碼會從 API 應用程式識別碼 (用戶端識別碼) 變更為 App ID URI。

appsettings.json

{
  "AzureAd": {
    ...
    "ClientId": "https://{TENANT}.onmicrosoft.com/{PROJECT NAME}"
    ...
  }
}

您可以在 OIDC 提供者應用程式註冊描述中,找到要使用的 App ID URI。

用來為自訂服務擷取使用者的線路處理常式

使用 CircuitHandlerAuthenticationStateProvider 擷取使用者,並在服務中設定使用者。 如果您要更新使用者,請將回撥註冊到 AuthenticationStateChanged 並將 Task 排入佇列,以取得新的使用者並更新服務。 下列範例會示範此方法。

在以下範例中:

  • 每次線路重新連線時都會呼叫 OnConnectionUpAsync,為連線的存留期設定使用者。 除非您透過處理常式來實作驗證變更的更新 (下列範例中的 AuthenticationChanged),否則只需要 OnConnectionUpAsync 方法。
  • 呼叫 OnCircuitOpenedAsync 以附加驗證變更的處理常式 AuthenticationChanged,以更新使用者。
  • UpdateAuthentication 工作的 catch 區塊不會對例外狀況採取任何動作,因為目前無法在執行程式碼時將它們回報。 如果有例外狀況從工作擲回,則會在應用程式中的其他位置回報例外狀況。

UserService.cs

using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;

public class UserService
{
    private ClaimsPrincipal currentUser = new(new ClaimsIdentity());

    public ClaimsPrincipal GetUser()
    {
        return currentUser;
    }

    internal void SetUser(ClaimsPrincipal user)
    {
        if (currentUser != user)
        {
            currentUser = user;
        }
    }
}

internal sealed class UserCircuitHandler : CircuitHandler, IDisposable
{
    private readonly AuthenticationStateProvider authenticationStateProvider;
    private readonly UserService userService;

    public UserCircuitHandler(
        AuthenticationStateProvider authenticationStateProvider,
        UserService userService)
    {
        this.authenticationStateProvider = authenticationStateProvider;
        this.userService = userService;
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        authenticationStateProvider.AuthenticationStateChanged += 
            AuthenticationChanged;

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    private void AuthenticationChanged(Task<AuthenticationState> task)
    {
        _ = UpdateAuthentication(task);

        async Task UpdateAuthentication(Task<AuthenticationState> task)
        {
            try
            {
                var state = await task;
                userService.SetUser(state.User);
            }
            catch
            {
            }
        }
    }

    public override async Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        var state = await authenticationStateProvider.GetAuthenticationStateAsync();
        userService.SetUser(state.User);
    }

    public void Dispose()
    {
        authenticationStateProvider.AuthenticationStateChanged -= 
            AuthenticationChanged;
    }
}
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;

public class UserService
{
    private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());

    public ClaimsPrincipal GetUser()
    {
        return currentUser;
    }

    internal void SetUser(ClaimsPrincipal user)
    {
        if (currentUser != user)
        {
            currentUser = user;
        }
    }
}

internal sealed class UserCircuitHandler : CircuitHandler, IDisposable
{
    private readonly AuthenticationStateProvider authenticationStateProvider;
    private readonly UserService userService;

    public UserCircuitHandler(
        AuthenticationStateProvider authenticationStateProvider,
        UserService userService)
    {
        this.authenticationStateProvider = authenticationStateProvider;
        this.userService = userService;
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        authenticationStateProvider.AuthenticationStateChanged += 
            AuthenticationChanged;

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    private void AuthenticationChanged(Task<AuthenticationState> task)
    {
        _ = UpdateAuthentication(task);

        async Task UpdateAuthentication(Task<AuthenticationState> task)
        {
            try
            {
                var state = await task;
                userService.SetUser(state.User);
            }
            catch
            {
            }
        }
    }

    public override async Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        var state = await authenticationStateProvider.GetAuthenticationStateAsync();
        userService.SetUser(state.User);
    }

    public void Dispose()
    {
        authenticationStateProvider.AuthenticationStateChanged -= 
            AuthenticationChanged;
    }
}

Program 檔案中:

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

...

builder.Services.AddScoped<UserService>();
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());

Startup.csStartup.ConfigureServices 中:

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

...

services.AddScoped<UserService>();
services.TryAddEnumerable(
    ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());

使用元件中的服務來取得使用者:

@inject UserService UserService

<h1>Hello, @(UserService.GetUser().Identity?.Name ?? "world")!</h1>

若要在 MVC、Razor Pages 和其他 ASP.NET Core 情節的中介軟體中設定使用者,請在驗證中介軟體執行之後呼叫自訂中介軟體中 UserService 上的 SetUser,或以 IClaimsTransformation 實作來設定使用者。 下列範例採用中介軟體方法。

UserServiceMiddleware.cs

public class UserServiceMiddleware
{
    private readonly RequestDelegate next;

    public UserServiceMiddleware(RequestDelegate next)
    {
        this.next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task InvokeAsync(HttpContext context, UserService service)
    {
        service.SetUser(context.User);
        await next(context);
    }
}

在即將呼叫 Program 檔案中的 app.MapRazorComponents<App>() 之前,呼叫中介軟體:

在即將呼叫 Program 檔案中的 app.MapBlazorHub() 之前,呼叫中介軟體:

在即將呼叫 Startup.csStartup.Configure 中的 app.MapBlazorHub() 之前,呼叫中介軟體:

app.UseMiddleware<UserServiceMiddleware>();

在傳出要求中介軟體中存取 AuthenticationStateProvider

IHttpClientFactory 建立之 HttpClientDelegatingHandler 中的 AuthenticationStateProvider,可以使用線路活動處理常式在傳出要求中介軟體中存取。

注意

如需取得一般指引,了解如何在 ASP.NET Core 應用程式中使用 IHttpClientFactory 建立的 HttpClient 執行個體來定義 HTTP 要求的委派處理常式,請參閱在 ASP.NET Core 中使用 IHttpClientFactory 提出 HTTP 要求 (部分機器翻譯) 的下列各節內容:

下列範例使用 AuthenticationStateProvider 將已驗證使用者的自訂使用者名稱標頭附加至傳出要求。

首先,實作 Blazor 相依性插入 (DI) 文章下列部分中的 CircuitServicesAccessor 類別:

從不同的 DI 範圍存取伺服器端 Blazor 服務 (部分機器翻譯)

使用 CircuitServicesAccessor 存取 DelegatingHandler 實作中的 AuthenticationStateProvider

AuthenticationStateHandler.cs

public class AuthenticationStateHandler : DelegatingHandler
{
    readonly CircuitServicesAccessor circuitServicesAccessor;

    public AuthenticationStateHandler(
        CircuitServicesAccessor circuitServicesAccessor)
    {
        this.circuitServicesAccessor = circuitServicesAccessor;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var authStateProvider = circuitServicesAccessor.Services
            .GetRequiredService<AuthenticationStateProvider>();
        var authState = await authStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            request.Headers.Add("X-USER-IDENTITY-NAME", user.Identity.Name);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Program 檔案中,註冊 AuthenticationStateHandler 並將處理常式新增至建立 HttpClient 執行個體的 IHttpClientFactory

builder.Services.AddTransient<AuthenticationStateHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<AuthenticationStateHandler>();