Dodatkowe scenariusze zabezpieczeń po stronie serwera ASP.NET Core Blazor

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

W tym artykule wyjaśniono, jak skonfigurować po stronie Blazor serwera dodatkowe scenariusze zabezpieczeń, w tym sposób przekazywania tokenów do Blazor aplikacji.

Uwaga

Przykłady kodu w tym artykule przyjmują typy odwołań dopuszczających wartość null (NRTs) i statyczną analizę stanu null kompilatora platformy .NET, które są obsługiwane w programie ASP.NET Core na platformie .NET 6 lub nowszym. W przypadku określania wartości docelowej ASP.NET Core 5.0 lub starszej usuń oznaczenie typu null (?) z string?typów , TodoItem[]?, WeatherForecast[]?i IEnumerable<GitHubBranch>? w przykładach artykułu.

Przekazywanie tokenów do aplikacji po stronie Blazor serwera

Tokeny dostępne poza Razor składnikami aplikacji po stronie Blazor serwera można przekazać do składników przy użyciu podejścia opisanego w tej sekcji. W przykładzie w tej sekcji skoncentrowano się na przekazywaniu tokenów tokenów tokenówBlazor (XSRF) dostępu, odświeżania i ochrony przed żądaniami (XSRF), ale podejście jest prawidłowe dla innego stanu kontekstu HTTP.

Uwaga

Przekazywanie tokenu XSRF do Razor składników jest przydatne w scenariuszach, w których składniki POST do Identity lub innych punktów końcowych, które wymagają weryfikacji. Jeśli aplikacja wymaga tylko tokenów dostępu i odświeżania, możesz usunąć kod tokenu XSRF z poniższego przykładu.

Uwierzytelnij aplikację tak, jak w przypadku zwykłych Razor stron lub aplikacji MVC. Aprowizuj i zapisz tokeny w uwierzytelnieniu cookie.

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

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

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

Opcjonalnie dodatkowe zakresy są dodawane z elementem options.Scope.Add("{SCOPE}");, gdzie symbol zastępczy {SCOPE} jest dodatkowym zakresem do dodania.

Zdefiniuj usługę dostawcy tokenów o określonym zakresie , która może być używana w Blazor aplikacji do rozpoznawania tokenów z iniekcji zależności (DI).

TokenProvider.cs:

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

Program W pliku dodaj usługi dla:

  • IHttpClientFactory: używany w WeatherForecastService klasie, która uzyskuje dane pogodowe z interfejsu API serwera z tokenem dostępu.
  • TokenProvider: przechowuje tokeny dostępu i odświeżania.
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();

W Startup.ConfigureServices pliku Startup.csdodaj usługi dla:

  • IHttpClientFactory: używany w WeatherForecastService klasie, która uzyskuje dane pogodowe z interfejsu API serwera z tokenem dostępu.
  • TokenProvider: przechowuje tokeny dostępu i odświeżania.
services.AddHttpClient();
services.AddScoped<TokenProvider>();

Zdefiniuj klasę do przekazania początkowego stanu aplikacji przy użyciu tokenów dostępu i odświeżania.

InitialApplicationState.cs:

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

Pages/_Host.cshtml W pliku utwórz i wystąpienie InitialApplicationState i przekaż go jako parametr do aplikacji:

Pages/_Layout.cshtml W pliku utwórz i wystąpienie InitialApplicationState i przekaż go jako parametr do aplikacji:

Pages/_Host.cshtml W pliku utwórz i wystąpienie InitialApplicationState i przekaż go jako parametr do aplikacji:

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

W składniku App (App.razor) rozwiąż usługę i zainicjuj ją przy użyciu danych 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();
    }
}

Uwaga

Alternatywą do przypisania stanu początkowego do TokenProvider poprzedniego przykładu jest skopiowanie danych do usługi OnInitializedAsync o określonym zakresie w celu użycia w całej aplikacji.

Dodaj odwołanie do pakietu do aplikacji dla Microsoft.AspNet.WebApi.Client pakietu NuGet.

Uwaga

Aby uzyskać instrukcje dodawania pakietów do aplikacji .NET, zobacz artykuły w sekcji Instalowanie pakietów i zarządzanie nimi w temacie Przepływ pracy użycia pakietów (dokumentacja programu NuGet). Sprawdź prawidłowe wersje pakietów pod adresem NuGet.org.

