ASP.NET 核心服务器端和其他 Blazor Web App 安全方案

注意

此版本不是本文的最新版本。 有关当前版本,请参阅本文.NET 9 版本。

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅本文.NET 9 版本。

重要

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

有关当前版本,请参阅本文.NET 9 版本。

本文介绍如何为其他安全方案配置服务器端 Blazor,其中包括如何将令牌传递给 Blazor 应用。

注意

本文中的代码示例采用在 .NET 6 或更高版本的 ASP.NET Core 中支持的可为空的引用类型 (NRT) 和 .NET 编译器 Null 状态静态分析。 当面向 .NET 5 或更早版本时,请从文章示例中的?string?TodoItem[]?WeatherForecast[]?类型中删除 null 类型指定(IEnumerable<GitHubBranch>?)。

将令牌传递到服务器端 Blazor 应用

本部分适用于 Blazor Web Apps。 有关 Blazor Server,请查看 本文部分的 .NET 7 版本

如果只想使用访问令牌从Blazor Web App命名 HTTP 客户端进行 Web API 调用,请参阅“使用 Web API 调用的令牌处理程序”部分,其中说明了如何使用DelegatingHandler实现将用户的访问令牌附加到传出请求。 本节中的以下指南适用于开发人员,这些指南针对服务器端需要访问令牌、刷新令牌及其他身份验证属性的其他用途。

若要在 Blazor Web App 保存令牌和其他身份验证属性以供服务器端使用,建议使用 IHttpContextAccessor/HttpContextIHttpContextAccessorHttpContext)。 如果在静态服务器端渲染(静态 SSR)或预渲染期间获取令牌,那么从HttpContext读取令牌(包括作为级联参数使用IHttpContextAccessor)是支持的,以便在交互式服务器渲染期间使用这些令牌。 但是,如果用户在建立连接后进行身份验证,则不会更新令牌,因为在 HttpContext 连接开始时已经捕获了 SignalR。 此外, AsyncLocal<T> 使用方式 IHttpContextAccessor 意味着在读取 HttpContext执行上下文之前必须小心不要丢失执行上下文。 有关详细信息,请参阅 ASP.NET Core Blazor 应用中IHttpContextAccessor/HttpContext。

在服务类中,获取对命名空间Microsoft.AspNetCore.Authentication成员的访问权限,以便在GetTokenAsync上显示HttpContext方法。 在以下示例中被注释掉的替代方法是调用 AuthenticateAsyncHttpContext 上。 对返回 AuthenticateResult.Properties调用 GetTokenValue

using Microsoft.AspNetCore.Authentication;

public class AuthenticationProcessor(IHttpContextAccessor httpContextAccessor)
{
    public async Task<string?> GetAccessToken()
    {
        if (httpContextAccessor.HttpContext is null)
        {
            throw new Exception("HttpContext not available");
        }

        // Approach 1: Call 'GetTokenAsync'
        var accessToken = await httpContextAccessor.HttpContext
            .GetTokenAsync("access_token");

        // Approach 2: Authenticate the user and call 'GetTokenValue'
        /*
        var authResult = await httpContextAccessor.HttpContext.AuthenticateAsync();
        var accessToken = authResult?.Properties?.GetTokenValue("access_token");
        */

        return accessToken;
    }
}

该服务在服务器项目的 Program 文件中注册:

builder.Services.AddScoped<AuthenticationProcessor>();

AuthenticationProcessor可以注入到服务器端服务中,例如用于预配置DelegatingHandlerHttpClient。 以下示例仅用于演示目的,或者如果需要在服务中 AuthenticationProcessor 执行特殊处理,因为可以直接注入 IHttpContextAccessor 并获取用于调用外部 Web API 的令牌(有关直接使用 IHttpContextAccessor 直接调用 Web API 的详细信息,请参阅“ 使用 Web API 调用令牌处理程序 ”部分)。

using System.Net.Http.Headers;

