Сценарии обеспечения дополнительной безопасности Blazor WebAssembly для ASP.NET Core

Примечание.

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

Внимание

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

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

В этой статье описываются дополнительные сценарии безопасности для приложений Blazor WebAssembly.

Присоединение маркеров к исходящим запросам

AuthorizationMessageHandler — это DelegatingHandler для обработки маркеров доступа. Токены получаются с помощью службы IAccessTokenProvider, которая регистрируется платформой. Если маркер получить невозможно, создается исключение AccessTokenNotAvailableException. Класс AccessTokenNotAvailableException имеет метод Redirect для перехода по AccessTokenResult.InteractiveRequestUrl с использованием заданных параметров AccessTokenResult.InteractionOptions с целью разрешить обновление маркера доступа.

Для удобства платформа предоставляет BaseAddressAuthorizationMessageHandler предварительно настроенным с базовым адресом приложения как разрешенным URL-адресом. Маркеры доступа добавляются, только если URI запроса находится в базовом URI приложения. Если URI исходящих запросов не находятся в базовом URI приложения, используйте настраиваемый класс AuthorizationMessageHandler (рекомендуется) или настройте AuthorizationMessageHandler.

Примечание.

Помимо настройки клиентского приложения для доступа к API сервера, серверный API должен также разрешить запросы независимо от источника (CORS), если клиент и сервер не находятся по одному и тому же базовому адресу. Дополнительные сведения о конфигурации CORS на стороне сервера см . в разделе "Общий доступ к ресурсам между источниками" (CORS) далее в этой статье.

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

В следующем примере HttpClientFactoryServiceCollectionExtensions.AddHttpClient используется расширение Microsoft.Extensions.Http. Добавьте пакет в приложение, которое еще не ссылается на него.

Примечание.

Рекомендации по добавлению пакетов в приложения .NET см. в разделе Способы установки пакетов NuGet в статье Рабочий процесс использования пакета (документация по NuGet). Проверьте правильность версий пакета на сайте NuGet.org.

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient("WebAPI", 
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("WebAPI"));

Для размещенного решенияBlazor, основанного на шаблоне проекта Blazor WebAssembly, по умолчанию URI запроса находятся в базовом URI приложения. Таким образом, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) назначается HttpClient.BaseAddress в приложении, созданном из шаблона проекта.

Настроенный объект HttpClient используется для выполнения авторизованных запросов с помощью шаблона try-catch:

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject HttpClient Http

...

protected override async Task OnInitializedAsync()
{
    try
    {
        var examples = 
            await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

        ...
    }
    catch (AccessTokenNotAvailableException exception)
    {
        exception.Redirect();
    }
}

Сценарии пользовательских запросов проверки подлинности

В следующих сценариях показано, как настроить запросы проверки подлинности и как получить путь входа из параметров проверки подлинности.

Настройка процесса входа

Управление дополнительными параметрами для запроса входа с помощью следующих методов один или несколько раз в новом экземпляре InteractiveRequestOptions:

В следующем LoginDisplay примере компонента в запрос входа добавляются дополнительные параметры:

  • prompt имеет значение login: принудительно вводите учетные данные пользователя в этом запросе, отрицая единый вход.
  • loginHint имеет значение peter@contoso.com: предварительно заполняет поле имени пользователя или адреса электронной почты страницы входа для пользователя peter@contoso.com. Приложения часто используют этот параметр во время повторной проверки подлинности, уже извлекая имя пользователя из предыдущего входа с помощью preferred_username утверждения.

Shared/LoginDisplay.razor:

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity?.Name!
        <button @onclick="BeginLogOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <button @onclick="BeginLogIn">Log in</button>
    </NotAuthorized>
</AuthorizeView>

@code{
    public void BeginLogOut()
    {
        Navigation.NavigateToLogout("authentication/logout");
    }

    public void BeginLogIn()
    {
        InteractiveRequestOptions requestOptions =
            new()
            {
                Interaction = InteractionType.SignIn,
                ReturnUrl = Navigation.Uri,
            };

        requestOptions.TryAddAdditionalParameter("prompt", "login");
        requestOptions.TryAddAdditionalParameter("loginHint", "peter@contoso.com");

        Navigation.NavigateToLogin("authentication/login", requestOptions);
    }
}

Дополнительные сведения см. на следующих ресурсах:

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

AccessTokenNotAvailableException Если происходит, управляйте дополнительными параметрами для нового запроса маркера доступа поставщика удостоверений с помощью следующих методов один или несколько раз в новом экземпляреInteractiveRequestOptions:

В следующем примере, который получает JSданные ON через веб-API, дополнительные параметры добавляются в запрос перенаправления, если маркер доступа недоступен (AccessTokenNotAvailableException создается):

  • prompt имеет значение login: принудительно вводите учетные данные пользователя в этом запросе, отрицая единый вход.
  • loginHint имеет значение peter@contoso.com: предварительно заполняет поле имени пользователя или адреса электронной почты страницы входа для пользователя peter@contoso.com. Приложения часто используют этот параметр во время повторной проверки подлинности, уже извлекая имя пользователя из предыдущего входа с помощью preferred_username утверждения.
try
{
    var examples = await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

    ...
}
catch (AccessTokenNotAvailableException ex)
{
    ex.Redirect(requestOptions => {
        requestOptions.TryAddAdditionalParameter("prompt", "login");
        requestOptions.TryAddAdditionalParameter("loginHint", "peter@contoso.com");
    });
}

В предыдущем примере предполагается следующее:

Дополнительные сведения см. на следующих ресурсах:

Настройка параметров при использовании IAccessTokenProvider

Если получение маркера завершается ошибкой при использовании IAccessTokenProvider, управляйте дополнительными параметрами для нового запроса маркера доступа поставщика удостоверений одним или несколькими методами в новом экземпляре InteractiveRequestOptions:

В следующем примере, который пытается получить маркер доступа для пользователя, дополнительные параметры добавляются в запрос входа, если попытка получить маркер завершается ошибкой при TryGetToken вызове:

  • prompt имеет значение login: принудительно вводите учетные данные пользователя в этом запросе, отрицая единый вход.
  • loginHint имеет значение peter@contoso.com: предварительно заполняет поле имени пользователя или адреса электронной почты страницы входа для пользователя peter@contoso.com. Приложения часто используют этот параметр во время повторной проверки подлинности, уже извлекая имя пользователя из предыдущего входа с помощью preferred_username утверждения.