W usłudze, która tworzy bezpieczne żądanie interfejsu API, należy wstrzyknąć dostawcę tokenu i pobrać token dla żądania interfejsu 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>();
    }
}

W przypadku tokenu XSRF przekazanego do składnika wprowadź TokenProvider token XSRF i dodaj go do żądania POST. Poniższy przykład dodaje token do punktu końcowego wylogowania POST. W poniższym przykładzie jest to, że punkt końcowy wylogowania (Areas/Identity/Pages/Account/Logout.cshtmlszkielet z aplikacją) nie określa IgnoreAntiforgeryTokenAttribute elementu (@attribute [IgnoreAntiforgeryToken]), ponieważ wykonuje pewną akcję oprócz normalnej operacji wylogowania, która musi być chroniona. Punkt końcowy wymaga prawidłowego tokenu XSRF, aby pomyślnie przetworzyć żądanie.

W składniku, który przedstawia przycisk Wyloguj się autoryzowanym użytkownikom:

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

Ustawianie schematu uwierzytelniania

W przypadku aplikacji, która używa więcej niż jednego oprogramowania pośredniczącego uwierzytelniania i w związku z tym ma więcej niż jeden schemat uwierzytelniania, schemat, który Blazor używa, można jawnie ustawić w konfiguracji punktu końcowego Program pliku. W poniższym przykładzie ustawiono schemat Połączenie OpenID (OIDC):

W przypadku aplikacji, która używa więcej niż jednego oprogramowania pośredniczącego uwierzytelniania i w związku z tym ma więcej niż jeden schemat uwierzytelniania, schemat, który Blazor używa, można jawnie ustawić w konfiguracji punktu końcowego Startup.csprogramu . W poniższym przykładzie ustawiono schemat Połączenie OpenID (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
    });

W przypadku aplikacji, która używa więcej niż jednego oprogramowania pośredniczącego uwierzytelniania i w związku z tym ma więcej niż jeden schemat uwierzytelniania, schemat, który Blazor używa, można jawnie ustawić w konfiguracji punktu końcowego Startup.Configureprogramu . W poniższym przykładzie ustawiono schemat identyfikatora Entra firmy Microsoft:

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

Używanie punktów końcowych openID Połączenie (OIDC) w wersji 2.0

W wersjach ASP.NET Core wcześniejszych niż 5.0 biblioteka uwierzytelniania i Blazor szablony używają punktów końcowych OpenID Połączenie (OIDC) w wersji 1.0. Aby użyć punktu końcowego w wersji 2.0 z wersjami ASP.NET Core przed wersją 5.0, skonfiguruj OpenIdConnectOptions.Authority opcję w pliku OpenIdConnectOptions:

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

Alternatywnie ustawienie można ustawić w pliku ustawień aplikacji (appsettings.json):

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

Jeśli tacking na segmencie do urzędu nie jest odpowiedni dla dostawcy OIDC aplikacji, na przykład z dostawcami innych niż ME-ID, ustaw Authority właściwość bezpośrednio. Ustaw właściwość w OpenIdConnectOptions pliku ustawień aplikacji lub w pliku ustawień aplikacji za pomocą Authority klucza.

Zmiany kodu

  • Lista oświadczeń w zmianach tokenu identyfikatora dla punktów końcowych w wersji 2.0. Dokumentacja firmy Microsoft dotycząca zmian została wycofana, ale wskazówki dotyczące oświadczeń w tokenie identyfikatora są dostępne w dokumentacji oświadczeń tokenu identyfikatora.

  • Ponieważ zasoby są określone w identyfikatorach URI zakresu dla punktów końcowych w wersji 2.0, usuń OpenIdConnectOptions.Resource ustawienie właściwości w pliku OpenIdConnectOptions:

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

Identyfikator URI identyfikatora aplikacji

  • W przypadku korzystania z punktów końcowych w wersji 2.0 interfejsy API definiują App ID URIelement , który ma reprezentować unikatowy identyfikator interfejsu API.
  • Wszystkie zakresy obejmują identyfikator URI identyfikatora aplikacji jako prefiks, a punkty końcowe w wersji 2.0 emitują tokeny dostępu przy użyciu identyfikatora URI identyfikatora aplikacji jako odbiorców.
  • W przypadku korzystania z punktów końcowych w wersji 2.0 identyfikator klienta skonfigurowany w interfejsie API serwera zmienia się z identyfikatora aplikacji interfejsu API (identyfikator klienta) na identyfikator URI identyfikatora aplikacji.