public class TokenHandler(AuthenticationProcessor authProcessor) : 
    DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var accessToken = authProcessor.GetAccessToken();

        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);

        return await base.SendAsync(request, cancellationToken);
    }
}

令牌处理程序已注册,并充当文件中命名 HTTP 客户端 Program 的委派处理程序:

builder.Services.AddHttpContextAccessor();

builder.Services.AddScoped<TokenHandler>();

builder.Services.AddHttpClient("ExternalApi",
      client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiUri"] ?? 
          throw new Exception("Missing base address!")))
      .AddHttpMessageHandler<TokenHandler>();

谨慎

确保令牌永远不会被客户端(.Client 项目)传输和处理,例如在采用交互式自动呈现的组件中,或由客户端侧服务进行渲染。 始终让客户端调用服务器(项目)以使用令牌处理请求。 令牌和其他身份验证数据不应离开服务器。

有关交互式自动组件,请参阅 ASP.NET 核心 Blazor 身份验证和授权,其中演示了如何在服务器上保留访问令牌和其他身份验证属性。 此外,请考虑采用后端-前端(BFF)模式,它采用类似的调用结构,并在使用 OpenID Connect(OIDC)保护的 ASP.NET Core 中描述,以及在使用 Microsoft Entra ID 保护的 ASP.NET Core 中描述,以实现安全性。

对 Web API 调用使用令牌处理程序

以下方法旨在将用户的访问令牌附加到传出请求,特别是对外部 Web API 应用进行 Web API 调用。 此方法适用于采用全局交互式服务器呈现的Blazor Web App,但相同的一般方法也适用于采用全局交互式自动呈现模式的Blazor Web App。 请记住的重要概念是,仅在服务器上执行访问 HttpContext 时才使用 IHttpContextAccessor

有关本部分中指南的演示,请参阅 BlazorWebAppOidcBlazorWebAppOidcServerGitHub 存储库中的Blazor示例应用(.NET 8 或更高版本)。 这些示例采用全局交互式呈现模式,并与 Microsoft Entra 配合使用 OIDC 身份验证,而无需使用特定于 Entra 的包。 这些示例演示如何传递 JWT 访问令牌来调用安全的 Web API。

Microsoft标识平台与用于Identity的Microsoft Web包提供一个API,用于通过自动令牌管理和续订从Blazor Web App中调用Web API。 有关详细信息,请参阅 使用 Microsoft Entra ID 保护 ASP.NET 核心Blazor Web App,以及 BlazorWebAppEntra 样本 GitHub 存储库中的 BlazorWebAppEntraBffBlazor 示例应用程序(.NET 9 或更高版本)

用于将用户访问令牌附加到传出请求的子类 DelegatingHandler 。 令牌处理程序仅在服务器上执行,因此使用 HttpContext 是安全的。

TokenHandler.cs:

using System.Net.Http.Headers;
using Microsoft.AspNetCore.Authentication;

public class TokenHandler(IHttpContextAccessor httpContextAccessor) : 
    DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (httpContextAccessor.HttpContext is null)
        {
            throw new Exception("HttpContext not available");
        }

        var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");

        if (accessToken is null)
        {
            throw new Exception("No access token");
        }

        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);

        return await base.SendAsync(request, cancellationToken);
    }
}

注意

有关如何从 AuthenticationStateProvider 访问 DelegatingHandler 的指南,请参阅 传出请求中间件中的访问 AuthenticationStateProvider 部分。

在项目的Program文件中,令牌处理程序(TokenHandler)被注册为一个作用域服务,并指定为具有AddHttpMessageHandler的消息处理程序。

在下面的示例中,{HTTP CLIENT NAME} 占位符是 HttpClient 的名称,{BASE ADDRESS} 占位符是 Web API 的基址 URI。 有关AddHttpContextAccessor的详细信息,请参阅ASP.NET Core Blazor 应用中的 IHttpContextAccessor/HttpContext

Program.cs中:

builder.Services.AddHttpContextAccessor();

builder.Services.AddScoped<TokenHandler>();

