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

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

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

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

Примечание

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

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

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

...

builder.Services.AddHttpClient("ServerAPI", 
        client => client.BaseAddress = new Uri("https://www.example.com/base"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

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

Для размещенного решения 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()
{
    private ExampleType[] examples;

    try
    {
        examples = 
            await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

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

Пользовательский класс 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 navigationManager)
        : base(provider, navigationManager)
    {
        ConfigureHandler(
            authorizedUrls: new[] { "https://www.example.com/base" },
            scopes: new[] { "example.read", "example.write" });
    }
}

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

builder.Services.AddScoped<CustomAuthorizationMessageHandler>();

builder.Services.AddHttpClient("ServerAPI",
        client => client.BaseAddress = new Uri("https://www.example.com/base"))
    .AddHttpMessageHandler<CustomAuthorizationMessageHandler>();

Для размещенного решения 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 {
    private ExampleType[] examples;

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

            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.Main (Program.cs):

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

...

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

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

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

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

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

WeatherForecastClient.cs:

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

public class WeatherForecastClient
{
    private readonly HttpClient http;
 
    public WeatherForecastClient(HttpClient http)
    {
        this.http = http;
    }
 
    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        var forecasts = new WeatherForecast[0];

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

        return forecasts;
    }
}

Заполнитель {APP ASSEMBLY} — это имя сборки приложения (например, using static BlazorSample.Data;).

Program.Main (Program.cs):

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

...

builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://www.example.com/base"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

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

Компонент FetchData (Pages/FetchData.razor):

@inject WeatherForecastClient Client

...

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

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

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

Program.Main (Program.cs):

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

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

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

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

Если приложение Blazor WebAssembly обычно использует защищенный клиент HttpClient по умолчанию, приложение также может выполнять неавторизованные или неаутентифицированные запросы к веб-API путем настройки именованного клиента HttpClient:

Program.Main (Program.cs):

builder.Services.AddHttpClient("ServerAPI.NoAuthenticationClient", 
    client => client.BaseAddress = new Uri("https://www.example.com/base"));

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

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

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

@inject IHttpClientFactory ClientFactory

...

@code {
    private WeatherForecast[] forecasts;

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

        forecasts = await client.GetFromJsonAsync<WeatherForecast[]>(
            "WeatherForecastNoAuthentication");
    }
}

Примечание

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

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

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

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

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

Program.Main (Program.cs):

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

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

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

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

В компоненте 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 Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider
...

<EditForm Model="User" @onsubmit="OnSaveAsync">
    <label>User
        <InputText @bind-Value="User.Name" />
    </label>
    <label>Last name
        <InputText @bind-Value="User.LastName" />
    </label>
</EditForm>

@code {
    public class Profile
    {
        public string Name { get; set; }
        public string LastName { get; set; }
    }

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

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

        if (currentQuery.Contains("state=resumeSavingProfile"))
        {
            User = await JS.InvokeAsync<Profile>("sessionStorage.getState", 
                "resumeSavingProfile");
        }
    }

    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.setState", 
                "resumeSavingProfile", User);
            Navigation.NavigateTo(tokenResult.RedirectUrl);
        }
    }
}

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

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

В приложении создается класс контейнера состояния со свойствами для хранения значений состояния приложения. В приведенном ниже примере контейнер используется для хранения значения счетчика для компонента Counter (Pages/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; }
}

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

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

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

В этом примере для проверки подлинности используется Azure Active Directory (AAD). В Program.Main (Program.cs):

  • ApplicationAuthenticationState настраивается с типом RemoteAuthenticationState из библиотеки проверки подлинности Майкрософт (MSAL).
  • Контейнер состояния регистрируется в контейнере службы.
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 (Pages/Authentication.razor):

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

<RemoteAuthenticatorView Action="@Action" />

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

Program.Main (Program.cs):

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 (Pages/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; }
}

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

