Дополнительные сценарии безопасности на стороне сервера ASP.NET Core Blazor

Примечание.

Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.

Внимание

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

В текущем выпуске см . версию .NET 8 этой статьи.

В этой статье объясняется, как настроить серверную сторону Blazor для дополнительных сценариев безопасности, включая передачу маркеров Blazor в приложение.

Примечание.

Примеры кода в этой статье используют типы ссылок, допускающие значение NULL (NRTs) и статический анализ состояния .NET компилятора NULL, которые поддерживаются в ASP.NET Core в .NET 6 или более поздней версии. При назначении ASP.NET Core 5.0 или более ранних версий удалите обозначение? типа NULL () из string?, TodoItem[]?WeatherForecast[]?и IEnumerable<GitHubBranch>? типы в примерах статьи.

Передача маркеров в серверное Blazor приложение

Обновление этого раздела для Blazor веб-приложения ожидается обновление раздела об передаче маркеров в Blazor веб-приложения (dotnet/AspNetCore.Docs#31691). Дополнительные сведения см. в разделе "Проблема предоставления маркера доступа к HttpClient" в интерактивном режиме сервера (dotnet/aspnetcore #52390).

Для Blazor Serverэтого раздела см. версию 7.0 этой статьи.

Маркеры, доступные за пределами Razor компонентов в серверном Blazor приложении, можно передать компонентам с помощью подхода, описанного в этом разделе. В примере этого раздела основное внимание уделяется передаче маркеров Blazor маркеров маркера XSRF, обновлению и защите от запросов (XSRF), но этот подход действителен для другого состояния контекста HTTP.

Примечание.

Передача маркера Razor XSRF компонентам полезна в сценариях, когда компоненты POST Identity или другие конечные точки, требующие проверки. Если приложению требуются только маркеры доступа и обновления, можно удалить код маркера 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: используется в WeatherForecastService классе, который получает данные о погоде из API сервера с маркером доступа.
  • TokenProvider: содержит маркеры доступа и обновления.
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();

В Startup.ConfigureServices этом Startup.csслучае добавьте службы для:

  • IHttpClientFactory: используется в WeatherForecastService классе, который получает данные о погоде из API сервера с маркером доступа.
  • 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 приведенному выше примеру является копирование данных в службу область d для использования в OnInitializedAsync приложении.

Добавьте в приложение ссылку на пакет NuGet Microsoft.AspNet.WebApi.Client.

Примечание.

Рекомендации по добавлению пакетов в приложения .NET см. в разделе Способы установки пакетов NuGet в статье Рабочий процесс использования пакета (документация по 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. Сценарий следующего примера заключается в том, что конечная точка выхода (с шаблонами в приложение) не указывает IgnoreAntiforgeryTokenAttribute (Areas/Identity/Pages/Account/Logout.cshtml@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>

Настройка схемы проверки подлинности

Для приложения, использующего несколько по промежуточному слоям проверки подлинности, и поэтому имеет несколько схем проверки подлинности, схема, которая Blazor используется, может быть явно задана в конфигурации конечной Program точки файла. В следующем примере задается схема OpenID Connect (OIDC):

Для приложения, использующего более одного ПО промежуточного слоя для проверки подлинности и, таким образом, имеющего несколько схем проверки подлинности, схему, которую использует Blazor, можно явно задать в конфигурации конечной точки Startup.cs. В следующем примере задается схема 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
    });

Для приложения, использующего более одного ПО промежуточного слоя для проверки подлинности и, таким образом, имеющего несколько схем проверки подлинности, схему, которую использует Blazor, можно явно задать в конфигурации конечной точки Startup.Configure. В следующем примере задается схема идентификатора Microsoft Entra ID:

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

Использование конечных точек OpenID Connect (OIDC) версии 2.0

До версии ASP.NET Core 5.0 в библиотеке проверки подлинности и шаблонах Blazor используются конечные точки OpenID Connect (OIDC) версии 1.0. Чтобы использовать конечную точку версии 2.0 с версиями ASP.NET Core до 5.0, настройте параметр OpenIdConnectOptions.Authority в OpenIdConnectOptions.

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

Кроме того, параметр можно задать в файле параметров приложения (appsettings.json):

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

Если атака на сегмент в центр не подходит для поставщика OIDC приложения, например с поставщиками, не относящихся к ME-ID, задайте свойство напрямую Authority . Задайте свойство либо в OpenIdConnectOptions, либо в файле параметров приложения с помощью ключа Authority.

Изменения в коде

  • Список утверждений в маркере идентификатора отличается для конечных точек версии 2.0. Документация Майкрософт по изменениям была прекращена, но рекомендации по утверждениям в маркере идентификатора доступны в справочнике по утверждениям маркера идентификатора.

  • Поскольку ресурсы указываются в URI области для конечных точек версии 2.0, удалите параметр OpenIdConnectOptions.Resource свойства в OpenIdConnectOptions.

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

URI идентификатора приложения

  • При использовании конечных точек версии 2.0 в интерфейсах API определяется код App ID URI, который должен представлять уникальный идентификатор интерфейса API.
  • Универсальный код ресурса (URI) идентификатора приложения включается во все области в качестве префикса, а конечные точки версии 2.0 выдают маркеры доступа с кодом URI идентификатора приложения в качестве аудитории.
  • При использовании конечных точек версии 2.0 идентификатор клиента, настроенный в API сервера, изменяется с идентификатора приложения API (идентификатора клиента) на код URI идентификатора приложения.

appsettings.json:

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

URI идентификатора приложения для использования находится в описании регистрации приложения поставщика OIDC.

Обработчик каналов для записи пользователей для пользовательских служб

Используйте для CircuitHandler записи пользователя из AuthenticationStateProvider службы и задания пользователя в службе. Если вы хотите обновить пользователя, зарегистрируйте обратный AuthenticationStateChanged вызов и в очереди Task , чтобы получить нового пользователя и обновить службу. Такой подход демонстрируется в приведенном ниже примере.

В следующем примере :

  • OnConnectionUpAsync вызывается каждый раз, когда канал повторно подключается, задав пользователю время существования подключения. OnConnectionUpAsync Этот метод требуется только в том случае, если вы не реализуете обновления с помощью обработчика изменений проверки подлинности (AuthenticationChangedв следующем примере).
  • OnCircuitOpenedAsync вызывается для подключения обработчика измененной проверки подлинности, AuthenticationChangedчтобы обновить пользователя.
  • Блок catchUpdateAuthentication задачи не выполняет никаких действий по исключениям, так как на этом этапе выполнения кода невозможно сообщить о них. Если исключение создается из задачи, исключение сообщается в другом месте приложения.

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.ConfigureServices в файле Startup.cs:

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, вызовите SetUserUserService пользовательское ПО промежуточного слоя после запуска ПО промежуточного слоя проверки подлинности или задайте пользователю реализацию 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);
    }
}