builder.Services.AddHttpClient("{HTTP CLIENT NAME}",
      client => client.BaseAddress = new Uri("{BASE ADDRESS}"))
      .AddHttpMessageHandler<TokenHandler>();

示例:

builder.Services.AddScoped<TokenHandler>();

builder.Services.AddHttpClient("ExternalApi",
      client => client.BaseAddress = new Uri("https://localhost:7277"))
      .AddHttpMessageHandler<TokenHandler>();

可以从配置builder.Configuration["{CONFIGURATION KEY}"]中提供 HTTP 客户端基址,其中{CONFIGURATION KEY}占位符是配置密钥:

new Uri(builder.Configuration["ExternalApiUri"] ?? throw new IOException("No URI!"))

appsettings.json中,指定ExternalApiUri。 以下示例将值设置为外部 Web API 的 localhost 地址:https://localhost:7277

"ExternalApiUri": "https://localhost:7277"

此时,组件创建的HttpClient可以发出安全的Web API请求。 在以下示例中,{REQUEST URI} 是相对请求 URI,而 {HTTP CLIENT NAME} 占位符是 HttpClient 的名称。

using var request = new HttpRequestMessage(HttpMethod.Get, "{REQUEST URI}");
var client = ClientFactory.CreateClient("{HTTP CLIENT NAME}");
using var response = await client.SendAsync(request);

示例:

using var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
var client = ClientFactory.CreateClient("ExternalApi");
using var response = await client.SendAsync(request);

计划为Blazor增加的其他功能由Access AuthenticationStateProvider跟踪,在传出请求中间件dotnet/aspnetcore(#52379)中实现。 在交互式服务器模式 (dotnet/aspnetcore #52390) 中向 HttpClient 提供访问令牌时出现问题 是一个封闭的问题,其中包含高级用例的有用讨论和潜在解决方法策略。

可使用本部分中介绍的方法将服务器端 Razor 应用中的 Blazor 组件外部可用的令牌传递给组件。 本部分中的示例重点介绍如何将访问、刷新和反请求伪造 (XSRF) 令牌传递给 Blazor 应用,但此方法对其他 HTTP 上下文状态有效。

注意

在组件 POST 到 Razor 或其他需要验证的终结点的情况下,将 XSRF 令牌传递给 Identity 组件非常有用。 如果应用只需要访问和刷新令牌,则可以从以下示例中删除 XSRF 令牌代码。

像对常规 Razor Pages 或 MVC 应用操作那样对你的应用进行身份验证。 预配令牌并将其保存到身份验证 cookie。

Program 文件中:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});

Startup.cs中:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});

Startup.cs中:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});

可选择使用 options.Scope.Add("{SCOPE}"); 添加其他作用域,其中占位符 {SCOPE} 是要添加的其他作用域。

定义可在 应用中使用的作用域令牌提供程序服务,以解析Blazor 中的令牌。

TokenProvider.cs:

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

Program 文件中,为以下对象添加服务:

  • IHttpClientFactory:用于 WeatherForecastService 类,该类使用访问令牌从服务器 API 获取天气数据。
  • TokenProvider:保留访问令牌和刷新令牌。
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();

Startup.ConfigureServicesStartup.cs 中,为以下对象添加服务:

  • IHttpClientFactory:用于 WeatherForecastService 类,该类使用访问令牌从服务器 API 获取天气数据。
  • TokenProvider:保留访问令牌和刷新令牌。
services.AddHttpClient();
services.AddScoped<TokenProvider>();

定义一个类,用于在初始应用状态下使用访问令牌和刷新令牌传递它。

InitialApplicationState.cs:

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

Pages/_Host.cshtml 文件中,创建 InitialApplicationState 实例,并将其作为参数传递给应用:

Pages/_Layout.cshtml 文件中,创建 InitialApplicationState 实例,并将其作为参数传递给应用:

Pages/_Host.cshtml 文件中,创建 InitialApplicationState 实例,并将其作为参数传递给应用:

