服务器端 ASP.NET Core Blazor 其他安全方案
注意
此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
本文介绍如何为其他安全方案配置服务器端 Blazor,其中包括如何将令牌传递给 Blazor 应用。
注意
本文中的代码示例采用在 .NET 6 或更高版本的 ASP.NET Core 中支持的可为空的引用类型 (NRT) 和 .NET 编译器 Null 状态静态分析。 面向 ASP.NET Core 5.0 或更早版本时,请从文章示例中的 string?
、TodoItem[]?
、WeatherForecast[]?
和 IEnumerable<GitHubBranch>?
类型中删除 NULL 类型指定 (?
)。
将令牌传递到服务器端 Blazor 应用
正在等待更新 Blazor Web App 的此部分更新有关在 Blazor Web App 中传递令牌的部分 (dotnet/AspNetCore.Docs
#31691)。 有关详细信息,请参阅在交互式服务器模式 (dotnet/aspnetcore
#52390) 下向 HttpClient 提供访问令牌的问题。
有关 Blazor Server,请查看本文 7.0 版本部分。
可使用本部分中介绍的方法将服务器端 Blazor 应用中的 Razor 组件外部可用的令牌传递给组件。 本部分中的示例重点介绍如何将访问、刷新和反请求伪造 (XSRF) 令牌传递给 Blazor 应用,但此方法对其他 HTTP 上下文状态有效。
注意
在组件 POST 到 Identity 或其他需要验证的终结点的情况下,将 XSRF 令牌传递给 Razor 组件非常有用。 如果应用只需要访问和刷新令牌,则可以从以下示例中删除 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 应用中使用的作用域令牌提供程序服务,以解析依赖项注入 (DI) 中的令牌。
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.cs
的 Startup.ConfigureServices
中,为以下对象添加服务:
- 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;
var request = new HttpRequestMessage(HttpMethod.Get,
"https://localhost:5003/WeatherForecast");
request.Headers.Add("Authorization", $"Bearer {token}");
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>
设置身份验证方案
对于使用多个身份验证中间件并因此具有多个身份验证方案的应用,可以在 Program
文件的终结点配置中显式设置 Blazor 使用的方案。 以下示例设置 OpenID Connect (OIDC) 方案:
对于使用多个身份验证中间件并因此具有多个身份验证方案的应用,可以在 Startup.cs
的终结点配置中显式设置 Blazor 使用的方案。 以下示例设置 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
});
对于使用多个身份验证中间件并因此具有多个身份验证方案的应用,可以在 Startup.Configure
的终结点配置中显式设置 Blazor 使用的方案。 以下示例设置了 Microsoft Entra ID 方案:
endpoints.MapBlazorHub().RequireAuthorization(
new AuthorizeAttribute
{
AuthenticationSchemes = AzureADDefaults.AuthenticationScheme
});
使用 OpenID Connect (OIDC) v2.0 终结点
在 5.0 之前的 ASP.NET Core 版本中,身份验证库和 Blazor 模板使用 OpenID Connect (OIDC) v1.0 终结点。 若要在 5.0 以前的版本中使用 v2.0 终结点,请在 OpenIdConnectOptions 中配置 OpenIdConnectOptions.Authority 选项:
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 属性。 使用 Authority 键在 OpenIdConnectOptions 或应用设置文件中设置属性。
代码更改
ID 令牌中的声明列表针对 v2.0 终结点会发生更改。 有关更改的 Microsoft 文档已停用,但在 ID 令牌声明参考中提供了有关 ID 令牌中的声明的指导。
由于在 v2.0 终结点的范围 URI 中指定了资源,请删除 OpenIdConnectOptions 中的 OpenIdConnectOptions.Resource 属性设置:
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,设置用户的连接生存期。 除非通过处理程序执行更新来实现身份验证更改(以下示例中的
AuthenticationChanged
),否则仅需要 OnConnectionUpAsync 方法。 - 调用 OnCircuitOpenedAsync 以附加身份验证更改的处理程序
AuthenticationChanged
才能对用户进行更新。 UpdateAuthentication
任务的catch
块不对异常执行任何操作,因为此时无法在代码执行中报告异常。 如果从任务引发异常,则会在应用中的其他位置报告该异常。
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.cs
的 Startup.ConfigureServices
中:
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 方案的中间件中设置用户,请在身份验证中间件运行后在自定义中间件中调用 UserService
上的 SetUser
,或使用 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);
}
}
在即将调用 Program
文件中的 app.MapRazorComponents<App>()
之前,调用中间件:
在即将调用 Program
文件中的 app.MapBlazorHub()
之前,调用中间件:
在即将调用 Startup.cs
的 Startup.Configure
中的 app.MapBlazorHub()
之前,调用中间件:
app.UseMiddleware<UserServiceMiddleware>();
在传出请求中间件中访问 AuthenticationStateProvider
使用 IHttpClientFactory 创建的针对 HttpClient 的 DelegatingHandler 中的 AuthenticationStateProvider 可使用线路活动处理程序在传出请求中间件中访问。
注意
如需了解关于通过在 ASP.NET Core 应用中使用 IHttpClientFactory 创建的 HttpClient 实例定义 HTTP 请求的委托处理程序的一般指导,可参阅在 ASP.NET Core 中使用 IHttpClientFactory 发出 HTTP 请求的以下部分:
以下示例使用 AuthenticationStateProvider 将经过身份验证的用户的自定义用户名标头附加到传出请求。
首先在 Blazor 依赖项注入 (DI) 文章的以下部分中实现 CircuitServicesAccessor
类:
使用 CircuitServicesAccessor
访问 DelegatingHandler 实现中的 AuthenticationStateProvider。
AuthenticationStateHandler.cs
:
public class AuthenticationStateHandler(
CircuitServicesAccessor circuitServicesAccessor)
: DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var authStateProvider = circuitServicesAccessor.Services
.GetRequiredService<AuthenticationStateProvider>();
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
,并将处理程序添加到创建 HttpClient 实例的 IHttpClientFactory:
builder.Services.AddTransient<AuthenticationStateHandler>();
builder.Services.AddHttpClient("HttpMessageHandler")
.AddHttpMessageHandler<AuthenticationStateHandler>();