Непосредственно перед вызовом app.MapRazorComponents<App>() в файле вызовите ПО промежуточного Program слоя:

Непосредственно перед вызовом app.MapBlazorHub() в файле вызовите ПО промежуточного Program слоя:

Непосредственно перед вызовом app.MapBlazorHub() в Startup.Configure , вызов ПО промежуточного Startup.csслоя:

app.UseMiddleware<UserServiceMiddleware>();

Доступ AuthenticationStateProvider к по промежуточному по промежуточному слоям исходящего запроса

С AuthenticationStateProvider помощью обработчика действий канала можно получить доступ к созданному с IHttpClientFactory помощью по промежуточного DelegatingHandlerHttpClient слоя исходящего запроса.

Примечание.

Общие рекомендации по определению делегирования обработчиков http-запросов HttpClient экземплярами, созданными в IHttpClientFactory приложениях ASP.NET Core, см. в следующих разделах : "Создание HTTP-запросов с помощью IHttpClientFactory" в ASP.NET Core:

В следующем примере используется AuthenticationStateProvider подключение пользовательского заголовка имени пользователя для прошедших проверку подлинности пользователей к исходящим запросам.

Сначала реализуйте CircuitServicesAccessor класс в следующем разделе статьи Blazor внедрения зависимостей (DI):

Доступ к службам на стороне Blazor сервера из другой область

CircuitServicesAccessor Используйте для доступа к AuthenticationStateProvider реализации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 В файле зарегистрируйте AuthenticationStateHandler обработчик и добавьте его в IHttpClientFactory создаваемые HttpClient экземпляры:

builder.Services.AddTransient<AuthenticationStateHandler>();

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