ASP.NET Core Blazor 身份验证和授权

注意

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

警告

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

重要

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

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

本文介绍 ASP.NET Core 对 Blazor 应用中的安全配置和管理的支持。

Blazor 使用现有的 ASP.NET Core 身份验证机制来确立用户的 identity。 具体机制取决于在服务器端或客户端托管 Blazor 应用的方式。

服务器端和客户端运行的授权代码在 Blazor 应用中的安全方案存在差异。 对于在服务器上运行的授权代码,授权检查能够对应用和组件区域强制实施访问规则。 由于客户端代码执行可能会被篡改,因此无法信任在客户端上执行的授权代码必定强制实施访问规则或控制客户端内容的显示。

如果必须保证强制实施授权规则,请不要在客户端代码中实现授权检查。 生成一个仅依赖服务器端呈现 (SSR) 进行授权检查和规则执行的 Blazor Web App。

如果必须保证强制实施授权规则并保证数据和代码安全,请不要开发客户端应用。 构建 Blazor Server 应用。

Razor Pages 授权约定 不适用于可路由的 Razor 组件。 如果将不可路由的 Razor 组件嵌入到 Razor Pages 应用的页面中,则该页面的授权约定会间接影响 Razor 组件以及页面内容的 rest。

ASP.NET Core Identity 设计用于 HTTP 请求和响应通信的上下文中,通常不是 Blazor 应用客户端-服务器通信模型。 将 ASP.NET Core Identity 用于用户管理的 ASP.NET Core 应用应该使用 Razor Pages,而不是 Identity 相关的 UI 的 Razor 组件,例如用户注册、登录、注销和其他用户管理任务。 在多种场景下,构建直接处理 Identity 任务的 Razor 组件是可行的,但 Microsoft 不建议或不支持此操作。

Razor 组件中不支持 ASP.NET Core 抽象,例如 SignInManager<TUser>UserManager<TUser>。 有关将 ASP.NET Core Identity 与 Blazor 结合使用的详细信息,请参阅在服务器端 Blazor 应用中构架 ASP.NET Core Identity

注意

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

安全地维护敏感数据和凭据

不要在客户端代码中存储应用机密、连接字符串、凭据、密码、个人标识号(PIN)、专用 .NET/C# 代码或私钥/令牌,这始终不安全 Blazor客户端代码应通过你控制的安全的 Web API 访问安全服务和数据库。

在测试/暂存和生产环境中,服务器端 Blazor 代码和 Web API 应使用安全身份验证流,以避免在项目代码或配置文件中维护凭据。 在本地开发测试之外,我们建议避免使用环境变量来存储敏感数据,因为环境变量不是最安全的方法。 对于本地开发测试, 建议使用机密管理器工具 来保护敏感数据。 有关更多信息,请参阅以下资源:

对于客户端和服务器端本地开发和测试,请使用 机密管理器工具来 保护敏感凭据。

Microsoft Azure 服务的托管标识

对于 Microsoft Azure 服务,我们建议使用托管标识。 托管标识可安全地向 Azure 服务进行身份验证,而无需在应用代码中存储凭据。 有关更多信息,请参阅以下资源:

防伪支持

Blazor 模板:

AntiforgeryToken 组件将防伪令牌呈现为隐藏字段,此组件会自动添加到窗体 (EditForm) 实例中。 有关详细信息,请参阅 ASP.NET Core Blazor 表单概述

AntiforgeryStateProvider 服务提供对与当前会话关联的防伪令牌的访问权限。 注入服务并调用其 GetAntiforgeryToken() 方法以获取当前的 AntiforgeryRequestToken。 有关详细信息,请参阅在 ASP.NET Core Blazor 应用中调用 Web API

Blazor 将请求令牌存储在组件状态中,这可以保证防伪令牌可供交互式组件使用,即便它们无权访问请求。

注意

只有当将表单数据以编码为 application/x-www-form-urlencodedmultipart/form-datatext/plain 的形式提交到服务器时,才要求防伪迁移,因为这些形式是唯一有效的表单 enctype

有关更多信息,请参阅以下资源:

服务器端 Blazor 身份验证

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

身份验证上下文仅在应用启动时建立,即当应用首次 通过 SignalR 与客户端的连接 连接到 WebSocket 时。 身份验证可以基于 cookie 或某个其他持有者令牌,但身份验证通过 SignalR 中心和完全在线路中进行管理。 线路的生存期内会保留身份验证上下文。 应用会定期(每隔 30 分钟)重新验证用户的身份验证状态。