var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = new[] { ... }
    });

if (!tokenResult.TryGetToken(out var token))
{
    tokenResult.InteractionOptions.TryAddAdditionalParameter("prompt", "login");
    tokenResult.InteractionOptions.TryAddAdditionalParameter("loginHint", 
        "peter@contoso.com");

    Navigation.NavigateToLogin(accessTokenResult.InteractiveRequestUrl, 
        accessTokenResult.InteractionOptions);
}

В предыдущем примере предполагается следующее:

Дополнительные сведения см. на следующих ресурсах:

Выход с настраиваемым URL-адресом возврата

В следующем примере пользователь выходит из системы и возвращается в конечную точку /goodbye:

Navigation.NavigateToLogout("authentication/logout", "goodbye");

Получение пути входа из параметров проверки подлинности

Получите настроенный путь входа из RemoteAuthenticationOptions:

var loginPath = 
    RemoteAuthOptions.Get(Options.DefaultName).AuthenticationPaths.LogInPath;

В предыдущем примере предполагается следующее:

Пользовательский класс AuthorizationMessageHandler

Руководство в этом разделе рекомендуется для клиентских приложений, которые делают исходящие запросы к URI, которые не находятся в базовом URI приложения.

В следующем примере пользовательский класс расширяет AuthorizationMessageHandler для использования в качестве DelegatingHandler для HttpClient. ConfigureHandler настраивает этот обработчик для авторизации исходящих HTTP-запросов с помощью маркера доступа. Маркер доступа прикрепляется, только если по крайней мере один из разрешенных URL-адресов является базовым для URI запроса (HttpRequestMessage.RequestUri).

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, 
        NavigationManager navigation)
        : base(provider, navigation)
    {
        ConfigureHandler(
            authorizedUrls: new[] { "https://api.contoso.com/v1.0" },
            scopes: new[] { "example.read", "example.write" });
    }
}

В приведенном выше коде области example.read и example.write являются общими примерами, не предназначенными для отражения допустимых областей для конкретного поставщика.

Program В файле CustomAuthorizationMessageHandler регистрируется в качестве временной службы и настраивается в качестве DelegatingHandler исходящих HttpResponseMessage экземпляров, сделанных именованнымHttpClient.

В следующем примере HttpClientFactoryServiceCollectionExtensions.AddHttpClient используется расширение Microsoft.Extensions.Http. Добавьте пакет в приложение, которое еще не ссылается на него.

Примечание.

Рекомендации по добавлению пакетов в приложения .NET см. в разделе Способы установки пакетов NuGet в статье Рабочий процесс использования пакета (документация по NuGet). Проверьте правильность версий пакета на сайте NuGet.org.

builder.Services.AddTransient<CustomAuthorizationMessageHandler>();

builder.Services.AddHttpClient("WebAPI",
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<CustomAuthorizationMessageHandler>();

Примечание.

В предыдущем примере класс DelegatingHandlerCustomAuthorizationMessageHandler регистрируется как временная служба для AddHttpMessageHandler. Временную регистрацию рекомендуется использовать для интерфейса IHttpClientFactory, который управляет собственными областями внедрения зависимостей. Дополнительные сведения см. на следующих ресурсах:

Для размещенного решения Blazor, основанного на шаблоне проекта Blazor WebAssembly, по умолчанию IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) назначается HttpClient.BaseAddress.

Настроенный объект HttpClient используется для выполнения авторизованных запросов с помощью шаблона try-catch. Когда клиент создается с помощью метода CreateClient (из пакета Microsoft.Extensions.Http), при выполнении запросов к интерфейсу API сервера объекту HttpClient передаются экземпляры, содержащие маркеры доступа. Если URI запроса является относительным URI, как показано в следующем примере (ExampleAPIMethod), он объединяется с BaseAddress, когда клиентское приложение выполняет запрос.

@inject IHttpClientFactory ClientFactory

...

@code {
    protected override async Task OnInitializedAsync()
    {
        try
        {
            var client = ClientFactory.CreateClient("WebAPI");

            var examples = 
                await client.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

            ...
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}

Настройка AuthorizationMessageHandler

Для AuthorizationMessageHandler можно настроить разрешенные URL-адреса, области и URL-адреса возврата с помощью метода ConfigureHandler. ConfigureHandler настраивает обработчик для авторизации исходящих HTTP-запросов с помощью маркера доступа. Маркер доступа прикрепляется, только если по крайней мере один из разрешенных URL-адресов является базовым для URI запроса (HttpRequestMessage.RequestUri). Если URI запроса является относительным URI, он объединяется с BaseAddress.

В следующем примере AuthorizationMessageHandler настраивается HttpClient файл:Program

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddScoped(sp => new HttpClient(
    sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: new[] { "https://api.contoso.com/v1.0" },
        scopes: new[] { "example.read", "example.write" }))
    {
        BaseAddress = new Uri("https://api.contoso.com/v1.0")
    });

В приведенном выше коде области example.read и example.write являются общими примерами, не предназначенными для отражения допустимых областей для конкретного поставщика.

Для размещенного решения Blazor, основанного на шаблоне проекта Blazor WebAssembly, по умолчанию IWebAssemblyHostEnvironment.BaseAddress назначается следующим элементам:

  • свойству HttpClient.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress));
  • URL-адресу массива authorizedUrls.

Типизированный HttpClient

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

WeatherForecastClient.cs:

using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using static {ASSEMBLY NAME}.Data;

public class WeatherForecastClient
{
    private readonly HttpClient http;
    private WeatherForecast[]? forecasts;

    public WeatherForecastClient(HttpClient http)
    {
        this.http = http;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        try
        {
            forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
                "WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }

        return forecasts ?? Array.Empty<WeatherForecast>();
    }
}
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using static {ASSEMBLY NAME}.Data;

public class WeatherForecastClient
{
    private readonly HttpClient http;
    private WeatherForecast[] forecasts;