@using Microsoft.AspNetCore.Authentication
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf

...

@{
    var tokens = new InitialApplicationState
    {
        AccessToken = await HttpContext.GetTokenAsync("access_token"),
        RefreshToken = await HttpContext.GetTokenAsync("refresh_token"),
        XsrfToken = Xsrf.GetAndStoreTokens(HttpContext).RequestToken
    };
}

<component ... param-InitialState="tokens" ... />

App 组件 (App.razor) 中,解析服务并使用参数中的数据对其进行初始化:

@inject TokenProvider TokenProvider

...

@code {
    [Parameter]
    public InitialApplicationState? InitialState { get; set; }

    protected override Task OnInitializedAsync()
    {
        TokenProvider.AccessToken = InitialState?.AccessToken;
        TokenProvider.RefreshToken = InitialState?.RefreshToken;
        TokenProvider.XsrfToken = InitialState?.XsrfToken;

        return base.OnInitializedAsync();
    }
}

注意

将初始状态分配给上一示例中的 TokenProvider 的替代方法是将数据复制到 OnInitializedAsync 内的作用域服务中,以供在整个应用中使用。

Microsoft.AspNet.WebApi.Client NuGet 包添加对应用的包引用。

注意

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

在发出安全 API 请求的服务中,注入令牌提供程序并检索 API 请求的令牌:

WeatherForecastService.cs:

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class WeatherForecastService
{
    private readonly HttpClient http;
    private readonly TokenProvider tokenProvider;

    public WeatherForecastService(IHttpClientFactory clientFactory, 
        TokenProvider tokenProvider)
    {
        http = clientFactory.CreateClient();
        this.tokenProvider = tokenProvider;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        var token = tokenProvider.AccessToken;
        using var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://localhost:5003/WeatherForecast");
        request.Headers.Add("Authorization", $"Bearer {token}");
        using var response = await http.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ?? 
            Array.Empty<WeatherForecast>();
    }
}

对于传递给组件的 XSRF 令牌,注入 TokenProvider 并将 XSRF 令牌添加到 POST 请求。 以下示例将令牌添加到注销终结点 POST。 在以下示例描述的场景中,注销终结点(Areas/Identity/Pages/Account/Logout.cshtml构建到应用)不会指定 IgnoreAntiforgeryTokenAttribute (@attribute [IgnoreAntiforgeryToken]),因为它除了执行必须保护的正常注销操作外,还会执行某些操作。 终结点需要有效的 XSRF 令牌才能成功地处理请求。

在向授权用户显示“注销”按钮的组件中:

@inject TokenProvider TokenProvider

...

<AuthorizeView>
    <Authorized>
        <form action="/Identity/Account/Logout?returnUrl=%2F" method="post">
            <button class="nav-link btn btn-link" type="submit">Logout</button>
            <input name="__RequestVerificationToken" type="hidden" 
                value="@TokenProvider.XsrfToken">
        </form>
    </Authorized>
    <NotAuthorized>
        ...
    </NotAuthorized>
</AuthorizeView>

设置身份验证方案

对于使用多个身份验证中间件并因此具有多个身份验证方案的应用,可以在 Blazor 文件的终结点配置中显式设置 Program 使用的方案。 以下示例设置 OpenID Connect (OIDC) 方案:

对于使用多个身份验证中间件并因此具有多个身份验证方案的应用,可以在 Blazor 的终结点配置中显式设置 Startup.cs 使用的方案。 以下示例设置 OpenID Connect (OIDC) 方案:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;

...

app.MapRazorComponents<App>().RequireAuthorization(
    new AuthorizeAttribute
    {
        AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
    })
    .AddInteractiveServerRenderMode();
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

...

app.MapBlazorHub().RequireAuthorization(
    new AuthorizeAttribute 
    {
        AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
    });

对于使用多个身份验证中间件并因此具有多个身份验证方案的应用,可以在 Blazor 的终结点配置中显式设置 Startup.Configure 使用的方案。 以下示例设置了 Microsoft Entra ID 方案:

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

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