如果应用必须捕获用户以获取自定义服务或响应用户更新,请参阅 ASP.NET 核心服务器端和其他 Blazor Web App 安全方案

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 类型指定 (?)。

内置或自定义的 AuthenticationStateProvider 服务可从 ASP.NET Core 的 HttpContext.User 获取身份验证状态数据。 身份验证状态就是这样与现有 ASP.NET Core 身份验证机制集成。

有关服务器端身份验证的详细信息,请参阅 ASP.NET 核心 Blazor 身份验证和授权

Razor 组件中的 IHttpContextAccessor/HttpContext

必须避免将 IHttpContextAccessor 与交互式呈现一起使用,因为无有效的 HttpContext 可用。

IHttpContextAccessor 可用于在服务器上静态呈现的组件。 但是,建议尽可能避免这种做法。

HttpContext 只能在常规任务的静态呈现根组件中用作级联参数,这些任务包括检查和修改 App 组件 (Components/App.razor) 中的标头或其他属性。 对于交互式呈现,值始终是 null

[CascadingParameter]
public HttpContext? HttpContext { get; set; }

对于交互式组件中需要 HttpContext 的场景,建议通过服务器的永久性组件状态传输数据。 有关详细信息,请参阅 ASP.NET 核心服务器端和其他 Blazor Web App 安全方案

请勿在服务器端 Blazor 应用的 Razor 组件中直接或间接使用 IHttpContextAccessor/HttpContext Blazor 应用在 ASP.NET Core 管道上下文之外运行。 既不保证 HttpContextIHttpContextAccessor 中可用,也不保证 HttpContext 会保留启动了 Blazor 应用的上下文。

建议在 Blazor 应用的初始呈现期间通过根组件参数将请求状态传递给此应用。 或者,应用可以将数据复制到根组件的初始化生命周期事件中的作用域内服务中,以便在整个应用中使用。 有关详细信息,请参阅 ASP.NET 核心服务器端和其他 Blazor Web App 安全方案

服务器端 Blazor 安全性的一个关键方面是,连接到给定线路的用户可能会在建立 Blazor 线路后的某个时间点得到更新,但 IHttpContextAccessor 不会更新。 有关使用自定义服务解决这种情况的详细信息,请参阅 ASP.NET 核心服务器端和其他 Blazor Web App 安全方案

共享状态

服务器端 Blazor 应用位于服务器内存中,多个应用会话托管在同一进程中。 对于每个应用会话,Blazor 根据其自身的依赖项注入容器范围启动线路,因此,作用域服务对于每个 Blazor 会话是唯一的。

警告

我们不建议同一服务器上的应用共享使用单一实例服务的状态,除非采取了极其谨慎的措施,因为这可能会带来安全漏洞,如跨线路泄露用户状态。

如果有状态的单一实例服务是专门为 Blazor 应用设计的,则可以在这些应用中使用这些服务。 例如,使用单一实例内存缓存是可以接受的,因为内存缓存需要密钥来访问给定条目。 假设用户无法控制随缓存一起使用的缓存键,则存储在缓存中的状态不会跨线路泄漏。

有关状态管理的一般指南,请参阅 ASP.NET Core Blazor 状态管理

敏感数据和凭据的服务器端安全性

在测试/暂存和生产环境中,服务器端 Blazor 代码和 Web API 应使用安全身份验证流,以避免在项目代码或配置文件中维护凭据。 在本地开发测试之外,我们建议避免使用环境变量来存储敏感数据,因为环境变量不是最安全的方法。 对于本地开发测试, 建议使用机密管理器工具 来保护敏感数据。 有关更多信息,请参阅以下资源:

对于客户端和服务器端本地开发和测试,请使用 机密管理器工具来 保护敏感凭据。

项目模板

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

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

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

BlazorIdentity UI(个人帐户)

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

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

模板:

  • 支持经过身份验证的用户的交互式服务器端呈现(交互式 SSR)和客户端呈现 (CSR) 方案。
  • 添加 IdentityRazor 组件和相关逻辑,用于例行身份验证任务,例如让用户登录和注销。这些 Identity 组件还支持高级 Identity 功能,例如使用第三方应用进行帐户确认和密码恢复多重身份验证。 请注意,Identity 组件本身不支持交互性。
  • 添加 Identity 相关的包和依赖项。
  • 引用 _Imports.razor 中的 Identity 包。
  • 创建自定义用户 Identity 类(ApplicationUser)。
  • 创建和注册 EF Core 数据库上下文(ApplicationDbContext)。
  • 配置内置 Identity 终结点的路由。
  • 包含 Identity 验证和业务逻辑。

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

