ASP.NET Core Blazor WebAssembly 其他安全方案

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

本文介绍 Blazor WebAssembly 应用的其他安全方案。

将令牌附加到传出请求

AuthorizationMessageHandler 是用于处理访问令牌的 DelegatingHandler。 令牌是使用由框架注册的 IAccessTokenProvider 服务获取的。 如果无法获取令牌,则会引发 AccessTokenNotAvailableExceptionAccessTokenNotAvailableExceptionRedirect 方法使用给定的 AccessTokenResult.InteractionOptions 导航到 AccessTokenResult.InteractiveRequestUrl 以允许刷新访问令牌。

为了方便起见,框架提供 BaseAddressAuthorizationMessageHandler,它预先配置了应用基址作为授权的 URL。 仅当请求 URI 在应用的基 URI 中时,才会添加访问令牌。 当传出请求 URI 不在应用的基 URI 中时,请使用自定义 AuthorizationMessageHandler 类(推荐)配置 AuthorizationMessageHandler

注意

除了用于服务器 API 访问的客户端应用配置之外,在客户端和服务器不位于同一基址时,服务器 API 还必须允许跨域请求 (CORS)。 有关服务器端 CORS 配置的详细信息,请参阅本文后面的跨域资源共享 (CORS) 部分。

如下示例中:

在下面的示例中,HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http 中的扩展。 将包添加到尚未引用它的应用。

