注意
此版本不是本文的最新版本。 有关当前版本,请参阅 本文的 .NET 10 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅 本文的 .NET 10 版本。
本文介绍如何为其他安全方案配置服务器端 Blazor,其中包括如何将令牌传递给 Blazor 应用。
注意
本文中的代码示例采用在 .NET 6 或更高版本的 ASP.NET Core 中支持的可为空的引用类型 (NRT) 和 .NET 编译器 Null 状态静态分析。 面向 .NET 5 或更早版本时,请从文章示例中的 ?、string?、TodoItem[]? 和 WeatherForecast[]? 类型中删除 NULL 类型指定 (IEnumerable<GitHubBranch>?)。
将令牌发送到服务器端 Blazor 应用
本部分适用于 Blazor Web App。 有关 Blazor Server,请查看 本文部分的 .NET 7 版本。
如果只想使用访问令牌从Blazor Web App命名 HTTP 客户端进行 Web API 调用,请参阅“使用 Web API 调用的令牌处理程序”部分,其中说明了如何使用DelegatingHandler实现将用户的访问令牌附加到传出请求。 本节中的以下指南适用于开发人员,这些指南针对服务器端需要访问令牌、刷新令牌及其他身份验证属性的其他用途。
注意
如需 DelegatingHandler 实例的更多信息,请参阅使用 IHttpClientFactory 的 HTTP 请求 - ASP.NET Core。
若要在 Blazor Web App 保存令牌和其他身份验证属性以供服务器端使用,建议使用 IHttpContextAccessor/HttpContext(IHttpContextAccessor,HttpContext)。 如果令牌是在静态服务器端渲染(静态 SSR)或预渲染期间获得的,则支持通过使用 HttpContext 从 读取令牌(包括作为IHttpContextAccessor)来获取令牌,以便在交互式服务器渲染期间使用。 但是,如果用户在线路建立后进行身份验证,则令牌不会更新,因为 HttpContext 是在 SignalR 连接开始时捕获的。 此外,AsyncLocal<T> 使用 IHttpContextAccessor 意味着在读取 HttpContext 之前必须小心不要丢失执行上下文。 有关详细信息,请参阅ASP.NET Core Blazor 应用中的 IHttpContextAccessor/HttpContext。
在服务类中,获取对命名空间Microsoft.AspNetCore.Authentication成员的访问权限,以便在GetTokenAsync上显示HttpContext方法。 另一种方法(在以下示例中已注释掉)是在 AuthenticateAsync 上调用 HttpContext。 对于返回的 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 可以注入到服务器端服务中,例如在预配 DelegatingHandler 的 HttpClient 中。 以下示例仅用于演示目的,或者如果需要在服务中 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);
}
}
令牌处理程序已注册,并在 Program 文件中充当命名 HTTP 客户端的委托处理程序:
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 Blazor Web App(适用于 OIDC 提供程序)和使用 Microsoft Entra ID 保护 ASP.NET Core Blazor Web App(适用于 Microsoft Identity Web with Entra)中进行了介绍。
使用令牌处理程序进行 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) 注册为作用域服务,并指定为命名 HTTP 客户端的消息处理程序,使用 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.ConfigureServices 的 Startup.cs 中,为以下对象添加服务:
-
IHttpClientFactory:用于
WeatherForecastService类,该类使用访问令牌从服务器 API 获取天气数据。 -
TokenProvider:存储访问令牌和刷新令牌。
services.AddHttpClient();
services.AddScoped<TokenProvider>();
定义一个类,用于在初始应用状态下使用访问令牌和刷新令牌传递它。
InitialApplicationState.cs:
public class InitialApplicationState
{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
public string? XsrfToken { get; set; }
}
在 Pages/_Host.cshtml 文件中,创建 InitialApplicationState 实例,并将其作为参数传递给应用:
在 Pages/_Layout.cshtml 文件中,创建 InitialApplicationState 实例,并将其作为参数传递给应用:
在 Pages/_Host.cshtml 文件中,创建 InitialApplicationState 实例,并将其作为参数传递给应用:
@using Microsoft.AspNetCore.Authentication
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
...
@{
var tokens = new InitialApplicationState
{
AccessToken = await HttpContext.GetTokenAsync("access_token"),
RefreshToken = await HttpContext.GetTokenAsync("refresh_token"),
XsrfToken = Xsrf.GetAndStoreTokens(HttpContext).RequestToken
};
}
<component ... param-InitialState="tokens" ... />
在 App 组件 (App.razor) 中,解析服务并使用参数中的数据对其进行初始化:
@inject TokenProvider TokenProvider
...
@code {
[Parameter]
public InitialApplicationState? InitialState { get; set; }
protected override Task OnInitializedAsync()
{
TokenProvider.AccessToken = InitialState?.AccessToken;
TokenProvider.RefreshToken = InitialState?.RefreshToken;
TokenProvider.XsrfToken = InitialState?.XsrfToken;
return base.OnInitializedAsync();
}
}
注意
将初始状态分配给上一示例中的 TokenProvider 的替代方法是将数据复制到 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 终结点。 要在 .NET 5 之前的 ASP.NET Core 版本中使用 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。
用于捕获自定义服务用户的电路处理程序
使用 CircuitHandler 从 AuthenticationStateProvider 捕获用户,并在服务中设置用户。 如果要更新用户,请将回调注册到 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.ConfigureServices 的 Startup.cs 中:
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;
...
services.AddScoped<UserService>();
services.TryAddEnumerable(
ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());
使用组件中的服务获取用户:
@inject UserService UserService
<h1>Hello, @(UserService.GetUser().Identity?.Name ?? "world")!</h1>
若要在 MVC、Razor Pages 和其他 ASP.NET Core 方案的中间件中设置用户,请在身份验证中间件运行后在自定义中间件中调用 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
IHttpClientFactory在独立于应用的依赖注入(DI)范围中创建DelegatingHandler实例。 如果注入 AuthenticationStateProvider 到派生 DelegatingHandler 类型中,处理程序无权从 Blazor 线路访问当前用户的身份验证状态。
使用以下任一方法来解决此方案:
- 应用程序范围处理程序 (建议)
- 线路活动处理程序
注意
有关为使用 HttpClient 创建的 IHttpClientFactory 实例发出的 HTTP 请求定义委托处理程序的一般指导,请参阅使用 IHttpClientFactory 发出 HTTP 请求 - ASP.NET Core的以下章节:
以下小节中的示例将经过身份验证的用户的自定义用户名标头附加到传出请求。
应用程序范围处理程序(建议)
本节中的方法使用键控服务注册一个自定义的 HttpClient,该自定义项使用从当前应用程序范围解析的应用程序范围处理程序包装基客户端以访问 AuthenticationStateProvider。
方法概述:
- 基础客户端配置:调用 AddHttpClient,以通过 注册一个IHttpClientFactory。
- 键控注册:一个自定义的
AddApplicationScopeHandler扩展方法使用相同的客户端名称注册一个键控的 HttpClient。 - 范围感知处理程序:应用程序范围处理程序从当前范围解析,使其能够访问 AuthenticationStateProvider。
- 处理程序缓存:应用程序范围处理程序使用 IHttpMessageHandlerFactory 获取缓存的 HttpMessageHandler,从而保留连接池。
- 配置重用:应用范围处理程序将与基础客户端相同的HttpClientFactoryOptions配置应用于其HttpClient。
创建以下方法和类:
-
AddApplicationScopeHandler:用于将应用程序范围处理程序和键式 HttpClient 服务添加到 DI 容器的扩展方法。 -
ApplicationScopeHandler:应用程序范围处理程序类。 -
AuthenticationStateHandler:一个 DelegatingHandler,用于向出站请求附加经过身份验证的用户的自定义用户名标头。
Services/ApplicationScopeHttpClientExtensions.cs:
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
namespace BlazorSample.Services;
public static class ApplicationScopeHttpClientExtensions
{
public static readonly HttpRequestOptionsKey<IServiceProvider> ScopeKey =
new("ApplicationScope");
public static IHttpClientBuilder AddApplicationScopeHandler(
this IHttpClientBuilder builder)
{
var name = builder.Name;
builder.Services.AddTransient<ApplicationScopeHandler>();
builder.Services.AddKeyedScoped<HttpClient>(name, (sp, key) =>
{
var handler = sp.GetRequiredService<ApplicationScopeHandler>();
handler.InnerHandler =
sp.GetRequiredService<IHttpMessageHandlerFactory>()
.CreateHandler(name);
var client = new HttpClient(handler, disposeHandler: false);
var options =
sp.GetRequiredService<IOptionsMonitor<HttpClientFactoryOptions>>()
.Get(name);
foreach (var action in options.HttpClientActions)
{
action(client);
}
return client;
});
return builder;
}
}
public class ApplicationScopeHandler(IServiceProvider serviceProvider)
: DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Options.Set(ApplicationScopeHttpClientExtensions.ScopeKey,
serviceProvider);
return base.SendAsync(request, cancellationToken);
}
}
public class AuthenticationStateHandler : DelegatingHandler
{
private ClaimsPrincipal? user;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (user is null)
{
if (request.Options.TryGetValue(
ApplicationScopeHttpClientExtensions.ScopeKey, out var sp))
{
var authStateProvider = sp.GetService<AuthenticationStateProvider>();
if (authStateProvider is not null)
{
user = (await authStateProvider.GetAuthenticationStateAsync())
.User;
}
}
}
if (user?.Identity?.IsAuthenticated)
{
request.Headers.TryAddWithoutValidation("X-USER-IDENTITY-NAME",
user.Identity.Name);
}
return await base.SendAsync(request, cancellationToken);
}
}
在前面的示例中,AuthenticationStateHandler缓存用户的时间与DelegatingHandler的生存期相同。 若要为每个请求提取用户的当前身份验证状态,请删除 null 对用户的条件检查。
在 Program 文件中注册命名客户端,调用 AddApplicationScopeHandler 以添加应用程序范围处理程序:
builder.Services.AddHttpClient("ExternalApi", client =>
{
client.BaseAddress = new Uri("{REQUEST URI}");
})
.AddApplicationScopeHandler()
.AddHttpMessageHandler<AuthenticationStateHandler>();
前面的示例中的 {REQUEST URI} 占位符是请求 URI(localhost 示例: http://localhost:5209) 。
使用密钥服务将客户端注入组件:
@using Microsoft.Extensions.DependencyInjection
@code {
[Inject(Key = "ExternalApi")]
public HttpClient Http { get; set; } = default!;
private async Task CallApiAsync()
{
var response = await Http.GetAsync("/api/endpoint");
}
}
电路活动处理程序
本节中采用的方法是使用 电路活动处理程序 来访问 AuthenticationStateProvider,它是上一节推荐的 应用程序范围处理程序方法 的替代方案。
首先在 CircuitServicesAccessor 依赖项注入 (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>();
不透明(参考)访问令牌支持
本部分中的指南介绍了如何实现不透明(参考)访问令牌支持,这在 JSON Web 令牌(JWT)上具有以下优势:
- 严格撤销:在访问令牌自然过期之前的任何时间使其失效。
- 令牌大小限制:将大量用户声明存储在令牌中,以避免出现非常大的 JWT。
- 安全性:防止 API 使用者或第三方读取访问令牌声明。
注意
以下指南要求使用支持不透明(引用)访问令牌的认证服务器。 目前,Microsoft Entra不支持不透明的访问令牌验证。 默认情况下,Keycloak 和 Okta 颁发 JWT 访问令牌。 本节中的不透明令牌处理程序仍可与 Keycloak 和 Okta 配合使用,因为它仅依赖 RFC 7662 令牌自省。 本节中的“不透明”指的是客户端如何处理该令牌,而不是服务器如何生成它。 或者, Duende IdentityServer 可以配置为仅颁发不透明的令牌。
在与 Keycloak 测试此模式时,API 的自省客户端必须与颁发该用户访问令牌的 OIDC 客户端不同。 使用签发该令牌的客户端对令牌进行自省时,会返回 {"active": false},并且服务器日志中会显示“Access token JWT check failed”。 在以下场景中,这种情况不会自然发生,因为 Blazor Web App 和 Minimal API(MinimalApiJwt)是彼此独立的客户端。
AddOpenIdConnect支持不透明令牌,因为在将其配置为使用代码交换证明密钥(PKCE)的授权码流程时,它不会验证访问令牌。 它依赖 ASP.NET Core 服务器与 OIDC 身份验证服务之间的 HTTPS 后向通道,使用用户登录后重定向回 ASP.NET Core 应用时收到的授权代码来获取 ID 令牌。 如果应用只需要使用 OIDC 登录用户才能获取有效的身份验证 cookie,则支持不透明的访问令牌,而无需修改应用。
仅当由 AddOpenIdConnect 获取的不透明令牌被传递给另一个尝试使用 AddJwtBearer 对其进行验证的服务时,才会发生失败。 与自包含的 JWT 不同,不透明令牌需要向授权服务器发起请求,以验证其状态并获取声明。 若要解决此限制,请使用第三方 API(如 Duende Introspection Authentication Handler),或 创建自定义 AuthenticationHandler 来验证令牌。
Important
Duende Software 和 Okta不受Microsoft拥有或控制,并可能要求你为其服务和库的生产使用支付许可证费。
以下 AuthenticationHandler<TOptions> 以及相关的配置和辅助代码作为一种通用方法提供,可能需要进一步开发,以满足特定授权服务器的要求。 以下处理程序从 Authorization 标头中提取针对授权服务器自省端点的 HTTP 调用所用的不透明令牌,并创建一个包含用户声明的 AuthenticationTicket。
调用授权服务器的自省端点需要进行身份验证。 以下示例依赖于使用 机密管理器工具 在本地开发和测试请求的授权标头(base64 编码凭据)中设置客户端密码进行身份验证。
警告
请勿在客户端代码中存储应用机密、连接字符串、凭据、密码、个人标识号(PIN)、专用 C#/.NET 代码或私钥/令牌,这 始终不安全。 在测试/暂存和生产环境中,服务器端 Blazor 代码和 Web API 应使用安全身份验证流,以避免在项目代码或配置文件中维护凭据。 在本地开发测试之外,我们建议避免使用环境变量来存储敏感数据,因为环境变量不是最安全的方法。 对于本地开发测试,建议使用 机密管理器工具 来保护敏感数据。 有关详细信息,请参阅 安全维护敏感数据和凭据。
在以下处理程序中,授权服务器的自省端点客户端密钥使用的配置键为 Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret。 对于生产应用,请考虑使用 客户端断言。 有关详细信息,请参阅 机密客户端断言(Microsoft Entra 文档)。
如果尚未为机密管理器工具初始化 Blazor 服务器项目,请使用命令外壳(如 Visual Studio 中的开发人员 PowerShell 命令 shell)执行以下命令。 在执行命令之前,请将包含 cd 该命令的目录更改为服务器项目的目录。 该命令将建立用户机密标识符(<UserSecretsId> 在服务器应用的项目文件中):
dotnet user-secrets init
执行以下命令,为授权服务器设置客户端密码。
{SECRET} 占位符是客户端密钥:
dotnet user-secrets set "Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret" "{SECRET}"
如果使用 Visual Studio,可以通过右键单击 解决方案资源管理器 中的服务器项目并选择“ 管理用户机密”来确认机密是否已设置。
Extensions/HttpRequestExtensions.cs:
namespace MinimalApiJwt.Extensions;
public static class HttpRequestExtensions
{
public static string? ExtractBearerToken(this HttpRequest request)
{
var authorizationHeader = request.Headers.Authorization.ToString();
if (!string.IsNullOrEmpty(authorizationHeader) &&
authorizationHeader.StartsWith("Bearer ",
StringComparison.OrdinalIgnoreCase))
{
var token = authorizationHeader["Bearer ".Length..].Trim();
if (!string.IsNullOrEmpty(token))
{
return token;
}
}
return null;
}
}
Authentication/OpaqueTokenAuthenticationOptions.cs:
using Microsoft.AspNetCore.Authentication;
namespace MinimalApiJwt.Authentication;
public class OpaqueTokenAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "OpaqueTokenAuthentication";
public string? IntrospectionEndpoint { get; set; }
public string? ClientId { get; set; }
public string? ClientSecret { get; set; }
}
以下处理程序尝试验证不透明(引用)访问令牌。 使用令牌和 API 凭据对授权服务器的反省终结点进行 HTTP 调用。 处理响应以确定令牌是否有效:
- 如果令牌有效,则会创建一个包含用户声明的 AuthenticationTicket。
- 如果令牌无效,则返回失败的授权结果。
处理程序的选项(Options)是由 AuthenticationHandler<TOptions> 基类型提供的 OpaqueTokenAuthenticationOptions 实例,它在应用的 Program 文件中配置了授权服务器的自省终结点和 API 的客户端 ID。 API 的客户端密码在开发过程中由机密管理器工具提供。
IOptionsMonitor<OpaqueTokenAuthenticationOptions> (optionsMonitor) 不直接由处理程序使用,但可用于在运行时支持动态配置更改。
对于请求中 FormUrlEncodedContent 的内容,某些服务器需要令牌类型提示(token_type_hint)。 例如,所需的值可能是 access_token。 有关详细信息,请参阅身份验证服务器的文档。
Authentication/OpaqueTokenAuthenticationHandler.cs:
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using MinimalApiJwt.Extensions;
namespace MinimalApiJwt.Authentication;
public class OpaqueTokenAuthenticationHandler(
IOptionsMonitor<OpaqueTokenAuthenticationOptions> optionsMonitor,
ILoggerFactory logger,
UrlEncoder encoder,
IHttpClientFactory httpClientFactory)
: AuthenticationHandler<OpaqueTokenAuthenticationOptions>(optionsMonitor,
logger, encoder)
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var opaqueToken = Request.ExtractBearerToken();
if (opaqueToken is null)
{
var failedResult = AuthenticateResult.Fail(
"Bearer token not found in Authorization header.");
return failedResult;
}
var introspectionUri = Options.IntrospectionEndpoint;
var clientId = Options.ClientId;
var clientSecret = Options.ClientSecret;
if (string.IsNullOrWhiteSpace(introspectionUri) ||
string.IsNullOrWhiteSpace(clientId) ||
string.IsNullOrWhiteSpace(clientSecret))
{
var failedResult = AuthenticateResult.Fail(
"Opaque token authentication isn't fully configured.");
return failedResult;
}
using var client = httpClientFactory.CreateClient();
// Set the Authorization header (base64 encoded credentials)
var authString = Convert.ToBase64String(
System.Text.Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}"));
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", authString);
// Prepare the form-encoded body containing the token
var content = new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("token", opaqueToken)
]);
// Post to the introspection endpoint
var response = await client.PostAsync(introspectionUri, content);
if (!response.IsSuccessStatusCode)
{
var failedResult = AuthenticateResult.Fail(
"Introspection endpoint failure.");
return failedResult;
}
// Parse the JSON response
var responseString = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(responseString);
// The 'active' property determines if the token is valid and not expired
var tokenIsValid =
doc.RootElement.TryGetProperty("active", out var activeProperty) &&
activeProperty.ValueKind == JsonValueKind.True;
if (tokenIsValid)
{
// Map standard introspection response fields onto claims.
// Field names below match what Keycloak, Duende IdentityServer,
// Auth0, and Okta return; adjust the role source for your provider.
var claims = new List<Claim>();
string? Get(string name) =>
doc.RootElement.TryGetProperty(name, out var v) &&
v.ValueKind == JsonValueKind.String ? v.GetString() : null;
var sub = Get("sub");
var username = Get("preferred_username") ?? Get("username") ?? sub;
if (sub is not null) claims.Add(new Claim(ClaimTypes.NameIdentifier, sub));
if (username is not null) claims.Add(new Claim(ClaimTypes.Name, username));
if (Get("email") is { } email) claims.Add(new Claim(ClaimTypes.Email, email));
if ((Get("client_id") ?? Get("azp")) is { } cid)
claims.Add(new Claim("client_id", cid));
if (Get("scope") is { } scope)
foreach (var s in scope.Split(' ', StringSplitOptions.RemoveEmptyEntries))
claims.Add(new Claim("scope", s));
// Keycloak surfaces realm roles under realm_access.roles.
// Duende/IdentityServer uses a flat "role" claim; Auth0 uses a
// configurable custom claim. Adjust for your authorization server.
if (doc.RootElement.TryGetProperty("realm_access", out var ra) &&
ra.ValueKind == JsonValueKind.Object &&
ra.TryGetProperty("roles", out var roles) &&
roles.ValueKind == JsonValueKind.Array)
{
foreach (var r in roles.EnumerateArray())
if (r.ValueKind == JsonValueKind.String)
claims.Add(new Claim(ClaimTypes.Role, r.GetString()!));
}
var identity = new ClaimsIdentity(claims,
OpaqueTokenAuthenticationOptions.DefaultScheme,
nameType: ClaimTypes.Name,
roleType: ClaimTypes.Role);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal,
OpaqueTokenAuthenticationOptions.DefaultScheme);
var result = AuthenticateResult.Success(ticket);
return result;
}
else
{
var failedResult = AuthenticateResult.Fail("Bearer token invalid.");
return failedResult;
}
}
}
注意
可以使用 OpenID Connect 发现终结点并为客户端 HttpClient 的反省请求添加缓存来进一步改进上述方法。
在 Program 文件中:
using MinimalApiJwt.Authentication;
...
builder.Services.AddHttpClient();
builder.Services.AddAuthentication()
.AddScheme<OpaqueTokenAuthenticationOptions, OpaqueTokenAuthenticationHandler>(
OpaqueTokenAuthenticationOptions.DefaultScheme,
options =>
{
options.IntrospectionEndpoint = "{AUTH SERVER INTROSPECTION URI}";
options.ClientId = "{API CLIENT ID}";
options.ClientSecret =
builder.Configuration[
"Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret"];
});
前述示例的占位符:
-
{AUTH SERVER INTROSPECTION URI}:身份验证服务器的自省 URI -
{API CLIENT ID}:API 客户端 ID
身份验证服务器的内省 URI({AUTH SERVER INTROSPECTION URI})和 API 客户端 ID({API CLIENT ID})的值可从应用设置或任何其他配置源提供。
令牌通常会在注销时通过吊销端点失效。 以下示例是进一步开发的起点:
app.MapPost("/logout",
async ([FromForm] string? returnUrl, HttpContext context,
IHttpClientFactory httpClientFactory) =>
{
var accessToken = await context.GetTokenAsync("access_token");
if (!string.IsNullOrEmpty(accessToken))
{
// Prepare the revocation request (RFC 7009)
var content =
new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "token", accessToken },
{ "token_type_hint", "access_token" },
{ "client_id", "{API CLIENT ID}" },
{ "client_secret", "{CLIENT SECRET}" }
});
// POST to the revocation endpoint
using var client = httpClientFactory.CreateClient();
await client.PostAsync("{AUTH SERVER TOKEN REVOCATION URI}", content);
}
return TypedResults.SignOut(new AuthenticationProperties { RedirectUri = "{REDIRECT URI}" },
[CookieAuthenticationDefaults.AuthenticationScheme]);
});
前面示例中的占位符:
-
{AUTH SERVER TOKEN REVOCATION URI}:身份验证服务器的令牌撤销 URI。 -
{API CLIENT ID}:API 客户端 ID。 -
{CLIENT SECRET}:安全获取的客户端机密。 -
{REDIRECT URI}:重定向 URI。
在 Duende IdentityServer 中,通过将客户端配置属性设置为 CoordinateLifetimeWithUserSession 自动撤销令牌,该属性 true会在会话结束时自动清理关联的令牌。 有关详细信息,请参阅会话清理和注销(Duende 文档)。
将在未来版本的 .NET 中内置对不透明访问令牌的支持,目前正在考虑中。 有关详细信息,请参阅 Opaque - 引用令牌验证(dotnet/aspnetcore#46026)。