appsettings.json:

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

Identyfikator URI identyfikatora aplikacji do użycia można znaleźć w opisie rejestracji aplikacji dostawcy OIDC.

Obsługa obwodu w celu przechwytywania użytkowników dla usług niestandardowych

Użyj elementu , CircuitHandler aby przechwycić użytkownika z obiektu AuthenticationStateProvider i ustawić użytkownika w usłudze. Jeśli chcesz zaktualizować użytkownika, zarejestruj wywołanie zwrotne i AuthenticationStateChanged utwórz kolejkę, Task aby uzyskać nowego użytkownika i zaktualizować usługę. W poniższym przykładzie pokazano podejście.

W poniższym przykładzie:

  • OnConnectionUpAsync jest wywoływany za każdym razem, gdy obwód ponownie łączy się, ustawiając użytkownika na okres istnienia połączenia. Tylko metoda jest wymagana OnConnectionUpAsync , chyba że zaimplementujesz aktualizacje za pośrednictwem programu obsługi dla zmian uwierzytelniania (AuthenticationChanged w poniższym przykładzie).
  • OnCircuitOpenedAsync Jest wywoływana w celu dołączenia zmienionej procedury obsługi uwierzytelniania, AuthenticationChanged, w celu zaktualizowania użytkownika.
  • UpdateAuthentication Blok catch zadania nie podejmuje żadnych działań w przypadku wyjątków, ponieważ nie ma możliwości raportowania ich w tym momencie wykonywania kodu. Jeśli wyjątek jest zgłaszany z zadania, wyjątek jest zgłaszany w innym miejscu w aplikacji.

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

W pliku Program:

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

...

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

W Startup.ConfigureServices pliku :Startup.cs

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

...

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

Użyj usługi w składniku, aby uzyskać użytkownika:

@inject UserService UserService

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

Aby ustawić użytkownika w oprogramowania pośredniczącego dla mvC, Razor stron i w innych scenariuszach ASP.NET Core, wywołaj SetUserUserService w niestandardowym oprogramowania pośredniczącego po uruchomieniu oprogramowania pośredniczącego uwierzytelniania lub ustaw użytkownika z implementacją IClaimsTransformation . Poniższy przykład stosuje podejście oprogramowania pośredniczącego.

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

Bezpośrednio przed wywołaniem app.MapRazorComponents<App>() metody w Program pliku wywołaj oprogramowanie pośredniczące:

Bezpośrednio przed wywołaniem app.MapBlazorHub() metody w Program pliku wywołaj oprogramowanie pośredniczące:

Bezpośrednio przed wywołaniem metody app.MapBlazorHub() w Startup.Configure programie Startup.cswywołaj oprogramowanie pośredniczące:

app.UseMiddleware<UserServiceMiddleware>();

Dostęp AuthenticationStateProvider do oprogramowania pośredniczącego żądań wychodzących

Dostęp AuthenticationStateProvider do elementu z DelegatingHandler elementu dla HttpClient utworzonego IHttpClientFactory za pomocą można uzyskać w rozwiązaniu pośredniczącym żądań wychodzących przy użyciu programu obsługi działań obwodu.

Uwaga

Aby uzyskać ogólne wskazówki dotyczące definiowania procedur obsługi delegowania żądań HTTP dla HttpClient wystąpień utworzonych przy użyciu IHttpClientFactory w aplikacjach platformy ASP.NET Core, zobacz następujące sekcje w temacie Make HTTP requests using IHttpClientFactory in ASP.NET Core (Tworzenie żądań HTTP przy użyciu klasy IHttpClientFactory w usłudze ASP.NET Core):

W poniższym przykładzie użyto AuthenticationStateProvider metody dołączania niestandardowego nagłówka nazwy użytkownika dla uwierzytelnionych użytkowników do żądań wychodzących.

Najpierw zaimplementuj klasę CircuitServicesAccessor w następującej Blazor sekcji artykułu wstrzykiwania zależności (DI):

Uzyskiwanie dostępu do usług po stronie Blazor serwera z innego zakresu di

Użyj elementu , CircuitServicesAccessor aby uzyskać dostęp do AuthenticationStateProvider elementu w implementacji DelegatingHandler .

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 W pliku zarejestruj AuthenticationStateHandler program obsługi i dodaj do programu IHttpClientFactory , który tworzy HttpClient wystąpienia:

builder.Services.AddTransient<AuthenticationStateHandler>();

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