为 ASP.NET Core 服务器端 Blazor 应用提供保护

注意

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

重要

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

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

本文介绍如何将服务器端 Blazor 应用作为 ASP.NET Core 应用程序来提供保护。

服务器端 Blazor 应用采用与 ASP.NET Core 应用相同方式的安全配置。 有关详细信息,请参阅 ASP.NET Core 安全性主题下的文章。

身份验证上下文仅在应用启动时建立,即应用首次连接到 WebSocket 时。 线路的生存期内会保留身份验证上下文。 应用会定期重新验证用户的身份验证状态,目前默认每 30 分钟重新验证一次。

如果应用必须捕获自定义服务的用户或对用户的更新做出响应,请参阅服务器端 ASP.NET Core Blazor 其他安全方案

Blazor 不同于传统的服务器呈现的 Web 应用,它会在每个页面导航上使用 cookie 发出新的 HTTP 请求。 在导航事件期间检查身份验证。 但不涉及 cookie。 仅在向服务器发出 HTTP 请求时发送 Cookie,当用户在 Blazor 应用中导航时不会发生这种情况。 在导航期间,会在 Blazor 线路内检查用户的身份验证状态,你可以使用 RevalidatingAuthenticationStateProvider 抽象随时在服务器上对其进行更新。

重要

不建议在导航期间执行自定义 NavigationManager 以实现身份验证。 如果应用必须在导航期间执行自定义身份验证状态逻辑,请使用自定义 AuthenticationStateProvider

注意

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

项目模板

按照适用于 ASP.NET Core Blazor 的工具中的指南创建新的服务器端 Blazor 应用。

选择服务器端应用模板并配置项目后,在“身份验证类型”下选择应用的身份验证

  • (默认):无身份验证。
  • 个人帐户:使用 ASP.NET Core Identity 将用户帐户存储在应用中。
  • (默认):无身份验证。
  • 个人帐户:使用 ASP.NET Core Identity 将用户帐户存储在应用中。
  • Microsoft 标识平台:有关详细信息,请参阅 ASP.NET Core Blazor 身份验证和授权
  • Windows:使用 Windows 身份验证。

BlazorIdentity UI(个人帐户)

选择个人帐户的身份验证选项时,Blazor 支持生成基于 Blazor 的完整 Identity UI。

Blazor Web 应用模板为 SQL Server 数据库搭建了 Identity 代码。 命令行版本默认使用 SQLite,并包含 Identity 的 SQLite 数据库。

该模板处理以下内容:

  • 为例行身份验证任务添加 IdentityRazor 组件和相关逻辑,例如用户登录和注销。
  • 添加 Identity 相关的包和依赖项。
  • 引用 _Imports.razor 中的 Identity 包。
  • 创建自定义用户 Identity 类(ApplicationUser)。
  • 创建和注册 EF Core 数据库上下文(ApplicationDbContext)。
  • 配置内置 Identity 终结点的路由。
  • 包含 Identity 验证和业务逻辑。

若要检查 Blazor 框架的 Identity 组件,请在 Blazor Web 应用项目模板(引用源)中的 Account 文件夹PagesShared 文件夹中访问它们。

选择交互式 WebAssembly 或“交互式自动”呈现模式时,服务器将处理所有身份验证和授权请求,Identity 组件将在 Blazor Web 应用的主项目中的服务器上静态呈现。 项目模板包含 .Client 项目中的 PersistentAuthenticationStateProvider 类(引用源),用于在服务器和浏览器之间同步用户的身份验证状态。 该类是 AuthenticationStateProvider 的一个自定义实现。 提供程序使用 PersistentComponentState 类预呈现身份验证状态并将其保存到页面。

BlazorIdentity 取决于 DbContext 实例不由中心创建,这是有意的,因为 DbContext 足以让项目模板的 Identity 组件静态呈现,而无需支持交互性。

在 Blazor Web 应用的主项目中,身份验证状态提供程序命名为 IdentityRevalidatingAuthenticationStateProvider(引用源)(仅限服务器交互解决方案)或 PersistingRevalidatingAuthenticationStateProvider(引用源)(WebAssembly 或自动交互解决方案)。

有关如何在为 Identity 组件强制实施静态 SSR 的同时将全局交互式呈现模式应用于非 Identity 组件的说明,请参阅 ASP.NET Core Blazor 呈现模式

有关保留预呈现状态的详细信息,请参阅《预呈现 ASP.NET 核心 Razor 组件》。

有关 BlazorIdentity UI 的详细信息以及有关通过社交网站集成外部登录的指导,请参阅《.NET 8 中标识的新增功能》。

注意

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

管理 Blazor Web 应用中的身份验证状态