选择交互式 WebAssembly 或“交互式自动”呈现模式时,服务器将处理所有身份验证和授权请求,Identity 组件将在 Blazor Web App 的主项目中的服务器上静态呈现。

该框架在服务器和客户端 (.Client) 项目中提供自定义 AuthenticationStateProvider,用于将用户的身份验证状态流向浏览器。 服务器项目会调用 AddAuthenticationStateSerialization,而客户端项目会调用 AddAuthenticationStateDeserialization。 在服务器上而不是在客户端上进行身份验证,可以让应用在预呈现期间和初始化 .NET WebAssembly 运行时之前访问身份验证状态。 自定义 AuthenticationStateProvider 实现使用 永久性组件状态服务(PersistentComponentState)将身份验证状态序列化为 HTML 注释,然后重新从 WebAssembly 读取,以创建新的 AuthenticationState 实例。 有关详细信息,请参阅管理 Blazor Web App 中的身份验证状态部分。

仅适用于交互式服务器解决方案,IdentityRevalidatingAuthenticationStateProvider(引用源) 是服务器端 AuthenticationStateProvider,每 30 分钟重新评估连接交互式线路时连接的用户的安全戳。

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

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

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

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

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

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

注意

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

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

本节适用于 Blazor Web App,其采用:

  • 个人帐户
  • 客户端呈现(CSR、基于 WebAssembly 的交互性)。

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

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

在服务器项目的 Program 文件中,调用 AddAuthenticationStateSerialization,它使用 持久组件状态服务(PersistentComponentState)序列化服务器端 AuthenticationStateProvider 返回的 AuthenticationState

builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents()
    .AddAuthenticationStateSerialization();

API 只会序列化服务器端的名称和角色声明,以便在浏览器中访问。 若要包括所有声明,请在服务器端调用中将 SerializeAllClaims 设置为 trueAddAuthenticationStateSerialization

builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents()
    .AddAuthenticationStateSerialization(
        options => options.SerializeAllClaims = true);

在客户端 (.Client) 项目的 Program 文件中,调用 AddAuthenticationStateDeserialization,这会添加一个 AuthenticationStateProvider,其中使用 AuthenticationStateData 持久组件状态服务 (PersistentComponentState) 从服务器反序列化 AuthenticationState。 服务器项目中应有对 AddAuthenticationStateSerialization 的相应调用。

builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthenticationStateDeserialization();

注意

指向 .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,因为这会导致创建未正确初始化的 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 项目模板的应用,如果在 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)

注销时的身份验证状态管理

服务器端 Blazor 在线路的生存期内保留用户身份验证状态,包括跨浏览器选项卡。 若要在用户在一个选项卡上注销时跨浏览器选项卡主动注销用户,必须使用一个简短 RevalidationInterval 来实现 RevalidatingServerAuthenticationStateProvider引用源)。

注意

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

临时重定向 URL 有效期

本部分适用于 Blazor Web App。

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

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

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

客户端 Blazor 身份验证

在客户端 Blazor 应用中,可以绕过客户端身份验证检查,因为用户可以修改所有客户端代码。 所有客户端应用程序技术都是如此,其中包括 JavaScript SPA 框架和任何操作系统的本机应用程序。

添加以下内容:

若要处理身份验证,请使用内置或自定义 AuthenticationStateProvider 服务。

有关客户端身份验证的详细信息,请参阅 安全 ASP.NET Core Blazor WebAssembly

AuthenticationStateProvider 服务

AuthenticationStateProviderAuthorizeView 组件和级联身份验证服务用于获取用户的身份验证状态的基础服务。

AuthenticationStateProviderAuthorizeView 组件和 CascadingAuthenticationState 组件用于获取用户身份验证状态的基础服务。

通常不直接使用 AuthenticationStateProvider。 使用本文后面介绍的 AuthorizeView 组件Task<AuthenticationState> 方法。 直接使用 AuthenticationStateProvider 的主要缺点是,如果基础身份验证状态数据发生更改,不会自动通知组件。

若要实现自定义 AuthenticationStateProvider,请参阅 ASP.NET Core Blazor 身份验证状态,其中包括有关实现用户身份验证状态更改通知的指导。

获取用户的声明主体数据