Маршрут Fragment
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 присваивается пользовательский массив значений свойств JSON amr. AuthenticationMethod заполняется платформой автоматически при проверке подлинности пользователя.

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 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 navigationManager, 
        IAccessTokenProviderAccessor accessor) : base(accessor)
    {
    }
  
    public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity.IsAuthenticated)
        {
            foreach (var value in account.AuthenticationMethod)
            {
                ((ClaimsIdentity)initialUser.Identity)
                    .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>();
    

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

Дополнительный пример, который работает с группами безопасности AAD, ролями администратора AAD и настраиваемым классом учетной записи пользователя, см. в разделе ASP.NET Core Blazor WebAssembly с группами и ролями Azure Active Directory.

Поддержка предварительной отрисовки с помощью проверки подлинности

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

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

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

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.RootComponents.Add...;

        // Services that only the client app uses
        builder.Services.AddScoped( ... );

        ConfigureCommonServices(builder.Services);

        await builder.Build().RunAsync();
    }

    public static void ConfigureCommonServices(IServiceCollection services)
    {
        // Common service registrations that both apps use
        services.Add...;
    }
}

В Startup.ConfigureServices серверного приложения зарегистрируйте следующие дополнительные службы и вызовите 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);
}

В методе Startup.Configure серверного приложения замените endpoints.MapFallbackToFile("index.html") на endpoints.MapFallbackToPage("/_Host"):

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
    endpoints.MapFallbackToPage("/_Host");
});

В серверном приложении создайте папку Pages, если она отсутствует. В папке серверного приложения Pages создайте страницу _Host.cshtml. Вставьте содержимое из файла wwwroot/index.html клиентского приложения в файл Pages/_Host.cshtml. Обновите содержимое файла:

  • Добавьте @page "_Host" в начало файла.

  • Замените тег <div id="app">Loading...</div> следующим:

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

    Заполнитель {CLIENT APP ASSEMBLY NAME} в предыдущем примере — это имя сборки клиентского приложения (например, BlazorSample.Client).

  • Добавьте @page "_Host" в начало файла.

  • Замените тег <app>Loading...</app> следующим:

    <app>
        @if (!HttpContext.Request.Path.StartsWithSegments("/authentication"))
        {
            <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
                render-mode="Static" />
        }
        else
        {
            <text>Loading...</text>
        }
    </app>
    

    Заполнитель {CLIENT APP ASSEMBLY NAME} в предыдущем примере — это имя сборки клиентского приложения (например, BlazorSample.Client).

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

При аутентификации и авторизации размещенного приложения 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. В следующем примере для AAD настраивается версия 2.0 путем добавления сегмента v2.0 к свойству Authority:

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

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

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

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

Список утверждений в маркере идентификатора отличается для конечных точек версии 2.0. Дополнительные сведения см. в статье Зачем выполнять обновление до платформы удостоверений Майкрософт (версия 2.0)?

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

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

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;
using {APP ASSEMBLY}.Shared;

...

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

Заполнитель {APP ASSEMBLY} — это имя сборки приложения (например, BlazorSample). Поместите файл .proto в проект Shared размещенного решения Blazor.

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

@page "/grpc"
@attribute [Authorize]
@using Microsoft.AspNetCore.Authorization
@using {APP ASSEMBLY}.Shared
@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();
        }
    }
}

Заполнитель {APP ASSEMBLY} — это имя сборки приложения (например, BlazorSample). Чтобы использовать свойство Status.DebugException, используйте Grpc.Net.Client версии 2.30.0 или более поздней.

Для получения дополнительной информации см. Использование gRPC в приложениях на основе браузера.

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

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

  1. Убедитесь, что в системе установлен последний пакет SDK для разработчиков .NET, или получите и установите последнюю версию пакета SDK для разработчиков, как описано здесь. Для этого сценария настройка внутренних веб-каналов NuGet не требуется.
  2. Настройте репозиторий GitHub dotnet/aspnetcore для разработки, как описано здесь. Создайте вилку и клонируйте или скачайте 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 приложения в опубликованных ресурсах приложения.

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