本节适用于Blazor采用以下内容的 Web 应用

  • 交互式服务器端呈现(交互式 SSR )和 CSR。
  • 客户端呈现 (CSR)

客户端身份验证状态提供程序仅在 Blazor 中使用,不与 ASP.NET Core 身份验证系统集成。 在预呈现期间,Blazor 尊重页面上定义的元数据,并使用 ASP.NET Core 身份验证系统,以确定用户是否已通过身份验证。 当用户从一个页面导航到另一个页面时,将使用客户端身份验证提供程序。 当用户刷新页面(重新加载整个页面)时,客户端身份验证状态提供程序不参与服务器上的身份验证决策。 由于服务器没有持久化用户的状态,因此客户端维护的任何身份验证状态都将丢失。

为了解决这个问题,最佳方法是在 ASP.NET Core 身份验证系统内执行身份验证。 客户端身份验证状态提供程序只负责反映用户的身份验证状态。 Blazor Web 应用项目模板演示了如何使用身份验证状态提供程序实现这一点的示例:

注意

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

设置Identity的基架

有关如何将 Identity 架构到服务器端 Blazor 应用中的详细信息,请参阅在 ASP.NET Core 项目中设置 Identity 的基架

将 Identity 架构到服务器端 Blazor 应用中:

来自外部提供程序的额外声明和令牌

若要存储来自外部提供程序的其他声明,请参阅在 ASP.NET Core 中保留来自外部提供程序的其他声明和令牌

使用 Identity 服务器的 Linux 上的 Azure 应用服务

使用 Identity 服务器部署到 Linux 上的 Azure 应用服务时,显式指定颁发者。 有关详细信息,请参阅使用 Identity 保护 SPA 的 Web API 后端

实现自定义 AuthenticationStateProvider

如果应用需要自定义提供程序,请实现 AuthenticationStateProvider 并替代 GetAuthenticationStateAsync

在以下示例中,通过用户名 mrfibuli 对所有用户进行身份验证。

CustomAuthStateProvider.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, "mrfibuli"),
        }, "Custom Authentication");

        var user = new ClaimsPrincipal(identity);

        return Task.FromResult(new AuthenticationState(user));
    }
}

CustomAuthStateProvider 服务在 Program 文件中注册:

using Microsoft.AspNetCore.Components.Authorization;

...

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();

在调用 AddServerSideBlazor 后,CustomAuthStateProvider 服务将注册到 Program 文件中

using Microsoft.AspNetCore.Components.Authorization;

...

builder.Services.AddServerSideBlazor();

...

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();

在调用 AddServerSideBlazor 后,CustomAuthStateProvider 服务注册到 Startup.csStartup.ConfigureServices

using Microsoft.AspNetCore.Components.Authorization;

...

services.AddServerSideBlazor();

...

services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();

确认或添加 AuthorizeRouteViewRouter 组件。

Routes 组件 (Components/Routes.razor) 中:

<Router ...>
    <Found ...>
        <AuthorizeRouteView RouteData="routeData" 
            DefaultLayout="typeof(Layout.MainLayout)" />
        ...
    </Found>
</Router>

将级联身份验证状态服务添加到 Program 文件中的服务集合:

builder.Services.AddCascadingAuthenticationState();

注意

从启用了身份验证的某个 Blazor 项目模板创建 Blazor 应用时,该应用包含 AuthorizeRouteView 以及对 AddCascadingAuthenticationState 的调用。 有关详细信息,请参阅 ASP.NET Core Blazor 身份验证和授权,以及文章的使用路由器组件自定义未经授权的内容部分中提供的其他信息。

确认或添加 AuthorizeRouteViewCascadingAuthenticationStateRouter 组件:

<CascadingAuthenticationState>
    <Router ...>
        <Found ...>
            <AuthorizeRouteView RouteData="routeData" 
                DefaultLayout="typeof(MainLayout)" />
            ...
        </Found>
    </Router>
</CascadingAuthenticationState>

注意

从启用了身份验证的 Blazor 项目模板之一创建 Blazor 应用时,该应用将包括 AuthorizeRouteViewCascadingAuthenticationState 组件,如以上示例所示。 有关详细信息,请参阅 ASP.NET Core Blazor 身份验证和授权,以及文章的使用路由器组件自定义未经授权的内容部分中提供的其他信息。

AuthorizeView 在任何组件中演示经过身份验证的用户的名称:

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

有关使用 AuthorizeView 的指南,请参阅 ASP.NET Core Blazor 身份验证和授权

有关身份验证状态更改的通知

自定义 AuthenticationStateProvider 可以在 AuthenticationStateProvider 基类上调用 NotifyAuthenticationStateChanged,以通知使用者身份验证状态有所更改,以重新呈现。