AuthenticationStateProvider 服务可以提供当前用户的 ClaimsPrincipal 数据,如以下示例所示。

ClaimsPrincipalData.razor:

@page "/claims-principal-data"
@using System.Security.Claims
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>ClaimsPrincipal Data</h1>

<button @onclick="GetClaimsPrincipalData">Get ClaimsPrincipal Data</button>

<p>@authMessage</p>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li>@claim.Type: @claim.Value</li>
        }
    </ul>
}

<p>@surname</p>

@code {
    private string? authMessage;
    private string? surname;
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    private async Task GetClaimsPrincipalData()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            authMessage = $"{user.Identity.Name} is authenticated.";
            claims = user.Claims;
            surname = user.FindFirst(c => c.Type == ClaimTypes.Surname)?.Value;
        }
        else
        {
            authMessage = "The user is NOT authenticated.";
        }
    }
}

在上面的示例中:

@page "/claims-principal-data"
@using System.Security.Claims
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>ClaimsPrincipal Data</h1>

<button @onclick="GetClaimsPrincipalData">Get ClaimsPrincipal Data</button>

<p>@authMessage</p>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li>@claim.Type: @claim.Value</li>
        }
    </ul>
}

<p>@surname</p>

@code {
    private string? authMessage;
    private string? surname;
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    private async Task GetClaimsPrincipalData()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            authMessage = $"{user.Identity.Name} is authenticated.";
            claims = user.Claims;
            surname = user.FindFirst(c => c.Type == ClaimTypes.Surname)?.Value;
        }
        else
        {
            authMessage = "The user is NOT authenticated.";
        }
    }
}

由于用户是 ClaimsPrincipal,如果 user.Identity.IsAuthenticatedtrue,可以枚举声明并评估角色成员身份。

有关依赖关系注入 (DI) 和服务的详细信息,请参阅 ASP.NET Core Blazor 依赖关系注入ASP.NET Core 中的依赖关系注入。 有关如何实现自定义 AuthenticationStateProvider 的信息,请参阅 ASP.NET Core Blazor 身份验证状态

公开身份验证状态作为级联参数

如果过程逻辑需要身份验证状态数据(如在执行用户触发的操作时),请通过定义类型为 Task<AuthenticationState>级联参数来获取身份验证状态数据,如以下示例所示。

CascadeAuthState.razor:

@page "/cascade-auth-state"

<h1>Cascade Auth State</h1>

<p>@authMessage</p>

@code {
    private string authMessage = "The user is NOT authenticated.";

    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (authenticationState is not null)
        {
            var authState = await authenticationState;
            var user = authState?.User;

            if (user?.Identity is not null && user.Identity.IsAuthenticated)
            {
                authMessage = $"{user.Identity.Name} is authenticated.";
            }
        }
    }
}
@page "/cascade-auth-state"

<h1>Cascade Auth State</h1>

<p>@authMessage</p>

@code {
    private string authMessage = "The user is NOT authenticated.";

    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (authenticationState is not null)
        {
            var authState = await authenticationState;
            var user = authState?.User;

            if (user?.Identity is not null && user.Identity.IsAuthenticated)
            {
                authMessage = $"{user.Identity.Name} is authenticated.";
            }
        }
    }
}

如果 user.Identity.IsAuthenticatedtrue,可以枚举声明并评估角色成员身份。

使用 AuthorizeRouteView 和级联身份验证状态服务设置 Task<AuthenticationState> 级联参数

从启用了身份验证的某个 Blazor 项目模板创建 Blazor 应用时,该应用包含 AuthorizeRouteView 和对 AddCascadingAuthenticationState 的调用,如下例所示。 客户端 Blazor 应用还包括所需的服务注册。 使用 Router 组件自定义未授权的内容部分中提供了更多信息。

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

Program 文件中,注册级联身份验证状态服务:

builder.Services.AddCascadingAuthenticationState();

使用 AuthorizeRouteViewCascadingAuthenticationState 组件设置 Task<AuthenticationState> 级联参数

从启用了身份验证的某个 Blazor 项目模板创建 Blazor 应用时,该应用包含 AuthorizeRouteViewCascadingAuthenticationState 组件,如下例所示。 客户端 Blazor 应用还包括所需的服务注册。 使用 Router 组件自定义未授权的内容部分中提供了更多信息。

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

注意