    public WeatherForecastClient(HttpClient http)
    {
        this.http = http;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        try
        {
            forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
                "WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }

        return forecasts ?? Array.Empty<WeatherForecast>();
    }
}

В предыдущем примере WeatherForecast тип представляет собой статический класс, содержащий данные прогноза погоды. Заполнитель {ASSEMBLY NAME} — это имя сборки приложения (например, using static BlazorSample.Data;).

В следующем примере HttpClientFactoryServiceCollectionExtensions.AddHttpClient используется расширение Microsoft.Extensions.Http. Добавьте пакет в приложение, которое еще не ссылается на него.

Примечание.

Рекомендации по добавлению пакетов в приложения .NET см. в разделе Способы установки пакетов NuGet в статье Рабочий процесс использования пакета (документация по NuGet). Проверьте правильность версий пакета на сайте NuGet.org.

В файле Program:

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

Для размещенного решения Blazor, основанного на шаблоне проекта Blazor WebAssembly, по умолчанию IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) назначается HttpClient.BaseAddress.

В компоненте, который извлекает данные о погоде:

@inject WeatherForecastClient Client

...

protected override async Task OnInitializedAsync()
{
    forecasts = await Client.GetForecastAsync();
}

Настройка обработчика HttpClient

Обработчик можно дополнительно настроить с помощью ConfigureHandler для исходящих HTTP-запросов.

В следующем примере HttpClientFactoryServiceCollectionExtensions.AddHttpClient используется расширение Microsoft.Extensions.Http. Добавьте пакет в приложение, которое еще не ссылается на него.

Примечание.

Рекомендации по добавлению пакетов в приложения .NET см. в разделе Способы установки пакетов NuGet в статье Рабочий процесс использования пакета (документация по NuGet). Проверьте правильность версий пакета на сайте NuGet.org.

В файле Program:

builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler(sp => sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: new [] { "https://api.contoso.com/v1.0" },
        scopes: new[] { "example.read", "example.write" }));

В приведенном выше коде области example.read и example.write являются общими примерами, не предназначенными для отражения допустимых областей для конкретного поставщика.

Для размещенного решения Blazor, основанного на шаблоне проекта Blazor WebAssembly, по умолчанию IWebAssemblyHostEnvironment.BaseAddress назначается следующим элементам:

  • свойству HttpClient.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress));
  • URL-адресу массива authorizedUrls.

Запросы веб-API, не прошедшие проверку подлинности или неавторизованные, в приложении с защищенным клиентом по умолчанию

Приложение, которое обычно использует безопасное значение по умолчанию HttpClient , также может выполнять неавторизованные или несанкционированные запросы веб-API, настроив именованный HttpClient.

В следующем примере HttpClientFactoryServiceCollectionExtensions.AddHttpClient используется расширение Microsoft.Extensions.Http. Добавьте пакет в приложение, которое еще не ссылается на него.

Примечание.

Рекомендации по добавлению пакетов в приложения .NET см. в разделе Способы установки пакетов NuGet в статье Рабочий процесс использования пакета (документация по NuGet). Проверьте правильность версий пакета на сайте NuGet.org.

В файле Program:

builder.Services.AddHttpClient("WebAPI.NoAuthenticationClient", 
    client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"));

Для размещенного решения Blazor, основанного на шаблоне проекта Blazor WebAssembly, по умолчанию IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) назначается HttpClient.BaseAddress.

Приведенная выше регистрация является дополнением к существующей регистрации защищенного клиента HttpClient по умолчанию.

Компонент создает клиент HttpClient на основе IHttpClientFactory (из пакета Microsoft.Extensions.Http) для выполнения не прошедших проверку или неавторизованных запросов:

@inject IHttpClientFactory ClientFactory

...

@code {
    protected override async Task OnInitializedAsync()
    {
        var client = ClientFactory.CreateClient("WebAPI.NoAuthenticationClient");

        var examples = await client.GetFromJsonAsync<ExampleType[]>(
            "ExampleNoAuthentication");

        ...
    }
}

Примечание.

Контроллер в интерфейсе API сервера (ExampleNoAuthenticationController в предыдущем примере) не помечен атрибутом [Authorize].

Решение о том, следует ли использовать безопасный клиент или небезопасный клиент в качестве экземпляра HttpClient по умолчанию, принимает разработчик. При принятии такого решения рекомендуется учесть соотношение неавторизованных и аутентифицированных конечных точек, к которым обращается приложение. Если приложение отправляет большинство запросов к безопасным конечным точкам API, используйте аутентифицированный экземпляр HttpClient в качестве значения по умолчанию. В противном случае в качестве значения по умолчанию зарегистрируйте неавторизованный экземпляр HttpClient.

Альтернативный подход к использованию IHttpClientFactory заключается в создании типизированного клиента для неаутентифицированного доступа к анонимным конечным точкам.

Запрос дополнительных маркеров доступа

Маркеры доступа можно получать вручную, вызывая IAccessTokenProvider.RequestAccessToken. В приведенном ниже примере приложение требует дополнительной области для клиента HttpClient по умолчанию. В примере использования библиотеки проверки подлинности Майкрософт (MSAL) область настраивается с помощью MsalProviderOptions:

В файле Program:

builder.Services.AddMsalAuthentication(options =>
{
    ...

    options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 1}");
    options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 2}");
}

Заполнители {CUSTOM SCOPE 1} и {CUSTOM SCOPE 2} в предыдущем примере — это пользовательские области.

Примечание.

AdditionalScopesToConsent Не удается подготовить делегированные разрешения пользователя для Microsoft Graph с помощью пользовательского интерфейса согласия идентификатора Microsoft Entra ID, когда пользователь сначала использует приложение, зарегистрированное в Microsoft Azure. Дополнительные сведения см. в статье Использование API Graph с ASP.NET Core Blazor WebAssembly.

Метод IAccessTokenProvider.RequestAccessToken предоставляет перегрузку, которая позволяет приложению подготавливать маркер доступа с указанным набором областей.

В компоненте Razor:

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider

...

var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = new[] { "{CUSTOM SCOPE 1}", "{CUSTOM SCOPE 2}" }
    });

if (tokenResult.TryGetToken(out var token))
{
    ...
}

Заполнители {CUSTOM SCOPE 1} и {CUSTOM SCOPE 2} в предыдущем примере — это пользовательские области.

AccessTokenResult.TryGetToken возвращает:

  • true с пригодным для использования token.
  • false, если маркер не получен.

Общий доступ к ресурсам между источниками (CORS)

При отправке учетных данных (файлов cookie или заголовков авторизации) в запросах CORS заголовок Authorization должен быть разрешен политикой CORS.

Следующая политика включает в себя настройку для следующих элементов:

  • Источники запроса (http://localhost:5000, https://localhost:5001).
  • Любой метод (команда).
  • Заголовки Content-Type и Authorization. Чтобы разрешить пользовательский заголовок (например, x-custom-header), выведите список заголовков при вызове WithHeaders.
  • Учетные данные, установленные клиентским кодом JavaScript (для свойства credentials задано значение include).
app.UseCors(policy => 
    policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
        .AllowAnyMethod()
        .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, 
            "x-custom-header")
        .AllowCredentials());