以下示例基于遵循实现自定义 AuthenticationStateProvider 部分中的指南实现自定义 AuthenticationStateProvider

以下 CustomAuthStateProvider 实现公开了一种自定义方法 AuthenticateUser,用于登录用户并通知使用者身份验证状态发生更改。

CustomAuthStateProvider.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var identity = new ClaimsIdentity();
        var user = new ClaimsPrincipal(identity);

        return Task.FromResult(new AuthenticationState(user));
    }

    public void AuthenticateUser(string userIdentifier)
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, userIdentifier),
        }, "Custom Authentication");

        var user = new ClaimsPrincipal(identity);

        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(user)));
    }
}

在组件中:

@inject AuthenticationStateProvider AuthenticationStateProvider

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        ((CustomAuthStateProvider)AuthenticationStateProvider)
            .AuthenticateUser(userIdentifier);
    }
}
@inject AuthenticationStateProvider AuthenticationStateProvider

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        ((CustomAuthStateProvider)AuthenticationStateProvider)
            .AuthenticateUser(userIdentifier);
    }
}

可以增强上述方法,以通过自定义服务触发身份验证状态更改通知。 以下 AuthenticationService 通过 AuthenticationStateProvider 可以订阅到的事件 (UserChanged) 维护支持字段 (currentUser) 中当前用户的声明主体,其中该事件调用 NotifyAuthenticationStateChanged。 使用本部分后面的其他配置,可以将 AuthenticationService 注入到具有逻辑的组件中,该逻辑设置 CurrentUser 以触发 UserChanged 事件。

using System.Security.Claims;

public class AuthenticationService
{
    public event Action<ClaimsPrincipal>? UserChanged;
    private ClaimsPrincipal? currentUser;

    public ClaimsPrincipal CurrentUser
    {
        get { return currentUser ?? new(); }
        set
        {
            currentUser = value;

            if (UserChanged is not null)
            {
                UserChanged(currentUser);
            }
        }
    }
}

Program 文件中,在依赖项注入容器中注册 AuthenticationService

builder.Services.AddScoped<AuthenticationService>();

Startup.csStartup.ConfigureServices 中,在依赖项注入容器中注册 AuthenticationService

services.AddScoped<AuthenticationService>();

以下 CustomAuthStateProvider 订阅到 AuthenticationService.UserChanged 事件。 GetAuthenticationStateAsync 返回用户的身份验证状态。 最初,身份验证状态基于值 AuthenticationService.CurrentUser。 当用户发生更改时,将通过新用户(new AuthenticationState(newUser))创建新的身份验证状态来调用 GetAuthenticationStateAsync

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    private AuthenticationState authenticationState;

    public CustomAuthStateProvider(AuthenticationService service)
    {
        authenticationState = new AuthenticationState(service.CurrentUser);

        service.UserChanged += (newUser) =>
        {
            authenticationState = new AuthenticationState(newUser);

            NotifyAuthenticationStateChanged(
                Task.FromResult(new AuthenticationState(newUser)));
        };
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(authenticationState);
}

以下组件的 SignIn 方法为要在 AuthenticationService.CurrentUser 上设置的用户的标识符创建声明主体:

@inject AuthenticationService AuthenticationService

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        var currentUser = AuthenticationService.CurrentUser;

        var identity = new ClaimsIdentity(
            new[]
            {
                new Claim(ClaimTypes.Name, userIdentifier),
            },
            "Custom Authentication");

        var newUser = new ClaimsPrincipal(identity);

        AuthenticationService.CurrentUser = newUser;
    }
}
@inject AuthenticationService AuthenticationService

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        var currentUser = AuthenticationService.CurrentUser;

        var identity = new ClaimsIdentity(
            new[]
            {
                new Claim(ClaimTypes.Name, userIdentifier),
            },
            "Custom Authentication");

        var newUser = new ClaimsPrincipal(identity);

        AuthenticationService.CurrentUser = newUser;
    }
}

为作用到组件的服务注入 AuthenticationStateProvider

请勿尝试在自定义范围内解析 AuthenticationStateProvider,因为这会导致创建未正确初始化的 AuthenticationStateProvider 的新实例。

要访问作用到组件的服务内的 AuthenticationStateProvider,请使用 @inject 指令[Inject] 属性注入 AuthenticationStateProvider,并将其作为参数传递给服务。 此方法可确保对每个用户应用实例使用正确且初始化的 AuthenticationStateProvider 实例。

ExampleService.cs

public class ExampleService
{
    public async Task<string> ExampleMethod(AuthenticationStateProvider authStateProvider)
    {
        var authState = await authStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            return $"{user.Identity.Name} is authenticated.";
        }
        else
        {
            return "The user is NOT authenticated.";
        }
    }
}