在 .NET 5 之前的 ASP.NET Core 版本中,身份验证库和 Blazor 模板使用 OpenID Connect (OIDC) v1.0 终结点。 若要在 ASP.NET Core 版本(不包括 .NET 5)中使用 v2.0 终结点,请在 OpenIdConnectOptions.Authority 选项 OpenIdConnectOptions中进行配置:

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

也可以在应用设置 (appsettings.json) 文件中进行设置:

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

如果将段添加到授权不适合应用的 OIDC 提供程序(例如,使用非 ME-ID 提供程序),则直接设置 Authority 属性。 使用 OpenIdConnectOptions 键在 Authority 或应用设置文件中设置属性。

代码更改

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

  • 由于在 v2.0 终结点的范围 URI 中指定了资源,请删除 OpenIdConnectOptions.Resource 中的 OpenIdConnectOptions 属性设置:

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

应用 ID URI

  • 使用 v2.0 终结点时,API 会定义一个 App ID URI,来表示 API 的唯一标识符。
  • 所有作用域都将应用 ID URI 用作前缀,v2.0 终结点以应用 ID URI 为受众发出访问令牌。
  • 使用 V2.0 终结点时,服务器 API 中配置的客户端 ID 会从 API 应用程序 ID(客户端 ID)更改为应用 ID URI。

appsettings.json:

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

可以在 OIDC 提供程序应用注册说明中找到要使用的应用 ID URI。

用于捕获自定义服务用户的线路处理程序

使用 CircuitHandlerAuthenticationStateProvider 捕获用户,并在服务中设置用户。 如果要更新用户,请将回调注册到 AuthenticationStateChanged,并将 Task 排入队列以获取新用户和更新此服务。 下面的示例演示了该方法。

如下示例中:

  • 每次线路重新连接时调用 OnConnectionUpAsync,设置用户的连接生存期。 除非通过处理程序执行更新来实现身份验证更改(以下示例中的 OnConnectionUpAsync),否则仅需要 AuthenticationChanged 方法。
  • 调用 OnCircuitOpenedAsync 以附加身份验证更改的处理程序 AuthenticationChanged 才能对用户进行更新。
  • catch 任务的 UpdateAuthentication 块不对异常执行任何操作,因为此时无法在代码执行中报告异常。 如果从任务引发异常,则会在应用中的其他位置报告该异常。

UserService.cs:

using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;

public class UserService
{
    private ClaimsPrincipal currentUser = new(new ClaimsIdentity());

    public ClaimsPrincipal GetUser() => currentUser;

    internal void SetUser(ClaimsPrincipal user)
    {
        if (currentUser != user)
        {
            currentUser = user;
        }
    }
}

internal sealed class UserCircuitHandler(
        AuthenticationStateProvider authenticationStateProvider,
        UserService userService) 
        : CircuitHandler, IDisposable
{
    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        authenticationStateProvider.AuthenticationStateChanged += 
            AuthenticationChanged;

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    private void AuthenticationChanged(Task<AuthenticationState> task)
    {
        _ = UpdateAuthentication(task);

        async Task UpdateAuthentication(Task<AuthenticationState> task)
        {
            try
            {
                var state = await task;
                userService.SetUser(state.User);
            }
            catch
            {
            }
        }
    }

    public override async Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        var state = await authenticationStateProvider.GetAuthenticationStateAsync();
        userService.SetUser(state.User);
    }

    public void Dispose()
    {
        authenticationStateProvider.AuthenticationStateChanged -= 
            AuthenticationChanged;
    }
}
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;

public class UserService
{
    private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());

    public ClaimsPrincipal GetUser()
    {
        return currentUser;
    }

    internal void SetUser(ClaimsPrincipal user)
    {
        if (currentUser != user)
        {
            currentUser = user;
        }
    }
}