注意

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(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 WebAssembly 项目模板的托管Blazor解决方案,默认情况下,请求 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 的新实例上使用一次或多次以下方法来管理新标识提供者访问令牌请求的其他参数:

在通过 Web API 获取 JSON 数据的以下示例中,如果访问令牌不可用(引发了 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 以用作 HttpClientDelegatingHandlerConfigureHandler 配置此处理程序,以使用访问令牌授权出站 HTTP 请求。 仅当至少有一个授权 URL 是请求 URI (HttpRequestMessage.RequestUri) 的基 URI 时,才附加访问令牌。

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.readexample.write 是一般示例,不是为了反映任何特定提供程序的有效作用域。

Program 文件中,CustomAuthorizationMessageHandler 注册为临时服务,并配置为由已命名的 HttpClient 发出的传出 HttpResponseMessage 实例的 DelegatingHandler

在下面的示例中,HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http 中的扩展。 将包添加到尚未引用它的应用。

注意

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。

builder.Services.AddTransient<CustomAuthorizationMessageHandler>();

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

备注

在前面的示例中,CustomAuthorizationMessageHandlerDelegatingHandler 被注册为 AddHttpMessageHandler 的临时服务。 建议对 IHttpClientFactory 进行临时注册,用于管理其自己的 DI 范围。 有关详细信息,请参阅以下资源:

对于基于 Blazor WebAssembly 项目模板的托管 Blazor 解决方案,默认情况下,IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) 会分配给 HttpClient.BaseAddress

配置的 HttpClient 用于使用 try-catch 模式发出授权的请求。 其中使用 CreateClientMicrosoft.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

可以使用 ConfigureHandler 方法将 AuthorizationMessageHandler 配置为授权的 URL、作用域和返回 URL。 ConfigureHandler 配置此处理程序,以使用访问令牌授权出站 HTTP 请求。 仅当至少有一个授权 URL 是请求 URI (HttpRequestMessage.RequestUri) 的基 URI 时,才附加访问令牌。 如果请求 URI 是相对 URI,则将与 BaseAddress 结合使用。

在下面的示例中,AuthorizationMessageHandlerProgram 文件中配置了 HttpClient

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.readexample.write 是一般示例,不是为了反映任何特定提供程序的有效作用域。

对于基于 Blazor WebAssembly 项目模板的托管 Blazor 解决方案,默认情况下,IWebAssemblyHostEnvironment.BaseAddress 会分配给:

类型化 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.AddHttpClientMicrosoft.Extensions.Http 中的扩展。 将包添加到尚未引用它的应用。

注意

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(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 WebAssembly 项目模板的托管 Blazor 解决方案,默认情况下,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.AddHttpClientMicrosoft.Extensions.Http 中的扩展。 将包添加到尚未引用它的应用。

注意

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(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.readexample.write 是一般示例,不是为了反映任何特定提供程序的有效作用域。

对于基于 Blazor WebAssembly 项目模板的托管 Blazor 解决方案,默认情况下,IWebAssemblyHostEnvironment.BaseAddress 会分配给:

使用安全默认客户端的应用中未经身份验证或未经授权的 Web API 请求

通常使用安全的默认 HttpClient 的应用还可以通过配置命名的 HttpClient 来发出未经身份验证或未经授权的 Web API 请求。

在下面的示例中,HttpClientFactoryServiceCollectionExtensions.AddHttpClientMicrosoft.Extensions.Http 中的扩展。 将包添加到尚未引用它的应用。

注意

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。

Program 文件中:

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

对于基于 Blazor WebAssembly 项目模板的托管 Blazor 解决方案,默认情况下,IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) 会分配给 HttpClient.BaseAddress

先前的注册是对现有安全默认 HttpClient 注册的补充。

组件从 IHttpClientFactoryMicrosoft.Extensions.Http 包)创建 HttpClient以发出未经身份验证或未经授权的请求:

@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。 Microsoft 身份验证库 (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} 占位符是自定义作用域。

注意

当用户首次使用在 Microsoft Azure 中注册的应用时,AdditionalScopesToConsent 无法通过 Microsoft Entra ID 同意 UI 为 Microsoft Graph 预配委派的用户权限。 有关详细信息,请参阅将 Graph API 和 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 返回:

  • 如果使用 token,则为 true
  • 如果未检索到令牌,则为 false

跨源资源共享 (CORS)

在 CORS 请求中发送凭据(授权 cookie/标头)时,CORS 策略必须允许使用 Authorization 标头。

以下策略包括的配置用于:

  • 请求来源(http://localhost:5000https://localhost:5001)。
  • Any 方法(谓词)。
  • Content-TypeAuthorization 标头。 若要允许使用自定义标头(例如 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 WebAssembly 项目模板的托管 Blazor 解决方案对客户端和服务器应用使用相同的基址。 默认情况下,将客户端应用的 HttpClient.BaseAddress 设置为 builder.HostEnvironment.BaseAddress 的 URI。 CORS 配置不是托管 Blazor 解决方案默认配置中的必需配置。 不由服务器项目托管,并且不共享服务器应用的基址的其他客户端应用在服务器项目中需要 CORS 配置。

有关详细信息,请参阅在 ASP.NET Core 中启用跨源请求 (CORS) 和示例应用的 HTTP 请求测试器组件 (Components/HTTPRequestTester.razor)。

处理令牌请求错误

当单页应用程序 (SPA) 使用 Open ID Connect (OIDC) 对用户进行身份验证时,身份验证状态将以会话 cookie(因用户提供其凭据而设置)的形式在 SPA 内和 Identity 提供者 (IP) 中进行本地维护。

IP 为用户发出的令牌通常在短时间(约 1 小时)内有效,因此客户端应用必须定期提取新令牌。 否则,在授予的令牌到期后,将注销用户。 在大多数情况下,由于身份验证状态或“会话”保留在 IP 中,因此 OIDC 客户端能够预配新令牌,而无需用户再次进行身份验证。

在某些情况下,如果没有用户交互,客户端就无法获得令牌(例如,当用户出于某种原因而明确从 IP 注销时)。 如果用户访问 https://login.microsoftonline.com 并注销,则会发生这种情况。在这些场景下,应用不会立即知道用户已注销。客户端持有的任何令牌都可能不再有效。 另外,当前令牌过期后,如果没有用户交互,客户端将无法预配新令牌。

这些场景并不特定于基于令牌的身份验证。 它们本就是 SPA 的一部分。 如果删除了身份验证cookie,则使用 cookie 的 SPA 也无法调用服务器 API。

当应用对受保护的资源执行 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; }
    }
}

在进行身份验证操作之前使用会话存储和状态容器保存应用状态

在身份验证操作过程中,有时要在将浏览器重定向到 IP 之前保存应用状态。 其中一种情况是使用状态容器并希望在身份验证成功后恢复状态。 可以使用自定义身份验证状态对象来保留特定于应用的状态或对其的引用,并在身份验证操作成功完成期间恢复该状态。 下面的示例演示了该方法。