Размещенное решение Blazor на основе шаблона проекта Blazor WebAssembly использует тот же базовый адрес для клиентских и серверных приложений. Для HttpClient.BaseAddress клиентского приложения задан универсальный код ресурса (URI) builder.HostEnvironment.BaseAddress по умолчанию. Конфигурация CORS не требуется в конфигурации по умолчанию размещенного решения Blazor. Дополнительным клиентским приложениям, которые не размещаются в серверном проекте и не используют базовый адрес серверного приложения требуется конфигурация CORS в серверном проекте.

Дополнительные сведения см. в разделе Включение CORS в ASP.NET Core и компоненте тестера HTTP-запросов примера приложения (Components/HTTPRequestTester.razor).

Обработка ошибок при выполнении запросов маркеров

Когда одностраничное приложение (SPA) выполняет проверку подлинности пользователя с помощью OpenID Connect (OIDC), состояние проверки подлинности сохраняется локально в приложении SPA и в поставщике Identity (IP) в виде файла cookie сеанса, который задается в результате предоставления учетных данных пользователем.

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

В некоторых случаях клиент не может получить маркер без участия пользователя, например, если по какой-либо причине пользователь явно вышел из поставщика удостоверений. Этот сценарий возникает, если пользователь посещает https://login.microsoftonline.com и выходит из системы. В этих сценариях приложение не знает сразу, что пользователь выошел из системы. Любой маркер, который хранит клиент, больше не может быть допустимым. Кроме того, клиент не может подготовить новый маркер без участия пользователя после истечения срока действия текущего маркера.

Такие сценарии нехарактерны для проверки подлинности на основе маркеров. Они проистекают из особенностей одностраничных приложений. Одностраничному приложению, использующему файлы cookie, также не удастся вызвать интерфейс API сервера в случае удаления файла cookie для проверки подлинности.

Когда приложение выполняет вызовы API к защищенным ресурсам, необходимо учитывать следующее:

  • Для подготовки нового маркера доступа с целью вызова API пользователю может потребоваться пройти проверку подлинности повторно.
  • Даже если у клиента есть маркер, который представляется действительным, вызов сервера может завершиться ошибкой из-за того, что маркер был отозван пользователем.

Когда приложение запрашивает маркер, возможны два результата:

  • Запрос выполняется успешно, и приложение получает действительный маркер.
  • Запрос завершается ошибкой, и приложение должно повторно провести проверку подлинности пользователя, чтобы получить новый маркер.

При сбое запроса маркера необходимо решить, следует ли сохранить текущее состояние перед выполнением перенаправления. Существует несколько подходов к хранению состояния с увеличением сложности:

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

Сохранение состояния приложения перед операцией проверки подлинности с помощью хранилища сеансов

В приведенном ниже примере показано, как выполнить следующие задачи.

  • сохранить состояние перед перенаправлением на страницу входа;
  • Восстановите предыдущее состояние после проверки подлинности с помощью параметра строки запроса.
...
@using System.Text.Json
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider
@inject IJSRuntime JS
@inject NavigationManager Navigation

<EditForm Model="User" OnSubmit="OnSaveAsync">
    <label>
        First Name: 
        <InputText @bind-Value="User!.Name" />
    </label>
    <label>
        Last Name: 
        <InputText @bind-Value="User!.LastName" />
    </label>
    <button type="submit">Save User</button>
</EditForm>

@code {
    public Profile User { get; set; } = new Profile();

    protected override async Task OnInitializedAsync()
    {
        var currentQuery = new Uri(Navigation.Uri).Query;

        if (currentQuery.Contains("state=resumeSavingProfile"))
        {
            var user = await JS.InvokeAsync<string>("sessionStorage.getItem",
                "resumeSavingProfile");

            if (!string.IsNullOrEmpty(user))
            {
                User = JsonSerializer.Deserialize<Profile>(user);
            }
        }
    }

    public async Task OnSaveAsync()
    {
        var http = new HttpClient();
        http.BaseAddress = new Uri(Navigation.BaseUri);

        var resumeUri = Navigation.Uri + $"?state=resumeSavingProfile";

        var tokenResult = await TokenProvider.RequestAccessToken(
            new AccessTokenRequestOptions
            {
                ReturnUrl = resumeUri
            });

        if (tokenResult.TryGetToken(out var token))
        {
            http.DefaultRequestHeaders.Add("Authorization", 
                $"Bearer {token.Value}");
            await http.PostAsJsonAsync("Save", User);
        }
        else
        {
            await JS.InvokeVoidAsync("sessionStorage.setItem", 
                "resumeSavingProfile", JsonSerializer.Serialize(User));
            Navigation.NavigateTo(tokenResult.InteractiveRequestUrl);
        }
    }

    public class Profile
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }
}

Сохранение состояния приложения перед операцией проверки подлинности с хранилищем сеансов и контейнером состояния

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

В приложении создается класс контейнера состояния со свойствами для хранения значений состояния приложения. В приведенном ниже примере контейнер используется для хранения значения счетчика для компонента Counter (Counter.razor) шаблона проекта Blazor по умолчанию. Методы сериализации и десериализации контейнера основаны на System.Text.Json.

using System.Text.Json;

public class StateContainer
{
    public int CounterValue { get; set; }

    public string GetStateForLocalStorage()
    {
        return JsonSerializer.Serialize(this);
    }

    public void SetStateFromLocalStorage(string locallyStoredState)
    {
        var deserializedState = 
            JsonSerializer.Deserialize<StateContainer>(locallyStoredState);

        CounterValue = deserializedState.CounterValue;
    }
}

Компонент Counter использует контейнер состояния для хранения значения currentCount за пределами компонента:

@page "/counter"
@inject StateContainer State

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    protected override void OnInitialized()
    {
        if (State.CounterValue > 0)
        {
            currentCount = State.CounterValue;
        }
    }

    private void IncrementCount()
    {
        currentCount++;
        State.CounterValue = currentCount;
    }
}

Создайте ApplicationAuthenticationState на основе RemoteAuthenticationState. Укажите свойство Id, которое служит идентификатором для локально хранящегося состояния.