随着 ASP.NET Core 5.0.1 的发布及任何附加 5.x 版本的推出,Router 组件包含 PreferExactMatches 参数(设置为 @true)。 有关详细信息,请参阅从 ASP.NET Core 3.1 迁移到 5.0

在客户端 Blazor 应用中,将授权服务添加到 Program 文件:

builder.Services.AddAuthorizationCore();

在客户端 Blazor 应用中,将选项和授权服务添加到 Program 文件:

builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();

在服务器端 Blazor 应用中,已有选项和授权服务,因此无需执行进一步的步骤。

授权

对用户进行身份验证后,应用授权规则来控制用户可以执行的操作。

通常根据以下几点确定是授权访问还是拒绝访问:

  • 已对用户进行身份验证(已登录)。
  • 用户属于某个角色。
  • 用户具有声明。
  • 满足策略要求。

上述所有概念都与 ASP.NET Core MVC 或 Razor Pages 应用中的概念相同。 有关 ASP.NET Core 安全性的详细信息,请参阅 ASP.NET Core 安全性和 Identity 下的文章。

AuthorizeView 组件

AuthorizeView 组件根据用户是否获得授权来选择性地显示 UI 内容。 如果只需要为用户显示数据而不需要在程序逻辑中使用用户的 identity,那么此方法很有用。

该组件公开了一个 AuthenticationState 类型的 context 变量(Razor 语法中的 @context),可以使用该变量来访问有关已登录用户的信息:

<AuthorizeView>
    <p>Hello, @context.User.Identity?.Name!</p>
</AuthorizeView>

此外,如果用户未通过 AuthorizedNotAuthorized 参数的组合获得授权,你还可提供不同的内容以供显示:

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
        <p><button @onclick="HandleClick">Authorized Only Button</button></p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    private void HandleClick() { ... }
}

尽管组件 AuthorizeView 基于用户的授权状态控制元素的可见性,但它不会对事件处理程序本身强制实施安全性。 在前面的示例中,该方法 HandleClick 仅与授权用户可见的按钮相关联,但不会阻止从其他地方调用此方法。 若要确保方法级安全性,在处理程序本身或相关 API 中实现其他授权逻辑。

当服务器端在静态服务器端呈现(静态 SSR)期间授权失败时,Blazor Web App 的 Razor 组件永远不会显示 <NotAuthorized> 内容。 服务器端 ASP.NET 核心管道处理服务器上的授权。 使用服务器端技术来处理未经授权的请求。 有关详细信息,请参阅 ASP.NET Core Blazor 呈现模式

警告

AuthorizeView 关联的客户端标记和方法仅在客户端 Blazor 应用中呈现的 UI 中受到查看和执行保护。 为了保护客户端 Blazor 中的授权内容和安全方法,内容通常由对服务器 API 的安全授权 Web API 调用提供,并且永远不会存储在应用中。 有关详细信息,请参阅从 ASP.NET Core Blazor 应用调用 Web APIASP.NET Core Blazor WebAssembly 其他安全方案

AuthorizedNotAuthorized 的内容可以包括任意项,如其他交互式组件。

授权一节中介绍了授权条件,如用于控制 UI 选项或访问权限的角色或策略。

如果未指定授权条件,则 AuthorizeView 使用默认策略:

  • 将经过身份验证(已登录)的用户视为已授权。
  • 将未经过身份验证(已注销)的用户视为未授权。

可以在 NavMenu 组件 (Shared/NavMenu.razor) 中使用 AuthorizeView 组件来显示 NavLink 组件 (NavLink),但请注意,此方法仅从呈现的输出中删除列表项。 它不会阻止用户导航到该组件。 在目标组件中单独实现授权。

基于角色和基于策略的授权

AuthorizeView 组件支持基于角色或基于策略的授权 。

对于基于角色的授权,请使用 Roles 参数。 在以下示例中,用户必须对 AdminSuperuser 角色具有角色声明:

<AuthorizeView Roles="Admin, Superuser">
    <p>You have an 'Admin' or 'Superuser' role claim.</p>
</AuthorizeView>

若要要求用户同时具有 AdminSuperuser 角色声明,请嵌套 AuthorizeView 组件:

<AuthorizeView Roles="Admin">
    <p>User: @context.User</p>
    <p>You have the 'Admin' role claim.</p>
    <AuthorizeView Roles="Superuser" Context="innerContext">
        <p>User: @innerContext.User</p>
        <p>You have both 'Admin' and 'Superuser' role claims.</p>
    </AuthorizeView>
</AuthorizeView>