在应用中创建状态容器类,该类具有用于保存应用的状态值的属性。 在以下示例中,容器用于维护默认 Blazor 项目模板Counter 组件 (Counter.razor) 的计数器值。 用于序列化和反序列化容器的方法基于 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;
    }
}

RemoteAuthenticationState 创建 ApplicationAuthenticationState。 提供 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 序列化和反序列化方法(GetStateForLocalStorageSetStateFromLocalStorage)保存和还原应用的状态:

@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 配置为 Microsoft 身份验证库 (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" />

如果选择这样做,则允许将 UI 分成不同的页面。

自定义身份验证用户界面

RemoteAuthenticatorView 包括每个身份验证状态的一组默认 UI 片段。 可以通过传入自定义 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 的一个片段,该片段可用于每个身份验证路由。

路由 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 v1.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 安全组、ME-ID 管理员角色和自定义用户帐户类的其他示例,请参阅 ASP.NET Core Blazor WebAssembly 与 Microsoft Entra ID 组和角色

使用身份验证预呈现

目前不支持预呈现需要身份验证和授权的内容。 遵循任一 Blazor WebAssembly 安全应用主题中的指导后,请按照以下说明创建应用:

  • 预呈现不需要授权的路径。
  • 不预呈现需要授权的路径。

对于 Client 项目的 Program 文件,将常见服务注册的因素纳入单独的方法(例如在 Client 项目中创建 ConfigureCommonServices 方法)。 常见服务是开发人员注册以供客户端和服务器项目使用的服务。

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

Server 项目的 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);
}

有关Blazor 框架服务器身份验证提供程序 (ServerAuthenticationStateProvider) 的详细信息,请参阅 ASP.NET Core Blazor 身份验证和授权

Server 项目的 Pages/_Host.cshtml 文件中,将 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

使用针对第三方 API 提供程序的客户端 OAuth 流对用户进行身份验证:

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

在本方案中:

  • 托管应用的服务器不会发挥作用。
  • 无法保护服务器上的 API。
  • 应用只能调用受保护的第三方 API。

使用第三方提供程序对用户进行身份验证,并在主机服务器和第三方调用受保护的 API

使用第三方登录提供程序配置 Identity。 获取第三方 API 访问所需的令牌并进行存储。

当用户登录时,Identity 将在身份验证过程中收集访问令牌和刷新令牌。 此时,可通过几种方法向第三方 API 进行 API 调用。

使用服务器访问令牌检索第三方访问令牌

使用服务器上生成的访问令牌从服务器 API 终结点检索第三方访问令牌。 在此处,使用第三方访问令牌直接从客户端上的 Identity 调用第三方 API 资源。

不建议使用此方法。此方法需要将第三方访问令牌视为针对公共客户端生成。 在 OAuth 范畴,公共应用没有客户端机密,因为不能信任此类应用可以安全地存储机密,将为机密客户端生成访问令牌。 机密客户端具有客户端机密,并且假定能够安全地存储机密。

  • 第三方访问令牌可能会被授予其他作用域,以便基于第三方为更受信任的客户端发出令牌的情况执行敏感操作。
  • 同样,不应向不受信任的客户端颁发刷新令牌,因为这样做会给客户端提供无限制的访问权限,除非存在其他限制。

从客户端向服务器 API 发出 API 调用以便调用第三方 API

从客户端向服务器 API 发出 API 调用。 从服务器中检索第三方 API 资源的访问令牌,并发出任何所需调用。

建议使用此方法。尽管此方法需要额外的网络跃点通过服务器来调用第三方 API,但最终可提供更安全的体验:

  • 服务器可以存储刷新令牌,并确保应用不会失去对第三方资源的访问权限。
  • 应用无法从服务器泄漏可能包含更多敏感权限的访问令牌。

使用 OpenID Connect (OIDC) v2.0 终结点

身份验证库和 Blazor 项目模板使用 Open ID Connect (OIDC) v1.0 终结点。 要使用 v2.0 终结点,请配置 JWT 持有者 JwtBearerOptions.Authority 选项。 在下面的示例中,通过向 Authority 属性追加 v2.0 段,将 ME-ID 配置为 v2.0:

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 属性。 使用 Authority 键在 JwtBearerOptions 或应用设置文件 (appsettings.json) 中设置属性。

ID 令牌中的声明列表针对 v2.0 终结点会发生更改。 有关更改的 Microsoft 文档已停用,但在 ID 令牌声明参考中提供了有关 ID 令牌中的声明的指导。

