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 状态静态分析。 面向 ASP.NET Core 5.0 或更早版本时,请从文章示例中的 string?TodoItem[]?WeatherForecast[]?IEnumerable<GitHubBranch>? 类型中删除 NULL 类型指定 (?)。

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

可使用本部分中介绍的方法将服务器端 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.csStartup.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。

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

使用 CircuitHandlerAuthenticationStateProvider 捕获用户,并在服务中设置用户。 如果要更新用户,请将回调注册到 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.csStartup.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.csStartup.Configure 中的 app.MapBlazorHub() 之前,调用中间件:

app.UseMiddleware<UserServiceMiddleware>();

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

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

注意

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

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

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

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

使用 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>();