前面的代码为内部 AuthorizeView 组件建立 Context,防止出现 AuthenticationState 上下文冲突。 在外部 AuthorizeView 中,AuthenticationState 上下文是通过用于访问上下文 (@context.User) 的标准方法访问的。 该上下文在内部 AuthorizeView 中使用命名的 innerContext 上下文 (@innerContext.User) 进行访问。

有关详细信息(包括配置指南),请参阅 ASP.NET Core 中基于角色的授权

对于基于策略的授权,请使用 Policy 具有单个策略名称的参数:

<AuthorizeView Policy="Over21">
    <p>You satisfy the 'Over21' policy.</p>
</AuthorizeView>

若要处理用户应满足多个策略之一的情况,请创建可确认用户满足其他策略的策略。

若要处理用户必须同时满足多个策略的情况,请采用以下任一方法:

  • AuthorizeView 创建一个策略,用于确认用户是否满足其他几个策略。

  • 将策略嵌套在多个 AuthorizeView 组件中:

    <AuthorizeView Policy="Over21">
        <AuthorizeView Policy="LivesInCalifornia">
            <p>You satisfy the 'Over21' and 'LivesInCalifornia' policies.</p>
        </AuthorizeView>
    </AuthorizeView>
    

基于策略的授权包含一个特例,即基于声明的授权。 例如,可以定义一个要求用户具有特定声明的策略。 有关详细信息,请参阅 ASP.NET Core 中基于策略的授权

如果 RolesPolicy 均未指定,则 AuthorizeView 使用默认策略:

  • 将经过身份验证(已登录)的用户视为已授权。
  • 将未经过身份验证(已注销)的用户视为未授权。

由于 .NET 字符串比较会区分大小写,因此匹配的角色和策略名称也区分大小写。 例如,Admin(大写 A)不被视为与 admin(小写 a)相同的角色。

Pascal 大小写通常用于角色和策略名称(例如 BillingAdministrator),但使用 Pascal 大小写并非严格要求。 允许不同的包装方案,如骆驼元素、烤肉元素和蛇元素。 在角色和策略名称中使用空格也是不常见的,但框架允许使用。 例如,billing administrator 在 .NET 应用中是一种不常见的角色或策略名称格式,但却是一种有效的角色或策略名称。

异步身份验证期间显示的内容

通过 Blazor,可通过异步方式确定身份验证状态。 此方法的主要应用场景是客户端 Blazor 应用向外部终结点发出身份验证请求。

正在进行身份验证时,AuthorizeView 不显示任何内容。 若要在进行身份验证时显示内容,请将内容分配给 Authorizing 参数:

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <Authorizing>
        <p>You can only see this content while authentication is in progress.</p>
    </Authorizing>
</AuthorizeView>

此方法通常不适用于服务器端 Blazor 应用。 身份验证状态一经确立,服务器端 Blazor 应用便会立即获知身份验证状态。 Authorizing 内容可以在应用的 AuthorizeView 组件中提供,但此内容从不显示。

[Authorize] 特性

[Authorize] 特性在 Razor 组件中可用:

@page "/"
@attribute [Authorize]

You can only see this if you're signed in.

重要

请仅在通过 Blazor 路由器到达的 @page 组件上使用 [Authorize]。 授权仅作为路由的一个方面执行,而不是作为页面中呈现的子组件来执行。 若要授权在页面中显示特定部分,请改用 AuthorizeView

[Authorize] 特性还支持基于角色或基于策略的授权。 对于基于角色的授权,请使用 Roles 参数:

@page "/"
@attribute [Authorize(Roles = "Admin, Superuser")]

<p>You can only see this if you're in the 'Admin' or 'Superuser' role.</p>

对于基于策略的授权,请使用 Policy 参数:

@page "/"
@attribute [Authorize(Policy = "Over21")]

<p>You can only see this if you satisfy the 'Over21' policy.</p>

如果 RolesPolicy 均未指定,则 [Authorize] 使用默认策略:

  • 将经过身份验证(已登录)的用户视为已授权。
  • 将未经过身份验证(已注销)的用户视为未授权。

当用户未获得授权并且应用未使用 Router 组件自定义未授权的内容时,框架会自动显示以下回退消息:

Not authorized.

资源授权

若要授权用户访问资源,请将请求的路由数据传递到 AuthorizeRouteViewResource 参数。

在请求的路由的 Router.Found 内容中:

<AuthorizeRouteView Resource="routeData" RouteData="routeData" 
    DefaultLayout="typeof(MainLayout)" />

