Další scénáře zabezpečení na straně serveru ASP.NET Core Blazor

Poznámka:

Toto není nejnovější verze tohoto článku. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Důležité

Tyto informace se týkají předběžného vydání produktu, který může být podstatně změněn před komerčním vydáním. Microsoft neposkytuje žádné záruky, výslovné ani předpokládané, týkající se zde uváděných informací.

Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Tento článek vysvětluje, jak nakonfigurovat serverovou stranu Blazor pro další scénáře zabezpečení, včetně způsobu předávání tokenů do Blazor aplikace.

Poznámka:

Příklady kódu v tomto článku přijímají referenční typy s možnou hodnotou null (NRT) a statickou analýzu stavu null-stav kompilátoru .NET, které jsou podporovány v ASP.NET Core v .NET 6 nebo novější. Pokud cílíte na ASP.NET Core 5.0 nebo starší, odeberte z WeatherForecast[]?string?TodoItem[]?příkladů článku označení typu null (?) a IEnumerable<GitHubBranch>? typy.

Předání tokenů do aplikace na straně Blazor serveru

Tokeny dostupné mimo Razor komponenty v aplikaci na straně Blazor serveru je možné předat komponentám pomocí přístupu popsaného v této části. Příklad v této části se zaměřuje na předávání tokenů Blazor XSRF (access, refresh a anti-request forgery), ale přístup je platný pro jiný stav kontextu HTTP.

Poznámka:

Předání tokenu Razor XSRF komponentám je užitečné ve scénářích, kdy komponenty POST do Identity nebo jiných koncových bodů, které vyžadují ověření. Pokud vaše aplikace vyžaduje pouze přístup a obnovovací tokeny, můžete z následujícího příkladu odebrat kód tokenu XSRF.

Ověřte aplikaci stejně jako u běžné Razor aplikace Pages nebo MVC. Zřiďte a uložte tokeny do ověřování cookie.

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

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

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

Volitelně jsou přidány další obory s options.Scope.Add("{SCOPE}");, kde zástupný symbol {SCOPE} je další obor, který chcete přidat.

Definujte službu zprostředkovatele tokenů s vymezeným oborem, kterou lze použít v aplikaci Blazor k překladu tokenů z injektáže závislostí (DI).

TokenProvider.cs:

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

Program V souboru přidejte služby pro:

  • IHttpClientFactory: Používá se WeatherForecastService ve třídě, která získává data o počasí z rozhraní API serveru s přístupovým tokenem.
  • TokenProvider: Uchovává přístupové a obnovovací tokeny.
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();

In Startup.ConfigureServices of Startup.cs, add services for:

  • IHttpClientFactory: Používá se WeatherForecastService ve třídě, která získává data o počasí z rozhraní API serveru s přístupovým tokenem.
  • TokenProvider: Uchovává přístupové a obnovovací tokeny.
services.AddHttpClient();
services.AddScoped<TokenProvider>();

Definujte třídu pro předání počátečního stavu aplikace pomocí přístupových a obnovovacích tokenů.

InitialApplicationState.cs:

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

Pages/_Host.cshtml V souboru vytvořte a InitialApplicationState předejte ji jako parametr aplikaci:

Pages/_Layout.cshtml V souboru vytvořte a InitialApplicationState předejte ji jako parametr aplikaci:

Pages/_Host.cshtml V souboru vytvořte a InitialApplicationState předejte ji jako parametr aplikaci:

@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" ... />

V komponentě App (App.razor) přeložte službu a inicializujete ji daty z parametru:

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

Poznámka:

Alternativou k přiřazení počátečního stavu k TokenProvider předchozímu příkladu je zkopírování dat do služby s vymezeným oborem pro OnInitializedAsync použití v rámci aplikace.

Přidejte do aplikace Microsoft.AspNet.WebApi.Client odkaz na balíček NuGet.

Poznámka:

Pokyny k přidávání balíčků do aplikací .NET najdete v článcích v části Instalace a správa balíčků na webu Pracovní postup používání balíčků (dokumentace k NuGetu). Ověřte správné verze balíčků na NuGet.org.

Ve službě, která vytvoří požadavek na zabezpečené rozhraní API, vkněte zprostředkovatele tokenu a načtěte token pro požadavek rozhraní 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>();
    }
}

V případě tokenu XSRF předaného komponentě vložte TokenProvider token XSRF a přidejte ho do požadavku POST. Následující příklad přidá token do koncového bodu odhlášení POST. Scénář pro následující příklad spočívá v tom, že koncový bod odhlášení (Areas/Identity/Pages/Account/Logout.cshtmlvygenerovaný do aplikace) nezadá IgnoreAntiforgeryTokenAttribute (@attribute [IgnoreAntiforgeryToken]), protože provádí určitou akci kromě normální operace odhlášení, která musí být chráněna. Koncový bod vyžaduje k úspěšnému zpracování požadavku platný token XSRF.

V komponentě, která představuje tlačítko odhlášení autorizovaným uživatelům:

@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>

Nastavení schématu ověřování

Pro aplikaci, která používá více než jeden middleware ověřování a má tedy více než jedno schéma ověřování, lze schéma, které Blazor používá, explicitně nastavit v konfiguraci koncového Program bodu souboru. Následující příklad nastaví schéma OpenID Připojení (OIDC):

Pro aplikaci, která používá více než jeden middleware ověřování, a proto má více než jedno schéma ověřování, schéma, které Blazor se používá, lze explicitně nastavit v konfiguraci koncového Startup.csbodu . Následující příklad nastaví schéma OpenID Připojení (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
    });