ApplicationAuthenticationState.cs:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class ApplicationAuthenticationState : RemoteAuthenticationState
{
    public string? Id { get; set; }
}
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class ApplicationAuthenticationState : RemoteAuthenticationState
{
    public string Id { get; set; }
}

Компонент Authentication (Authentication.razor) сохраняет и восстанавливает состояние приложения, используя локальное хранилище сеансов, с помощью методов сериализации и десериализации StateContainer (GetStateForLocalStorage и SetStateFromLocalStorage):

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IJSRuntime JS
@inject StateContainer State

<RemoteAuthenticatorViewCore Action="Action"
                             TAuthenticationState="ApplicationAuthenticationState"
                             AuthenticationState="AuthenticationState"
                             OnLogInSucceeded="RestoreState"
                             OnLogOutSucceeded="RestoreState" />

@code {
    [Parameter]
    public string? Action { get; set; }

    public ApplicationAuthenticationState AuthenticationState { get; set; } =
        new ApplicationAuthenticationState();

    protected override async Task OnInitializedAsync()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn,
            Action) ||
            RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
            Action))
        {
            AuthenticationState.Id = Guid.NewGuid().ToString();

            await JS.InvokeVoidAsync("sessionStorage.setItem",
                AuthenticationState.Id, State.GetStateForLocalStorage());
        }
    }

    private async Task RestoreState(ApplicationAuthenticationState state)
    {
        if (state.Id != null)
        {
            var locallyStoredState = await JS.InvokeAsync<string>(
                "sessionStorage.getItem", state.Id);

            if (locallyStoredState != null)
            {
                State.SetStateFromLocalStorage(locallyStoredState);
                await JS.InvokeVoidAsync("sessionStorage.removeItem", state.Id);
            }
        }
    }
}
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IJSRuntime JS
@inject StateContainer State

<RemoteAuthenticatorViewCore Action="Action"
                             TAuthenticationState="ApplicationAuthenticationState"
                             AuthenticationState="AuthenticationState"
                             OnLogInSucceeded="RestoreState"
                             OnLogOutSucceeded="RestoreState" />

@code {
    [Parameter]
    public string Action { get; set; }

    public ApplicationAuthenticationState AuthenticationState { get; set; } =
        new ApplicationAuthenticationState();

    protected override async Task OnInitializedAsync()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn,
            Action) ||
            RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
            Action))
        {
            AuthenticationState.Id = Guid.NewGuid().ToString();

            await JS.InvokeVoidAsync("sessionStorage.setItem",
                AuthenticationState.Id, State.GetStateForLocalStorage());
        }
    }

    private async Task RestoreState(ApplicationAuthenticationState state)
    {
        if (state.Id != null)
        {
            var locallyStoredState = await JS.InvokeAsync<string>(
                "sessionStorage.getItem", state.Id);

            if (locallyStoredState != null)
            {
                State.SetStateFromLocalStorage(locallyStoredState);
                await JS.InvokeVoidAsync("sessionStorage.removeItem", state.Id);
            }
        }
    }
}

В этом примере для проверки подлинности используется Microsoft Entra (ME-ID). В файле Program:

  • Он ApplicationAuthenticationState настроен как тип библиотеки проверки подлинности Майкрософт (MSAL RemoteAuthenticationState ).
  • Контейнер состояния регистрируется в контейнере службы.
builder.Services.AddMsalAuthentication<ApplicationAuthenticationState>(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});

builder.Services.AddSingleton<StateContainer>();

Настройка маршрутов приложения

Для представления различных состояний проверки подлинности библиотека Microsoft.AspNetCore.Components.WebAssembly.Authentication по умолчанию использует маршруты, приведенные в таблице ниже.

Маршрут Характер использования
authentication/login Активирует операцию входа.
authentication/login-callback Обрабатывает результат любой операции входа.
authentication/login-failed Отображает сообщения об ошибках при сбое операции входа по какой-либо причине.
authentication/logout Активирует операцию выхода.
authentication/logout-callback Обрабатывает результат любой операции выхода.
authentication/logout-failed Отображает сообщения об ошибках при сбое операции выхода по какой-либо причине.
authentication/logged-out Указывает, что пользователь успешно выполнил выход.
authentication/profile Активирует операцию изменения профиля пользователя.
authentication/register Активирует операцию регистрации нового пользователя.

Маршруты, представленные в предыдущей таблице, можно настраивать с помощью RemoteAuthenticationOptions<TRemoteAuthenticationProviderOptions>.AuthenticationPaths. При настройке параметров для предоставления пользовательских маршрутов убедитесь в том, что у приложения есть маршрут для обработки каждого пути.

В следующем примере все пути префиксируются с /securityпрефиксом.

Компонент Authentication (Authentication.razor):

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action" />

@code{
    [Parameter]
    public string? Action { get; set; }
}
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action" />

@code{
    [Parameter]
    public string Action { get; set; }
}

В файле Program:

builder.Services.AddApiAuthorization(options => { 
    options.AuthenticationPaths.LogInPath = "security/login";
    options.AuthenticationPaths.LogInCallbackPath = "security/login-callback";
    options.AuthenticationPaths.LogInFailedPath = "security/login-failed";
    options.AuthenticationPaths.LogOutPath = "security/logout";
    options.AuthenticationPaths.LogOutCallbackPath = "security/logout-callback";
    options.AuthenticationPaths.LogOutFailedPath = "security/logout-failed";
    options.AuthenticationPaths.LogOutSucceededPath = "security/logged-out";
    options.AuthenticationPaths.ProfilePath = "security/profile";
    options.AuthenticationPaths.RegisterPath = "security/register";
});

Если требуются совершенно разные пути, задайте маршруты, как описано выше, и обработайте RemoteAuthenticatorView с помощью явного параметра действия:

@page "/register"

<RemoteAuthenticatorView Action="RemoteAuthenticationActions.Register" />

При необходимости можно разделить пользовательский интерфейс на несколько страниц.

Настройка пользовательского интерфейса проверки подлинности

RemoteAuthenticatorView включает набор фрагментов пользовательского интерфейса по умолчанию для каждого состояния проверки подлинности. Каждое состояние можно настроить, передав пользовательский объект RenderFragment. Чтобы настроить текст, отображаемый во время первоначального входа в систему, можно изменить RemoteAuthenticatorView указанным ниже образом.

Компонент Authentication (Authentication.razor):

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

@code{
    [Parameter]
    public string? Action { get; set; }
}
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

@code{
    [Parameter]
    public string Action { get; set; }
}