若要详细了解如何在过程逻辑中传递和使用授权状态数据,请参阅公开身份验证状态作为级联参数部分。

AuthorizeRouteView 接收资源的路由数据时,授权策略可访问允许自定义逻辑做出授权决策的 RouteData.PageTypeRouteData.RouteValues

在下面的示例中,通过以下逻辑在 AuthorizationOptions 中为应用的授权服务配置 (AddAuthorizationCore) 创建 EditUser 策略:

  • 确定是否存在包含密钥 id 的路由值。 如果存在该密钥,则路由值存储在 value 中。
  • 在名为 id 的变量中,将 value 存储为字符串,或者设置空字符串值 (string.Empty)。
  • 如果 id 不是空字符串,则在字符串的值以 EMP 开头时断言符合策略(返回 true)。 否则,断言策略失败(返回 false)。

Program 文件中:

  • 添加 Microsoft.AspNetCore.ComponentsSystem.Linq 的命名空间:

    using Microsoft.AspNetCore.Components;
    using System.Linq;
    
  • 添加策略:

    options.AddPolicy("EditUser", policy =>
        policy.RequireAssertion(context =>
        {
            if (context.Resource is RouteData rd)
            {
                var routeValue = rd.RouteValues.TryGetValue("id", out var value);
                var id = Convert.ToString(value, 
                    System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty;
    
                if (!string.IsNullOrEmpty(id))
                {
                    return id.StartsWith("EMP", StringComparison.InvariantCulture);
                }
            }
    
            return false;
        })
    );
    

上述示例是一个过于简单的授权策略,仅用于通过工作示例演示概念。 若要详细了解如何创建和配置授权策略,请参阅 ASP.NET Core 中基于策略的授权

在以下 EditUser 组件中,位于 /users/{id}/edit 的资源具有用户标识符的路由参数 ({id})。 该组件使用前面的 EditUser 授权策略来确定 id 的路由值是否以 EMP 开头。 如果 idEMP 开头,则策略成功,并且已授权访问组件。 如果 id 不以 EMP 值开头,或者 id 是空字符串,则策略失败,并且不会加载组件。

EditUser.razor:

@page "/users/{id}/edit"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "EditUser")]

<h1>Edit User</h1>

<p>The "EditUser" policy is satisfied! <code>Id</code> starts with 'EMP'.</p>

@code {
    [Parameter]
    public string? Id { get; set; }
}
@page "/users/{id}/edit"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "EditUser")]

<h1>Edit User</h1>

<p>The "EditUser" policy is satisfied! <code>Id</code> starts with 'EMP'.</p>

@code {
    [Parameter]
    public string? Id { get; set; }
}

使用 Router 组件自定义未经授权的内容

Router 组件与 AuthorizeRouteView 组件搭配使用时,可允许应用程序在以下情况下指定自定义内容:

重要

Blazor 显示 <NotAuthorized><NotFound> 内容的路由器功能在静态服务器端呈现(静态 SSR)期间无法运行,因为请求处理完全由 ASP.NET 核心中间件管道请求处理进行处理,而对于未经授权或错误的请求,根本不会呈现 Razor 组件。 使用服务器端技术在静态 SSR 期间处理未经授权的和错误的请求。 有关详细信息,请参阅 ASP.NET Core Blazor 呈现模式

<Router ...>
    <Found ...>
        <AuthorizeRouteView ...>
            <NotAuthorized>
                ...
            </NotAuthorized>
            <Authorizing>
                ...
            </Authorizing>
        </AuthorizeRouteView>
    </Found>
</Router>

AuthorizedNotAuthorized 的内容可以包括任意项,如其他交互式组件。

注意

上面的内容要求在应用的 Program 文件中注册级联身份验证状态服务:

builder.Services.AddCascadingAuthenticationState();
<CascadingAuthenticationState>
    <Router ...>
        <Found ...>
            <AuthorizeRouteView ...>
                <NotAuthorized>
                    ...
                </NotAuthorized>
                <Authorizing>
                    ...
                </Authorizing>
            </AuthorizeRouteView>
        </Found>
    </Router>
</CascadingAuthenticationState>

NotFoundAuthorizedNotAuthorized 的内容可以包括任意项,如其他交互式组件。

如果未指定 NotAuthorized 内容,AuthorizeRouteView 就会使用以下回退消息:

Not authorized.