Pro aplikaci, která používá více než jeden middleware ověřování, a proto má více než jedno schéma ověřování, schéma, které Blazor se používá, lze explicitně nastavit v konfiguraci koncového Startup.Configurebodu . Následující příklad nastaví schéma MICROSOFT Entra ID:

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

Použití koncových bodů OpenID Připojení (OIDC) v2.0

Ve verzích ASP.NET Core starších než 5.0 používají knihovna ověřování a Blazor šablony koncové body OpenID Připojení (OIDC) v1.0. Pokud chcete použít koncový bod verze 2.0 s verzemi ASP.NET Core před 5.0, nakonfigurujte OpenIdConnectOptions.Authority možnost v :OpenIdConnectOptions

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

Případně můžete nastavení provést v souboru nastavení aplikace (appsettings.json):

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

Pokud přiřakání segmentu autoritě není vhodné pro poskytovatele OIDC aplikace, například u jiných poskytovatelů než ME-ID, nastavte Authority vlastnost přímo. Buď nastavte vlastnost v OpenIdConnectOptions souboru nastavení aplikace nebo v klíči Authority .

Změny kódu

  • Seznam deklarací identity v tokenu ID se změní pro koncové body v2.0. Dokumentace Microsoftu ke změnám byla vyřazena, ale pokyny k deklaracím identity v tokenu ID jsou k dispozici v referenčních informacích k deklaracím tokenů ID.

  • Vzhledem k tomu, že prostředky jsou zadané v identifikátorech URI oboru pro koncové body v2.0, odeberte OpenIdConnectOptions.Resource nastavení vlastnosti v OpenIdConnectOptions:

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

Identifikátor URI ID aplikace

  • Při použití koncových bodů v2.0 rozhraní API definují App ID URIrozhraní API , která má představovat jedinečný identifikátor rozhraní API.
  • Všechny obory zahrnují identifikátor URI ID aplikace jako předponu a koncové body verze 2.0 generují přístupové tokeny s identifikátorem URI ID aplikace jako cílovou skupinou.
  • Při použití koncových bodů V2.0 se ID klienta nakonfigurované v rozhraní API serveru změní z ID aplikace rozhraní API (ID klienta) na identifikátor URI ID aplikace aplikace.

appsettings.json:

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

Identifikátor URI ID aplikace, který se má použít, najdete v popisu registrace aplikace poskytovatele OIDC.

Obslužná rutina okruhu pro zachycení uživatelů pro vlastní služby

CircuitHandler Použijte k zachycení uživatele ze AuthenticationStateProvider služby a nastavení uživatele ve službě. Pokud chcete aktualizovat uživatele, zaregistrujte zpětná volání do AuthenticationStateChanged fronty a Task získejte nového uživatele a aktualizujte službu. Následující příklad ukazuje přístup.

V následujícím příkladu:

  • OnConnectionUpAsync se volá při každém opětovném připojení okruhu a nastavení uživatele po dobu životnosti připojení. OnConnectionUpAsync Pouze metoda se vyžaduje, pokud neimplementujete aktualizace prostřednictvím obslužné rutiny pro změny ověřování (AuthenticationChangedv následujícím příkladu).
  • OnCircuitOpenedAsync je volána pro připojení ověřovací změněné obslužné rutiny , AuthenticationChangedaktualizovat uživatele.
  • Blok catchUpdateAuthentication úlohy nemá žádnou akci na výjimky, protože v tomto okamžiku není způsob, jak je v tomto okamžiku vyhlásit. Pokud je z úlohy vyvoláná výjimka, je výjimka hlášena jinde v aplikaci.

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

V souboru Program:

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

...

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

In Startup.ConfigureServices of Startup.cs:

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

...

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

Pomocí služby v komponentě získejte uživatele:

@inject UserService UserService

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

Pokud chcete nastavit uživatele v middlewaru pro MVC, Razor Pages a v jiných scénářích ASP.NET Core, zavolejte SetUserUserService ve vlastním middlewaru po spuštění middlewaru ověřování nebo nastavte uživatele s implementací IClaimsTransformation . Následující příklad přijímá přístup middlewaru.

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

Bezprostředně před voláním app.MapRazorComponents<App>() do Program souboru zavolejte middleware:

Bezprostředně před voláním app.MapBlazorHub() do Program souboru zavolejte middleware:

Bezprostředně před voláním app.MapBlazorHub()Startup.ConfigureStartup.csdo , zavolejte middleware:

app.UseMiddleware<UserServiceMiddleware>();

Přístup AuthenticationStateProvider k middlewaru odchozích požadavků

K AuthenticationStateProvider middlewaru DelegatingHandler odchozích požadavků se dá přistupovat z příkazu for HttpClient created with IHttpClientFactory a circuit activity handler.

Poznámka:

Obecné pokyny k definování delegování obslužných rutin pro požadavky HTTP podle HttpClient instancí vytvořených IHttpClientFactory v aplikacích ASP.NET Core najdete v následujících částech Vytváření požadavků HTTP pomocí IHttpClientFactory v ASP.NET Core:

Následující příklad používá AuthenticationStateProvider k připojení vlastní hlavičky uživatelského jména pro ověřené uživatele k odchozím požadavkům.

Nejprve implementujte CircuitServicesAccessor třídu v následující části Blazor článku injektáž závislostí (DI):

Přístup ke službám na straně Blazor serveru z jiného oboru DI

CircuitServicesAccessor Použijte k přístupu k implementaci AuthenticationStateProviderDelegatingHandler.

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 V souboru zaregistrujte AuthenticationStateHandler a přidejte obslužnou rutinu do IHttpClientFactory instance, která vytváří HttpClient instance:

builder.Services.AddTransient<AuthenticationStateHandler>();

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