В RemoteAuthenticatorView есть фрагмент, который можно использовать для каждого маршрута проверки подлинности, как показано в приведенной ниже таблице.

Маршрут Фрагмент
authentication/login <LoggingIn>
authentication/login-callback <CompletingLoggingIn>
authentication/login-failed <LogInFailed>
authentication/logout <LogOut>
authentication/logout-callback <CompletingLogOut>
authentication/logout-failed <LogOutFailed>
authentication/logged-out <LogOutSucceeded>
authentication/profile <UserProfile>
authentication/register <Registering>

Настройка пользователя

Пользователей, привязанных к приложению, можно настраивать.

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

В приведенном ниже примере пользователи приложения, прошедшие проверку подлинности, получают утверждение amr для каждого из методов проверки подлинности. Утверждение amr указывает способ проверки подлинности субъекта токена в утверждениях полезной нагрузки Microsoft Identity Platform версии 1.0. В примере используется настраиваемый класс учетной записи пользователя на основе RemoteUserAccount.

Создайте класс, расширяющий класс RemoteUserAccount. В приведенном ниже примере свойству AuthenticationMethod присваивается пользовательский массив значений свойств amrJSON. AuthenticationMethod заполняется платформой автоматически при проверке подлинности пользователя.

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("amr")]
    public string[]? AuthenticationMethod { get; set; }
}
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("amr")]
    public string[] AuthenticationMethod { get; set; }
}

Создайте фабрику, расширяющую AccountClaimsPrincipalFactory<TAccount> для создания утверждений на основе методов проверки подлинности пользователя, хранящихся в CustomUserAccount.AuthenticationMethod:

using System.Security.Claims;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory 
    : AccountClaimsPrincipalFactory<CustomUserAccount>
{
    public CustomAccountFactory(NavigationManager navigation, 
        IAccessTokenProviderAccessor accessor) : base(accessor)
    {
    }

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity != null && initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = (ClaimsIdentity)initialUser.Identity;

            if (account.AuthenticationMethod is not null)
            {
                foreach (var value in account.AuthenticationMethod)
                {
                    userIdentity.AddClaim(new Claim("amr", value));
                }
            }
        }

        return initialUser;
    }
}
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory 
    : AccountClaimsPrincipalFactory<CustomUserAccount>
{
    public CustomAccountFactory(NavigationManager navigation, 
        IAccessTokenProviderAccessor accessor) : base(accessor)
    {
    }

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity != null && initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = (ClaimsIdentity)initialUser.Identity;

            foreach (var value in account.AuthenticationMethod)
            {
                userIdentity.AddClaim(new Claim("amr", value));
            }
        }

        return initialUser;
    }
}

Зарегистрируйте CustomAccountFactory для используемого поставщика проверки подлинности. Допустима любая из следующих регистраций:

  • AddOidcAuthentication:

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddOidcAuthentication<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    
  • AddMsalAuthentication:

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddMsalAuthentication<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    
  • AddApiAuthorization:

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddApiAuthorization<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    

Группы безопасности и роли ME-ID с пользовательским классом учетной записи пользователя

Дополнительные примеры, которые работают с группами безопасности ME-ID и Ролями Администратор istrator и пользовательским классом учетной записи пользователя, см. в разделе ASP.NET Core Blazor WebAssembly с группами и ролями идентификаторов Microsoft Entra ID.

Предварительная подготовка с проверкой подлинности

Предварительная подготовка содержимого, требующего аутентификации и авторизации, сейчас не поддерживается. Выполнив инструкции в одном из разделов, посвященных безопасности приложений Blazor WebAssembly, выполните приведенные ниже действия, чтобы создать приложение, которое:

  • предварительно отрисовывает пути, не требующие авторизации;
  • не выполняет предварительную отрисовку путей, требующих авторизации.

Client Для файла проекта Program фактор общих регистраций служб в отдельный метод (например, создайте ConfigureCommonServices метод в Client проекте). Общими называются службы, которые разработчик регистрирует для использования клиентским и серверным проектами.

public static void ConfigureCommonServices(IServiceCollection services)
{
    services.Add...;
}

В файле Program:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...

builder.Services.AddScoped( ... );

ConfigureCommonServices(builder.Services);

await builder.Build().RunAsync();

Server В файле проекта Program зарегистрируйте следующие дополнительные службы и вызовConfigureCommonServices:

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddRazorPages();
builder.Services.TryAddScoped<AuthenticationStateProvider, 
    ServerAuthenticationStateProvider>();

Client.Program.ConfigureCommonServices(services);

В методе Startup.ConfigureServices проекта Server зарегистрируйте следующие дополнительные службы и вызовите ConfigureCommonServices:

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddRazorPages();
    services.AddScoped<AuthenticationStateProvider, 
        ServerAuthenticationStateProvider>();
    services.AddScoped<SignOutSessionStateManager>();

    Client.Program.ConfigureCommonServices(services);
}

Дополнительные сведения о поставщике Blazor проверки подлинности сервера платформы смServerAuthenticationStateProvider. в разделе ASP.NET Проверка подлинности и авторизация CoreBlazor.

В файле Pages/_Host.cshtml проекта Server замените вспомогательное приложение тегов Component (<component ... />) на следующее:

<div id="app">
    @if (HttpContext.Request.Path.StartsWithSegments("/authentication"))
    {
        <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
            render-mode="WebAssembly" />
    }
    else
    {
        <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
            render-mode="WebAssemblyPrerendered" />
    }
</div>

В предыдущем примере:

  • Заполнитель {CLIENT APP ASSEMBLY NAME} — это имя сборки клиентского приложения (например, BlazorSample.Client).
  • Условный проверка для /authentication сегмента пути:
    • Позволяет избежать предварительной отрисовки (render-mode="WebAssembly") для путей проверки подлинности.
    • Выполняет предварительную отрисовку (render-mode="WebAssemblyPrerendered") для путей, не относящихся к проверке подлинности.

Варианты для размещенных приложений и сторонних поставщиков входа

При аутентификации и авторизации размещенного приложения Blazor WebAssembly в стороннем поставщике доступно несколько вариантов аутентификации пользователя. Выбор варианта зависит от вашего сценария.

Дополнительные сведения см. в разделе Сохранение дополнительных утверждений и маркеров от внешних поставщиков в ASP.NET Core.

Проверка подлинности пользователей для вызова только защищенных сторонних API