将服务注册为已限定范围。 在服务器端 Blazor 应用中,范围内服务的生存期等于客户端连接线路的持续时间。

Program 文件中:

builder.Services.AddScoped<ExampleService>();

Startup.csStartup.ConfigureServices 中:

services.AddScoped<ExampleService>();

在下面的 InjectAuthStateProvider 组件中:

InjectAuthStateProvider.razor

@page "/inject-auth-state-provider"
@inherits OwningComponentBase
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>Inject <code>AuthenticationStateProvider</code> Example</h1>

<p>@message</p>

@code {
    private string? message;
    private ExampleService? ExampleService { get; set; }

    protected override async Task OnInitializedAsync()
    {
        ExampleService = ScopedServices.GetRequiredService<ExampleService>();

        message = await ExampleService.ExampleMethod(AuthenticationStateProvider);
    }
}
@page "/inject-auth-state-provider"
@inject AuthenticationStateProvider AuthenticationStateProvider
@inherits OwningComponentBase

<h1>Inject <code>AuthenticationStateProvider</code> Example</h1>

<p>@message</p>

@code {
    private string? message;
    private ExampleService? ExampleService { get; set; }

    protected override async Task OnInitializedAsync()
    {
        ExampleService = ScopedServices.GetRequiredService<ExampleService>();

        message = await ExampleService.ExampleMethod(AuthenticationStateProvider);
    }
}

有关详细信息,请参阅 ASP.NET Core Blazor 依赖项注入OwningComponentBase 的相关指南。

使用自定义 AuthenticationStateProvider 预呈现时显示未经授权的内容

为了避免在使用自定义 AuthenticationStateProvider 预呈现时显示未经授权的内容(例如 AuthorizeView 组件中的内容),请采用以下方法之一

  • 禁用预呈现:通过在应用组件层次结构中的最高级别组件(不是根组件)处将 prerender 参数设置为 false 来指示呈现模式。

    注意

    不支持让根组件具有交互性(例如 App 组件)。 因此,App 组件无法直接禁用预呈现。

    对于基于 Blazor Web 应用项目模板的应用,如果在 App 组件 (Components/App.razor) 中使用了 Routes 组件,通常会禁用预呈现:

    <Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
    

    此外,请禁用 HeadOutlet 组件的预呈现:

    <HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />
    

    还可以选择性地禁用预呈现,并精细控制应用于 Routes 组件实例的呈现模式。 有关详细信息,请参阅 ASP.NET Core Blazor 呈现模式

  • 禁用预呈现:打开 _Host.cshtml 文件,并将组件标记帮助程序render-mode 属性更改为 Server

    <component type="typeof(App)" render-mode="Server" />
    
  • 在应用启动前对服务器上的用户进行身份验证:要采用此方法,应用必须使用基于 Identity 的登录页或视图响应用户的初始请求,并阻止任何对 Blazor 终结点的请求,直到其进行身份验证。 有关详细信息,请参阅通过授权保护的用户数据创建 ASP.NET Core 应用。 身份验证后,只有当用户真正未经授权查看内容时,才会显示预呈现 Razor 组件中的未授权内容。

用户状态管理

尽管名称中存在“state”一词,但 AuthenticationStateProvider 不适用于存储常规用户状态。 AuthenticationStateProvider 仅向应用指示用户的身份验证状态,无论用户是否已登录到应用,又是以何种身份进行登录的。

身份验证使用与 Razor Pages 和 MVC 应用相同的 ASP.NET Core Identity 身份验证。 针对 ASP.NET Core Identity 存储的用户状态流向 Blazor,而无需向应用添加其他代码。 按照 ASP.NET Core Identity 文章和教程中的指南执行操作,使 Identity 功能在应用的 Blazor 部分生效。

有关 ASP.NET Core Identity 之外的常规状态管理的指南,请参阅 ASP.NET Core Blazor 状态管理

其他安全抽象

另外两个抽象参与管理身份验证状态:

注意

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

临时重定向 URL 有效期

本部分适用于 BlazorWeb 应用。

使用 RazorComponentsServiceOptions.TemporaryRedirectionUrlValidityDuration 选项获取或设置 Blazor 由服务器端的呈现所发出的临时重定向 URL 的数据保护有效期。 这些只是暂时使用,因此生存期只需为客户端提供足够时间接收 URL 和开始导航到该 URL 即可。 但它仍应足够长,足以满足跨服务器的时钟偏斜。 默认值为 5 分钟。

在以下示例中,该值延长至 7 分钟:

builder.Services.AddRazorComponents(options => 
    options.TemporaryRedirectionUrlValidityDuration = 
        TimeSpan.FromMinutes(7));

其他资源