从启用了身份验证的 Blazor WebAssembly 项目模板创建的应用包含一个 RedirectToLogin 组件,它位于 Router 组件的 <NotAuthorized> 内容中。 当用户未进行身份验证 (context.User.Identity?.IsAuthenticated != true) 时,RedirectToLogin 组件会将浏览器重定向到 authentication/login 终结点以进行身份验证。 使用 identity 提供程序进行身份验证后,用户将返回到请求的 URL。

过程逻辑

如果需要应用在过程逻辑中检查授权规则,请使用类型为 Task<AuthenticationState> 的级联参数来获取用户的 ClaimsPrincipalTask<AuthenticationState> 可以与其他服务(如 IAuthorizationService)结合使用来评估策略。

如下示例中:

  • user.Identity.IsAuthenticated 为经过身份验证(已登录)的用户执行代码。
  • user.IsInRole("admin") 为担任“管理员”角色的用户执行代码。
  • (await AuthorizationService.AuthorizeAsync(user, "content-editor")).Succeeded 为满足“content-editor”策略的用户执行代码。

从项目模板创建时,服务器端 Blazor 应用包含相应的命名空间。 在客户端 Blazor 应用中,确认组件或应用的 _Imports.razor 文件中是否存在 Microsoft.AspNetCore.AuthorizationMicrosoft.AspNetCore.Components.Authorization 命名空间:

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization

ProceduralLogic.razor:

@page "/procedural-logic"
@inject IAuthorizationService AuthorizationService

<h1>Procedural Logic Example</h1>

<button @onclick="@DoSomething">Do something important</button>

@code {
    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    private async Task DoSomething()
    {
        if (authenticationState is not null)
        {
            var authState = await authenticationState;
            var user = authState?.User;

            if (user is not null)
            {
                if (user.Identity is not null && user.Identity.IsAuthenticated)
                {
                    // ...
                }

                if (user.IsInRole("Admin"))
                {
                    // ...
                }

                if ((await AuthorizationService.AuthorizeAsync(user, "content-editor"))
                    .Succeeded)
                {
                    // ...
                }
            }
        }
    }
}
@page "/procedural-logic"
@inject IAuthorizationService AuthorizationService

<h1>Procedural Logic Example</h1>

<button @onclick="@DoSomething">Do something important</button>

@code {
    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    private async Task DoSomething()
    {
        if (authenticationState is not null)
        {
            var authState = await authenticationState;
            var user = authState?.User;

            if (user is not null)
            {
                if (user.Identity is not null && user.Identity.IsAuthenticated)
                {
                    // ...
                }

                if (user.IsInRole("Admin"))
                {
                    // ...
                }

                if ((await AuthorizationService.AuthorizeAsync(user, "content-editor"))
                    .Succeeded)
                {
                    // ...
                }
            }
        }
    }
}

排查错误

常见错误:

  • 需要 Task<AuthenticationState> 类型的级联参数才能进行授权。 请考虑使用 CascadingAuthenticationState 来提供此参数。

  • 对于 authenticationStateTask,收到了 null

项目可能不是使用启用了身份验证的服务器端 Blazor 模板创建的。

在 .NET 7 或更低版本中,请使用 <CascadingAuthenticationState> 包装 UI 树的某些部分,例如包装 Blazor 路由器:

<CascadingAuthenticationState>
    <Router ...>
        ...
    </Router>
</CascadingAuthenticationState>

在 .NET 8 中或更高版本中,请勿使用 CascadingAuthenticationState 组件:

- <CascadingAuthenticationState>
      <Router ...>
          ...
      </Router>
- </CascadingAuthenticationState>

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

builder.Services.AddCascadingAuthenticationState();

CascadingAuthenticationState 组件(.NET 7 或更低版本)或 AddCascadingAuthenticationState 提供的服务(.NET 8 或更高版本)提供了 Task<AuthenticationState> 级联参数,它随后从基础 AuthenticationStateProvider 依赖项注入服务接收该参数的值。

个人身份信息 (PII)

当文档讨论个人身份信息 (PII) 时,Microsoft 使用 GDPR 的“个人数据”定义 (GDPR 4.1)

PII 是指与已识别或可识别的自然人相关的任何信息。 可识别的自然人是可通过以下内容直接或间接识别的个人:

  • 名称
  • 身份证号
  • 位置坐标
  • 联机标识符
  • 其他特定因素
    • 物理
    • 生理特征
    • 遗传特征
    • 心理特征
    • 经济特征
    • 文化
    • 社交 identity

其他资源