internal sealed class UserCircuitHandler : CircuitHandler, IDisposable
{
    private readonly AuthenticationStateProvider authenticationStateProvider;
    private readonly UserService userService;

    public UserCircuitHandler(
        AuthenticationStateProvider authenticationStateProvider,
        UserService userService)
    {
        this.authenticationStateProvider = authenticationStateProvider;
        this.userService = userService;
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        authenticationStateProvider.AuthenticationStateChanged += 
            AuthenticationChanged;

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    private void AuthenticationChanged(Task<AuthenticationState> task)
    {
        _ = UpdateAuthentication(task);

        async Task UpdateAuthentication(Task<AuthenticationState> task)
        {
            try
            {
                var state = await task;
                userService.SetUser(state.User);
            }
            catch
            {
            }
        }
    }

    public override async Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        var state = await authenticationStateProvider.GetAuthenticationStateAsync();
        userService.SetUser(state.User);
    }

    public void Dispose()
    {
        authenticationStateProvider.AuthenticationStateChanged -= 
            AuthenticationChanged;
    }
}

Program 文件中:

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

...

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

Startup.ConfigureServicesStartup.cs 中:

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

...

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

使用组件中的服务获取用户:

@inject UserService UserService

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

若要在 MVC、Razor Pages 和其他 ASP.NET Core 方案的中间件中设置用户,请在身份验证中间件运行后在自定义中间件中调用 SetUser 上的 UserService,或使用 IClaimsTransformation 实现设置用户。 以下示例采用中间件方法。

UserServiceMiddleware.cs:

public class UserServiceMiddleware
{
    private readonly RequestDelegate next;

    public UserServiceMiddleware(RequestDelegate next)
    {
        this.next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task InvokeAsync(HttpContext context, UserService service)
    {
        service.SetUser(context.User);
        await next(context);
    }
}

在即将调用 app.MapRazorComponents<App>() 文件中的 Program 之前,调用中间件:

在即将调用 app.MapBlazorHub() 文件中的 Program 之前,调用中间件:

在即将调用 app.MapBlazorHub()Startup.Configure 中的 Startup.cs 之前,调用中间件:

app.UseMiddleware<UserServiceMiddleware>();

在传出请求中间件中访问 AuthenticationStateProvider

使用 AuthenticationStateProvider 创建的针对 DelegatingHandlerHttpClient 中的 IHttpClientFactory 可使用线路活动处理程序在传出请求中间件中访问。

注意

如需了解关于通过在 ASP.NET Core 应用中使用 HttpClient 创建的 IHttpClientFactory 实例定义 HTTP 请求的委托处理程序的一般指导,可参阅在 ASP.NET Core 中使用 IHttpClientFactory 发出 HTTP 请求的以下部分:

以下示例使用 AuthenticationStateProvider 将经过身份验证的用户的自定义用户名标头附加到传出请求。

首先在 CircuitServicesAccessor 依赖项注入 (DI) 文章的以下部分中实现 Blazor 类:

从其他 DI 范围访问服务器端 Blazor 服务

使用 CircuitServicesAccessor 访问 AuthenticationStateProvider 实现中的 DelegatingHandler

AuthenticationStateHandler.cs:

using Microsoft.AspNetCore.Components.Authorization;

public class AuthenticationStateHandler(
    CircuitServicesAccessor circuitServicesAccessor) 
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var authStateProvider = circuitServicesAccessor.Services?
            .GetRequiredService<AuthenticationStateProvider>();

        if (authStateProvider is null)
        {
            throw new Exception("AuthenticationStateProvider not available");
        }

        var authState = await authStateProvider.GetAuthenticationStateAsync();

        var user = authState?.User;

        if (user?.Identity is not null && user.Identity.IsAuthenticated)
        {
            request.Headers.Add("X-USER-IDENTITY-NAME", user.Identity.Name);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Program 文件中注册 AuthenticationStateHandler,并将处理程序添加到创建 IHttpClientFactory 实例的 HttpClient

builder.Services.AddTransient<AuthenticationStateHandler>();

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