Проверьте подлинность пользователя с помощью потока OAuth на стороне клиента в стороннем поставщике API:

builder.services.AddOidcAuthentication(options => { ... });

В этом сценарии:

  • Сервер, на котором размещено приложение, не имеет особого значения.
  • Невозможно обеспечить защиту API на сервере.
  • Приложение может вызывать только защищенные сторонние интерфейсы API.

Проверка подлинности пользователей в стороннем поставщике и вызов защищенных API на сервере узла и сторонних API

Настройте Identity с помощью стороннего поставщика входа. Получите токены, необходимые для доступа к сторонним API, и сохраните их.

При входе пользователя в систему Identity собирает маркеры доступа и обновления в рамках процесса проверки подлинности. На этом этапе существует несколько подходов для отправки вызовов API к сторонним API.

Использование токена доступа сервера для получения стороннего токена доступа

С помощью созданного на сервере токена доступа получите сторонний токен доступа из конечной точки API сервера. Затем воспользуйтесь сторонним маркером доступа для вызова ресурсов стороннего API непосредственно из Identity в клиенте.

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

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

Отправка вызовов API с API клиента на API сервера для вызова сторонних API

Отправьте вызов API с API клиента на API сервера. На сервере получите токен доступа для ресурса стороннего API и осуществите необходимый вызов.

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

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

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

В библиотеке проверки подлинности и шаблонах проекта Blazor используются конечные точки OpenID Connect (OIDC) версии 1.0. Чтобы использовать конечную точку версии 2.0, настройте параметр JwtBearerOptions.Authority маркера носителя JWT. В следующем примере ME-ID настраивается для версии 2.0 путем добавления v2.0 сегмента к свойству Authority :

using Microsoft.AspNetCore.Authentication.JwtBearer;

...

builder.Services.Configure<JwtBearerOptions>(
    JwtBearerDefaults.AuthenticationScheme, 
    options =>
    {
        options.Authority += "/v2.0";
    });

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

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

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

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

Настройка и использование gRPC в компонентах

Чтобы настроить приложение Blazor WebAssembly для использования платформы ASP.NET Core gRPC, выполните указанные ниже действия.

  • Включите gRPC-Web на сервере. Дополнительные сведения см. в статье gRPC-Web в приложениях ASP.NET Core gRPC.
  • Зарегистрируйте службы gRPC для обработчика сообщений приложения. В следующем примере обработчик сообщений авторизации приложения настраивается для использования GreeterClient службы из руководства по gRPC ( Program файл).

Примечание.

Предварительная отрисовка включена по умолчанию в Blazor веб-приложения, поэтому сначала необходимо учитывать отрисовку компонента с сервера, а затем от клиента. Любое предварительно созданное состояние должно передаваться клиенту, чтобы его можно было повторно использовать. Дополнительные сведения см. в разделе "Предварительная ASP.NET Основные Razor компоненты".

Примечание.

Предварительная отрисовка включена по умолчанию в размещенных Blazor WebAssembly приложениях, поэтому сначала необходимо учитывать отрисовку компонента с сервера, а затем от клиента. Любое предварительно созданное состояние должно передаваться клиенту, чтобы его можно было повторно использовать. Дополнительные сведения см. в статье Компоненты Razor для предварительной визуализации и интеграции ASP.NET Core.

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;

...

builder.Services.AddScoped(sp =>
{
    var baseAddressMessageHandler = 
        sp.GetRequiredService<BaseAddressAuthorizationMessageHandler>();
    baseAddressMessageHandler.InnerHandler = new HttpClientHandler();
    var grpcWebHandler = 
        new GrpcWebHandler(GrpcWebMode.GrpcWeb, baseAddressMessageHandler);
    var channel = GrpcChannel.ForAddress(builder.HostEnvironment.BaseAddress, 
        new GrpcChannelOptions { HttpHandler = grpcWebHandler });

    return new Greeter.GreeterClient(channel);
});

Компонент в клиентском приложении может выполнять вызовы gRPC с помощью клиента gRPC (Grpc.razor):

@page "/grpc"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject Greeter.GreeterClient GreeterClient

<h1>Invoke gRPC service</h1>

<p>
    <input @bind="name" placeholder="Type your name" />
    <button @onclick="GetGreeting" class="btn btn-primary">Call gRPC service</button>
</p>

Server response: <strong>@serverResponse</strong>

@code {
    private string name = "Bert";
    private string? serverResponse;

    private async Task GetGreeting()
    {
        try
        {
            var request = new HelloRequest { Name = name };
            var reply = await GreeterClient.SayHelloAsync(request);
            serverResponse = reply.Message;
        }
        catch (Grpc.Core.RpcException ex)
            when (ex.Status.DebugException is 
                AccessTokenNotAvailableException tokenEx)
        {
            tokenEx.Redirect();
        }
    }
}

Чтобы применить свойство Status.DebugException, используйте Grpc.Net.Client версии 2.30.0 или более поздней.

Дополнительные сведения см. в статье gRPC-Web в приложениях ASP.NET Core gRPC.

Замена реализации AuthenticationService

В следующих подразделах объясняется, как выполнить замену:

  • любой реализации AuthenticationService JavaScript.
  • библиотеки проверки подлинности Майкрософт для JavaScript (MSAL.js).

Замена любой реализации AuthenticationService JavaScript

Создайте библиотеку JavaScript для обработки сведений о пользовательской проверке подлинности.

Предупреждение

В этом разделе представлены подробные инструкции по реализации класса RemoteAuthenticationService<TRemoteAuthenticationState,TAccount,TProviderOptions> по умолчанию. Код TypeScript в этом разделе применяется специально к ASP.NET Core в .NET 7 и подлежит изменению без уведомления в предстоящих выпусках ASP.NET Core.

// .NET makes calls to an AuthenticationService object in the Window.
declare global {
  interface Window { AuthenticationService: AuthenticationService }
}

export interface AuthenticationService {
  // Init is called to initialize the AuthenticationService.
  public static init(settings: UserManagerSettings & AuthorizeServiceSettings, logger: any) : Promise<void>;

  // Gets the currently authenticated user.
  public static getUser() : Promise<{[key: string] : string }>;

  // Tries to get an access token silently.
  public static getAccessToken(options: AccessTokenRequestOptions) : Promise<AccessTokenResult>;

  // Tries to sign in the user or get an access token interactively.
  public static signIn(context: AuthenticationContext) : Promise<AuthenticationResult>;