在组件中配置和使用 gRPC

若要将 Blazor WebAssembly 应用配置为使用 ASP.NET Core gRPC 框架

注意

默认情况下,在 Blazor Web 应用中启用预呈现,因此必须首先考虑服务器的组件呈现,然后考虑客户端的。 任何预呈现状态都应流向客户端,以便可以重复使用它。 有关详细信息,请参阅 Prerender ASP.NET Core Razor 组件

注意

默认情况下,在托管的 Blazor WebAssembly 应用中启用预呈现,因此必须首先考虑服务器的组件呈现,然后考虑客户端的。 任何预呈现状态都应流向客户端,以便可以重复使用它。 有关详细信息,请参阅预呈现和集成 ASP.NET Core Razor 组件

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.razor) 发出 gRPC 调用:

@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 或更高版本。

有关详细信息,请参阅 ASP.NET Core gRPC 应用中的 gRPC-Web

替换 AuthenticationService 实现

下面的小节说明了如何替换:

  • 任何 JavaScript AuthenticationService 实现。
  • 适用于 JavaScript 的 Microsoft 身份验证库 (MSAL.js)。

替换任何 JavaScript AuthenticationService 实现

创建一个 JavaScript 库来处理自定义身份验证详细信息。

警告

本部分的指南是默认 RemoteAuthenticationService<TRemoteAuthenticationState,TAccount,TProviderOptions> 的实现详细信息。 本部分的 TypeScript 代码仅适用于 .NET 7 中的 ASP.NET Core,在即将发布的 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> 标记替换为从 wwwroot/js 文件夹中加载 CustomAuthenticationService.js 库的标记。

</body> 结束标记内的 Blazor 脚本 (_framework/blazor.webassembly.js) 前的 wwwroot/index.html 中:

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

有关详细信息,请参阅 dotnet/aspnetcore GitHub 存储库中的 AuthenticationService.ts

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

替换适用于 JavaScript 的 Microsoft 身份验证库 (MSAL.js)

如果应用需要适用于 JavaScript 的 Microsoft 身份验证库 (MSAL.js) 的自定义版本,请执行以下步骤:

  1. 确认系统具有最新的开发人员 .NET SDK 或从 .NET Core SDK:安装程序和二进制文件获取并安装最新的开发人员 SDK。 此方案不需要配置内部 NuGet 源。
  2. 设置 dotnet/aspnetcore GitHub 存储库,以按照从源生成 ASP.NET Core 中的文档进行开发。 分叉和克隆或下载 dotnet/aspnetcore GitHub 存储库的 ZIP 存档。
  3. 打开 src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json 文件,并设置所需的 @azure/msal-browser 版本。 有关已发布版本列表,请访问 @azure/msal-browser npm 网站,然后选择“版本”选项卡。
  4. 使用命令行界面中的 yarn build 命令在 src/Components/WebAssembly/Authentication.Msal/src 文件夹中生成 Authentication.Msal 项目。
  5. 如果应用使用压缩资产 (Brotli/Gzip),则压缩 Interop/dist/Release/AuthenticationService.js 文件。
  6. 如果生成了文件,请将该文件的 AuthenticationService.js 文件和压缩版本 (.br/.gz) 从 Interop/dist/Release 文件夹复制到应用的已发布资产中应用的 publish/wwwroot/_content/Microsoft.Authentication.WebAssembly.Msal 文件夹中。

传递自定义提供程序选项

定义一个用于将数据传递到基础 JavaScript 库的类。

重要

类的结构必须与使用 System.Text.Json 序列化 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; }
}

在 DI 系统内注册提供程序选项并配置相应的值:

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。 以下是可用的替代方法:

  • 使用 IWebAssemblyHostEnvironment.BaseAddressTryCreate

    Uri.TryCreate(
        $"{builder.HostEnvironment.BaseAddress}authentication/login-callback", 
        UriKind.Absolute, out var redirectUri);
    options.RedirectUri = redirectUri;
    
  • 主机生成器配置

    options.RedirectUri = builder.Configuration["RedirectUri"];
    

    wwwroot/appsettings.json

    {
      "RedirectUri": "https://localhost:5001/authentication/login-callback"
    }
    

其他资源