  // Handles the sign-in process when a redirect is used.
  public static async completeSignIn(url: string) : Promise<AuthenticationResult>;

  // Signs the user out.
  public static signOut(context: AuthenticationContext) : Promise<AuthenticationResult>;

  // Handles the signout callback when a redirect is used.
  public static async completeSignOut(url: string) : Promise<AuthenticationResult>;
}

// The rest of these interfaces match their C# definitions.

export interface AccessTokenRequestOptions {
  scopes: string[];
  returnUrl: string;
}

export interface AccessTokenResult {
  status: AccessTokenResultStatus;
  token?: AccessToken;
}

export interface AccessToken {
  value: string;
  expires: Date;
  grantedScopes: string[];
}

export enum AccessTokenResultStatus {
  Success = 'Success',
  RequiresRedirect = 'RequiresRedirect'
}

export enum AuthenticationResultStatus {
  Redirect = 'Redirect',
  Success = 'Success',
  Failure = 'Failure',
  OperationCompleted = 'OperationCompleted'
};

export interface AuthenticationResult {
  status: AuthenticationResultStatus;
  state?: unknown;
  message?: string;
}

export interface AuthenticationContext {
  state?: unknown;
  interactiveRequest: InteractiveAuthenticationRequest;
}

export interface InteractiveAuthenticationRequest {
  scopes?: string[];
  additionalRequestParameters?: { [key: string]: any };
};

Вы можете импортировать библиотеку, удалив исходный тег <script> и добавив тег <script>, который загружает пользовательскую библиотеку. В следующем примере показано, как заменить тег <script> по умолчанию на тег, который загружает библиотеку с именем CustomAuthenticationService.js из папки wwwroot/js.

Перед wwwroot/index.html скриптом (_framework/blazor.webassembly.js) внутри закрывающего </body> тегаBlazor:

- <script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
+ <script src="js/CustomAuthenticationService.js"></script>

Дополнительные сведения см. в файле AuthenticationService.ts в репозитории GitHub dotnet/aspnetcore.

Примечание.

По ссылкам в документации на справочные материалы по .NET обычно загружается ветвь репозитория по умолчанию, которая представляет текущую разработку для следующего выпуска .NET. Чтобы выбрать тег для определенного выпуска, используйте раскрывающийся список Switch branches or tags (Переключение ветвей или тегов). Дополнительные сведения см. в статье Выбор тега версии исходного кода ASP.NET Core (dotnet/AspNetCore.Docs #26205).

Замена библиотеки проверки подлинности Майкрософт для JavaScript (MSAL.js)

Если приложению требуется пользовательская версия библиотеки проверки подлинности Майкрософт для JavaScript (MSAL.js), выполните следующие действия:

  1. Убедитесь, что в системе установлен последний пакет SDK для разработчиков .NET, или получите и установите последнюю версию пакета SDK для разработчиков, как описано здесь. Для этого сценария настройка внутренних веб-каналов NuGet не требуется.
  2. Настройте репозиторий dotnet/aspnetcore GitHub для разработки после документации по сборке ASP.NET Core из источника. Вилку и клонирование или скачивание ZIP-архива dotnet/aspnetcore репозитория GitHub.
  3. Откройте файл src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json и задайте нужную версию @azure/msal-browser. Чтобы получить список выпущенных версий, посетите веб-сайт npm @azure/msal-browser и выберите вкладку Versions (Версии).
  4. Создайте проект Authentication.Msal в папке src/Components/WebAssembly/Authentication.Msal/src с помощью команды yarn build в командной оболочке.
  5. Если приложение использует сжатые ресурсы (Brotli/Gzip), необходимо сжать файл Interop/dist/Release/AuthenticationService.js.
  6. Скопируйте файл AuthenticationService.js и сжатые версии (.br/.gz) файла, если они созданы, из папки Interop/dist/Release в папку publish/wwwroot/_content/Microsoft.Authentication.WebAssembly.Msal приложения в опубликованных ресурсах приложения.

Передача параметров настраиваемого поставщика

Определите класс для передачи данных в базовую библиотеку JavaScript.

Внимание

Структура класса должна соответствовать ожидаемой в библиотеке, если JSON сериализуется с помощью System.Text.Json.

В следующем примере демонстрируется класс ProviderOptions с атрибутами JsonPropertyName, которые соответствуют ожидаемым в библиотеке настраиваемого поставщика:

public class ProviderOptions
{
    public string? Authority { get; set; }
    public string? MetadataUrl { get; set; }

    [JsonPropertyName("client_id")]
    public string? ClientId { get; set; }

    public IList<string> DefaultScopes { get; } = 
        new List<string> { "openid", "profile" };

    [JsonPropertyName("redirect_uri")]
    public string? RedirectUri { get; set; }

    [JsonPropertyName("post_logout_redirect_uri")]
    public string? PostLogoutRedirectUri { get; set; }

    [JsonPropertyName("response_type")]
    public string? ResponseType { get; set; }

    [JsonPropertyName("response_mode")]
    public string? ResponseMode { get; set; }
}
public class ProviderOptions
{
    public string Authority { get; set; }
    public string MetadataUrl { get; set; }

    [JsonPropertyName("client_id")]
    public string ClientId { get; set; }

    public IList<string> DefaultScopes { get; } = 
        new List<string> { "openid", "profile" };

    [JsonPropertyName("redirect_uri")]
    public string RedirectUri { get; set; }

    [JsonPropertyName("post_logout_redirect_uri")]
    public string PostLogoutRedirectUri { get; set; }

    [JsonPropertyName("response_type")]
    public string ResponseType { get; set; }

    [JsonPropertyName("response_mode")]
    public string ResponseMode { get; set; }
}

Зарегистрируйте параметры поставщика в системе внедрения зависимостей и задайте соответствующие значения:

builder.Services.AddRemoteAuthentication<RemoteAuthenticationState, RemoteUserAccount,
    ProviderOptions>(options => {
        options.Authority = "...";
        options.MetadataUrl = "...";
        options.ClientId = "...";
        options.DefaultScopes = new List<string> { "openid", "profile", "myApi" };
        options.RedirectUri = "https://localhost:5001/authentication/login-callback";
        options.PostLogoutRedirectUri = "https://localhost:5001/authentication/logout-callback";
        options.ResponseType = "...";
        options.ResponseMode = "...";
    });

В предыдущем примере заданы URI перенаправления с использованием обычных строковых литералов. Доступны следующие варианты:

Дополнительные ресурсы