使用 Microsoft Entra ID 保护托管的 ASP.NET Core Blazor WebAssembly 应用

本文介绍如何创建使用 Microsoft Entra ID (ME-ID) 进行身份验证的托管 Blazor WebAssembly 解决方案。 本文重点介绍使用单租户 Azure 应用注册的单租户应用。

本文不涉及多租户 ME-ID 注册。 有关详细信息,请参阅使应用程序成为多租户

本文重点介绍如何使用 Microsoft Entra 租户,如快速入门:设置租户中所述。 如果应用是在 Azure Active Directory B2C 租户中注册的(如教程:创建 Azure Active Directory B2C 租户中所述),但遵循本文中的指导,则 App ID URI 由 ME-ID 采用不同的方式进行管理。 有关详细信息,请参阅本文的使用 Azure Active Directory B2C 租户部分。

阅读本文后,有关其他安全场景的介绍,请参阅 ASP.NET Core Blazor WebAssembly 其他安全场景

演练

演练的子节介绍了如何:

  • 在 Azure 中创建租户
  • 在 Azure 中注册服务器 API 应用
  • 在 Azure 中注册客户端应用
  • 创建 Blazor 应用
  • 修改 Serverappsettings.json 配置
  • 修改默认访问令牌范围方案
  • 运行应用

在 Azure 中创建租户

按照快速入门:设置租户中的指南操作,在 ME-ID 中创建租户。

在 Azure 中注册服务器 API 应用

为服务器 API 应用注册 ME-ID 应用:

  1. 在 Azure 门户中导航到“Microsoft Entra ID”。 在边栏中选择“应用”>“应用注册”。 选择“新建注册”按钮。
  2. 提供应用的名称(例如 Blazor Server ME-ID)。
  3. 选择支持的帐户类型。 为此体验选择“仅此组织目录中的帐户”(单个租户)。
  4. 在此场景中,“服务器 API 应用”不需要“重定向 URI”,因此请将“选择平台”下拉列表保留为未选中状态,也不要输入重定向 URI。
  5. 本文假定已在 Microsoft Entra 租户中注册应用。 如果应用已在 Azure Active Directory B2C 租户中注册,则存在并选中“权限 > 授予管理员对 openid 和 offline_access 权限的同意”复选框。 取消选中复选框可禁用设置。 使用 Active Azure Directory 租户时,该复选框不存在。
  6. 选择“注册”。

记录以下信息:

  • “服务器 API 应用”应用程序(客户端)ID(例如 41451fa7-82d9-4673-8fa5-69eff5a761fd
  • 目录(租户)ID(例如 e86c78e2-8bb4-4c41-aefd-918e0565a45e
  • ME-ID 主域/发布者域/租户域(例如 contoso.onmicrosoft.com):该域在 Azure 门户中已注册的应用的“品牌”边栏选项卡中作为发布者域提供。

在“API 权限”中,删除“Microsoft Graph”>“User.Read”权限,因为服务器 API 应用不需要额外的 API 访问权限即可登录用户和调用服务器 API 终结点。

在“公开 API”中:

  1. 确认或添加格式为 api://{SERVER API APP CLIENT ID} 的 应用 ID URI。
  2. 选择“添加范围”。
  3. 选择“保存并继续”。
  4. 提供“作用域名称”(例如 API.Access)。
  5. 提供“管理员同意显示名称”(例如 Access API)。
  6. 提供“管理员同意说明”(例如 Allows the app to access server app API endpoints.)。
  7. 确认“状态”设置为“已启用” 。
  8. 选择“添加范围”。

记录以下信息:

  • 应用 ID URI GUID(例如 api://41451fa7-82d9-4673-8fa5-69eff5a761fd 的应用 ID URI 中的记录 41451fa7-82d9-4673-8fa5-69eff5a761fd
  • 范围名称(如 API.Access

重要

如果将自定义值用于应用 ID URI,则从 Blazor WebAssembly 项目模板创建应用后,ServerClient 应用均需要配置更改。 有关详细信息,请参阅使用自定义应用 ID URI 部分。

在 Azure 中注册客户端应用

为客户端应用注册 ME-ID 应用:

  1. 在 Azure 门户中导航到 Microsoft Entra ID。 在边栏中选择“应用注册”。 选择“新建注册”按钮。
  2. 提供应用的名称(例如 Blazor 客户端 ME-ID)。
  3. 选择支持的帐户类型。 为此体验选择“仅此组织目录中的帐户”(单个租户)。
  4. 将“重定向 URI”下拉列表设置为“单页应用程序(SPA)”,并提供以下重定向 URI:https://localhost/authentication/login-callback。 如果知道 Azure 默认主机(例如 azurewebsites.net)或自定义域主机(例如 contoso.com)的生产重定向 URI,还可以在提供 localhost 重定向 URI 的同时添加添加生产重定向 URI。 请确保在添加的任何生产重定向 URI 中包含非 :443 端口的端口号。
  5. 本文假定已在 Microsoft Entra 租户中注册应用。 如果应用已在 Azure Active Directory B2C 租户中注册,则存在并选中“权限 > 授予管理员对 openid 和 offline_access 权限的同意”复选框。 取消选中复选框可禁用设置。 使用 Active Azure Directory 租户时,该复选框不存在。
  6. 选择“注册”。

注意

不需要为 localhost ME-ID 重定向 URI 提供端口号。 有关详细信息,请参阅重定向 URI(回复 URL)限制和局限:Localhost 异常(Entra 文档)

记录 Client 应用应用程序(客户端)ID(例如 4369008b-21fa-427c-abaa-9b53bf58e538)。

在“身份验证”>“平台配置”>“单页应用程序”中:

  1. 确认存在 https://localhost/authentication/login-callback 的重定向 URI。
  2. 在“隐式授权”部分中,请确保没有选中“访问令牌”和“ID 令牌”的复选框。 对于使用 MSAL v2.0 或更高版本的 Blazor 应用,不建议使用隐式授权。 有关详细信息,请参阅保护 ASP.NET Core Blazor WebAssembly
  3. 此体验可接受应用的其余默认值。
  4. 如果进行了更改,请选择“保存”按钮。

在“API 权限”中:

  1. 确认应用拥有“Microsoft Graph”>“User.Read”权限 。
  2. 选择“添加权限”,然后选择“我的 API” 。
  3. 从“名称”列(例如 Blazor Server ME-ID)中选择“服务器 API 应用”。
  4. 打开 API 列表。
  5. 启用对 API 的访问(例如 API.Access)。
  6. 选择“添加权限”。
  7. 选择“为 {TENANT NAME} 授予管理员内容”按钮。 请选择“是”以确认。

重要

如果你在“API 权限”配置的最后一个步骤中无权向租户授予管理员同意(原因是对于使用该应用的同意已委派给用户),那么必须执行以下额外步骤:

  • 应用必须使用受信任的发布者域
  • 在 Azure 门户中的 Server 应用的配置中,选择“公开 API”。 在“授权的客户端应用程序”下,选择“添加客户端应用程序”按钮。 添加 Client 应用的应用程序(客户端)ID(例如 4369008b-21fa-427c-abaa-9b53bf58e538)。

创建 Blazor 应用

在空文件夹中,将以下命令中的占位符替换为前面记录的信息,然后在命令行界面中执行该命令:

dotnet new blazorwasm -au SingleOrg --api-client-id "{SERVER API APP CLIENT ID}" --app-id-uri "{SERVER API APP ID URI GUID}" --client-id "{CLIENT APP CLIENT ID}" --default-scope "{DEFAULT SCOPE}" --domain "{TENANT DOMAIN}" -ho -o {PROJECT NAME} --tenant-id "{TENANT ID}"

警告

请勿在应用名称 {PROJECT NAME} 中使用短划线 (-),它会破坏 OIDC 应用标识符的构成。 Blazor WebAssembly 项目模板中的逻辑在解决方案的配置中对 OIDC 应用标识符使用项目名称。 接受改用帕斯卡命名法 (BlazorSample) 或下划线 (Blazor_Sample)。 有关详细信息,请参阅托管的 Blazor WebAssembly 项目名称中的短划线会破坏 OIDC 安全性 (dotnet/aspnetcore #35337)

占位符 Azure 门户中的名称 示例
{PROJECT NAME} BlazorSample
{CLIENT APP CLIENT ID} Client 应用的应用程序(客户端)ID 4369008b-21fa-427c-abaa-9b53bf58e538
{DEFAULT SCOPE} 作用域名 API.Access
{SERVER API APP CLIENT ID} “服务器 API 应用”的应用程序(客户端)ID 41451fa7-82d9-4673-8fa5-69eff5a761fd
{SERVER API APP ID URI GUID} 应用程序 ID URI GUID 41451fa7-82d9-4673-8fa5-69eff5a761fd(仅限 GUID,默认情况下与 {SERVER API APP CLIENT ID} 匹配)
{TENANT DOMAIN} 主域/发布者域/租户域 contoso.onmicrosoft.com
{TENANT ID} 目录(租户)ID e86c78e2-8bb4-4c41-aefd-918e0565a45e

使用 -o|--output 选项指定的输出位置将创建一个项目文件夹(如果该文件夹不存在)并成为项目名称的一部分。 请勿在应用名称中使用短划线 (-),它会破坏 OIDC 应用标识符的构成(请查看前面的警告)。

重要

如果将自定义值用于应用 ID URI,则从 Blazor WebAssembly 项目模板创建应用后,ServerClient 应用均需要配置更改。 有关详细信息,请参阅使用自定义应用 ID URI 部分。

运行应用

Server 项目运行应用。 使用 Visual Studio 时,请执行以下任一操作:

  • 选择“运行”按钮旁边的下拉箭头。 从下拉列表中打开“配置启动项目”。 选择“单启动项目”选项。 确认启动项目的项目或将该项目更改为 Server 项目。

  • 确认在用以下任一方法启动应用之前,解决方案资源管理器中突出显示了 Server 项目:

    • 选择“运行”按钮。
    • 从菜单栏中,依次使用“调试”>“开始调试” 。
    • 按 F5
  • 在命令 shell 中,导航到解决方案的 Server 项目文件夹。 执行 dotnet run 命令。

配置 User.Identity.Name

本节中的指南包括根据需要使用 name 声明中的值填充 User.Identity.Name

默认情况下,Server 应用 API 使用 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name 声明类型(例如 2d64b3da-d9d5-42c6-9352-53d8df33d770@contoso.onmicrosoft.com)中的值填充 User.Identity.Name

要将应用配置为从 name 声明类型接收值,请执行以下操作:

解决方案的各个部分

本部分介绍从 Blazor WebAssembly 项目模板生成的解决方案的各个部分,并介绍如何配置解决方案的 ClientServer 项目以供参考。 如果使用演练部分的指南创建基本工作应用程序,则本部分中没有可用于该应用的特定指南。 本部分中的指南有助于更新应用以对用户进行身份验证和授权。 更新应用的另一种方法是根据演练部分的指南创建新的应用,然后将应用的组件、类和资源移动到新应用。

appsettings.json 配置

本部分涉及解决方案的 Server 应用。

appsettings.json 文件包含用于配置 JWT 持有者处理程序(用于验证访问令牌)的选项。 添加以下 AzureAd 配置部分:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "{TENANT DOMAIN}",
    "TenantId": "{TENANT ID}",
    "ClientId": "{SERVER API APP CLIENT ID}",
    "CallbackPath": "/signin-oidc",
    "Scopes": "{SCOPES}"
  }
}

示例:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "contoso.onmicrosoft.com",
    "TenantId": "e86c78e2-8bb4-4c41-aefd-918e0565a45e",
    "ClientId": "41451fa7-82d9-4673-8fa5-69eff5a761fd",
    "CallbackPath": "/signin-oidc",
    "Scopes": "API.Access"
  }
}

重要

如果已注册 Server 应用以在 ME-ID 中使用自定义应用 ID URI(不采用默认格式 api://{SERVER API APP CLIENT ID}),请参阅使用自定义应用 ID URI 部分。 ServerClient 应用均需要更改。

身份验证包

本部分涉及解决方案的 Server 应用。

Microsoft.Identity.Web 包提供使用 Microsoft Identity 平台验证和授权对 ASP.NET Core Web API 的调用的支持。

注意

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。

从 Blazor WebAssembly 模板创建的托管 Blazor 解决方案的 Server 应用默认包含 Microsoft.Identity.Web.UI 包。 包在 Web 应用中添加用于用户身份验证的 UI,并且不会被 Blazor 框架使用。 如果 Server 应用永远不会用于直接对用户进行身份验证,则可以安全地从 Server 应用的项目文件中删除包引用。

身份验证服务支持

本部分涉及解决方案的 Server 应用。

AddAuthentication 方法在应用中设置身份验证服务,并将 JWT 持有者处理程序配置为默认身份验证方法。 AddMicrosoftIdentityWebApi 方法将服务配置为使用 Microsoft Identity 平台 v2.0 保护 Web API。 此方法需要应用配置中的 AzureAd 部分通过必要的设置来初始化身份验证选项。

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));

备注

注册单个身份验证方案时,身份验证方案将自动用作应用的默认方案,无需向 AddAuthentication 或通过 AuthenticationOptions 声明方案。 有关详细信息,请参阅 ASP.NET Core 身份验证概述ASP.NET Core 公告 (aspnet/Announcements #490)

UseAuthenticationUseAuthorization 确保:

  • 应用尝试对传入请求上的令牌进行分析和验证。
  • 尝试在没有适当凭据的情况下访问受保护资源的任何请求都失败。
app.UseAuthentication();
app.UseAuthorization();

WeatherForecast 控制器

本部分涉及解决方案的 Server 应用。

WeatherForecast 控制器 (Controllers/WeatherForecastController.cs) 公开了一个受保护的 API,并将 [Authorize] 特性应用于控制器。 务必了解以下内容:

  • 仅此 API 控制器中的 [Authorize] 特性可以保护此 API 免受未经授权的访问。
  • Blazor WebAssembly 应用中使用的 [Authorize] 特性仅用作应用的提示,提示应授权用户应用才能正常运行。
[Authorize]
[ApiController]
[Route("[controller]")]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        ...
    }
}

wwwroot/appsettings.json 配置

本部分涉及解决方案的 Client 应用。

配置由 wwwroot/appsettings.json 文件提供:

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT APP CLIENT ID}",
    "ValidateAuthority": true
  }
}

示例:

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/e86c78e2-...-918e0565a45e",
    "ClientId": "4369008b-21fa-427c-abaa-9b53bf58e538",
    "ValidateAuthority": true
  }
}

身份验证包

本部分涉及解决方案的 Client 应用。

创建应用以使用工作或学校帐户 (SingleOrg) 时,应用会自动接收 Microsoft 身份验证库 (Microsoft.Authentication.WebAssembly.Msal) 的包引用。 此包提供了一组基元,可帮助应用验证用户身份并获取令牌以调用受保护的 API。

如果向应用添加身份验证,请手动将 Microsoft.Authentication.WebAssembly.Msal 包添加到应用中。

注意

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。

Microsoft.Authentication.WebAssembly.Msal 包会间接将 Microsoft.AspNetCore.Components.WebAssembly.Authentication 包传递到应用中。

身份验证服务支持

本部分涉及解决方案的 Client 应用。

添加了对 HttpClient 实例的支持,这些实例在向 Server 应用发出请求时包含访问令牌。

Program 文件中:

builder.Services.AddHttpClient("{PROJECT NAME}.ServerAPI", client => 
        client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("{PROJECT NAME}.ServerAPI"));

占位符 {PROJECT NAME} 是创建解决方案时的项目名称。 例如,提供 BlazorSample 的项目名称将生成 BlazorSample.ServerAPI 的命名的 HttpClient

使用由 Microsoft.Authentication.WebAssembly.Msal 包提供的 AddMsalAuthentication 扩展方法在服务容器中注册对用户进行身份验证的支持。 此方法设置应用与 Identity 提供者 (IP) 交互所需的服务。

Program 文件中:

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
    options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});

AddMsalAuthentication 方法接受回叫,以配置验证应用所需的参数。 注册应用时,可以从 Azure 门户 ME-ID 配置中获取配置应用所需的值。

访问令牌作用域

本部分涉及解决方案的 Client 应用。

默认访问令牌作用域表示访问令牌作用域的列表,这些作用域是:

  • 默认情况下包含在登录请求中。
  • 用于在身份验证后立即预配访问令牌。

可以根据需要在 Program 文件中添加其他范围:

builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});

使用 AdditionalScopesToConsent 指定其他作用域:

options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE URI}");

注意

当用户首次使用在 Microsoft Azure 中注册的应用时,AdditionalScopesToConsent 无法通过 Microsoft Entra ID 同意 UI 为 Microsoft Graph 预配委派的用户权限。 有关详细信息,请参阅将 Graph API 和 ASP.NET Core Blazor WebAssembly 结合使用

示例默认访问令牌范围:

options.ProviderOptions.DefaultAccessTokenScopes.Add(
    "api://41451fa7-82d9-4673-8fa5-69eff5a761fd/API.Access");

有关详细信息,请参阅“其他方案”一文的以下部分:

登录模式

本部分涉及解决方案的 Client 应用。

框架默认为弹出式登录模式;如果无法打开弹出窗口,则回到重定向登录模式。 通过将 MsalProviderOptionsLoginMode 属性设置为 redirect,将 MSAL 配置为使用重定向登录模式:

builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.LoginMode = "redirect";
});

默认设置为 popup,字符串值不区分大小写。

导入文件

本部分涉及解决方案的 Client 应用。

整个应用通过 _Imports.razor 文件提供 Microsoft.AspNetCore.Components.Authorization 命名空间:

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using {APPLICATION ASSEMBLY}
@using {APPLICATION ASSEMBLY}.Shared

索引页

本部分涉及解决方案的 Client 应用。

索引页 (wwwroot/index.html) 包含一个脚本,用于在 JavaScript 中定义 AuthenticationServiceAuthenticationService 处理 OIDC 协议的低级别详细信息。 应用从内部调用脚本中定义的方法以执行身份验证操作。

<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>

应用组件

本部分涉及解决方案的 Client 应用。

App 组件 (App.razor) 类似于 Blazor Server 应用中的 App 组件:

由于不同版本的 ASP.NET Core 中的框架发生了更改,因此本部分不会显示 App 组件 (App.razor) 的 Razor 标记。 若要检查给定版本的组件的标记,请使用以下方法之一

  • 创建一个应用,预配为从要使用的 ASP.NET Core 版本的默认 Blazor WebAssembly 项目模板进行身份验证。 在生成的应用中检查 App 组件 (App.razor)。

  • 引用源中检查 App 组件 (App.razor)。 从分支选择器中选择版本,并在存储库的 ProjectTemplates 文件夹中搜索该组件,因为经过多年它已移动。

    注意

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

RedirectToLogin 组件

本部分涉及解决方案的 Client 应用。

RedirectToLogin 组件 (RedirectToLogin.razor):

  • 管理将未经授权的用户重定向到登录页。
  • 保留用户尝试访问的当前 URL,以便在身份验证成功时可以通过以下方式将其返回到该页:
    • .NET 7 或更高版本中的 ASP.NET Core 的导航历史记录状态
    • .NET 6 或更早版本中的 ASP.NET Core 的查询字符串。

引用源中检查 RedirectToLogin 组件。 组件的位置随时间而更改,因此请使用 GitHub 搜索工具来查找该组件。

注意

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

LoginDisplay 组件

本部分涉及解决方案的 Client 应用。

LoginDisplay 组件 (LoginDisplay.razor) 在 MainLayout 组件 (MainLayout.razor) 中呈现并管理以下行为:

  • 对于经过身份验证的用户:
    • 显示当前用户名。
    • 提供指向 ASP.NET Core Identity 中的用户配置文件页面的链接。
    • 提供用于注销应用的按钮。
  • 对于匿名用户:
    • 提供用于注册的选项。
    • 提供用于登录的选项。

由于不同版本的 ASP.NET Core 中的框架发生了更改,因此本部分不会显示 LoginDisplay 组件的 Razor 标记。 若要检查给定版本的组件的标记,请使用以下方法之一

  • 创建一个应用,预配为从要使用的 ASP.NET Core 版本的默认 Blazor WebAssembly 项目模板进行身份验证。 在生成的应用中检查 LoginDisplay 组件。

  • 引用源中检查 LoginDisplay 组件。 组件的位置随时间而更改,因此请使用 GitHub 搜索工具来查找该组件。 使用 Hosted(为 true)的模板化内容。

    注意

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

身份验证组件

本部分涉及解决方案的 Client 应用。

Authentication 组件 (Pages/Authentication.razor) 生成的页面定义了处理各种身份验证阶段所需的路由。

RemoteAuthenticatorView 组件:

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action" />

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

注意

.NET 6 或更高版本的 ASP.NET Core 支持空引用类型 (NRT) 和 .NET 编译器 Null 状态静态分析。 在 .NET 6 中的 ASP.NET Core 发布之前,string 类型没有 null 类型指定 (?)。

FetchData 组件

本部分涉及解决方案的 Client 应用。

FetchData 组件显示如何:

  • 预配访问令牌。
  • 使用访问令牌调用 Server 应用中受保护的资源 API。

@attribute [Authorize] 指令向 Blazor WebAssembly 授权系统发出指示,用户必须获得授权才能访问此组件。 如果 Client 应用中存在该属性,则在没有正确凭据的情况下,不会阻止调用服务器上的 API。 Server 应用还必须在相应的终结点上使用 [Authorize] 才能适当地保护这些终结点。

IAccessTokenProvider.RequestAccessToken 负责请求可添加到请求中的访问令牌,以调用 API。 如果该令牌已缓存,或者该服务在没有用户交互的情况下能够预配新的访问令牌,则令牌请求会成功。 否则,令牌请求会失败,并出现 AccessTokenNotAvailableException,这是在 try-catch 语句中捕获的。

为了获得要包含在请求中的实际令牌,应用必须通过调用 tokenResult.TryGetToken(out var token) 来检查请求是否成功。

如果请求成功,将使用访问令牌填充令牌变量。 此标记的 AccessToken.Value 属性会公开要包含在 Authorization 请求标头中的文本字符串。

如果请求失败,因为在没有用户交互的情况下无法预配令牌:

  • .NET 7 或更高版本中的 ASP.NET Core:应用使用给定的 AccessTokenResult.InteractionOptions 导航到 AccessTokenResult.InteractiveRequestUrl 以允许刷新访问令牌。
  • .NET 6 或更低版本中的 ASP.NET Core:令牌结果包含重定向 URL。 导航到此 URL 后,用户将进入登录页,并在身份验证成功后返回到当前页面。
@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using {APP NAMESPACE}.Shared
@attribute [Authorize]
@inject HttpClient Http

...

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}

使用 Azure Active Directory B2C 租户

如果应用是在 Azure Active Directory B2C 租户中注册的(如教程:创建 Azure Active Directory B2C 租户中所述),但遵循本文中的指导,则 App ID URI 由 ME-ID 采用不同的方式进行管理。

可选择 ME-ID 组织概述顶部的“管理租户”链接,查看现有租户的租户类型。 查看组织的“租户类型”列值。 本部分适用于遵循本文中的指南但在 Azure Active Directory B2C 租户中注册的应用。

应用 ID URI 的格式为 https://{TENANT}.onmicrosoft.com/{SERVER API APP CLIENT ID OR CUSTOM VALUE},而不是匹配格式 api://{SERVER API APP CLIENT ID OR CUSTOM VALUE}。 此差异会影响 ClientServer 应用配置:

  • 对于服务器 API 应用,在应用设置文件 (appsettings.json) 中设置 Audience 以匹配 Azure 门户提供的无尾部斜杠的应用的受众(应用 ID URI):

    "Audience": "https://{TENANT}.onmicrosoft.com/{SERVER API APP CLIENT ID OR CUSTOM VALUE}"
    

    示例:

    "Audience": "https://contoso.onmicrosoft.com/41451fa7-82d9-4673-8fa5-69eff5a761fd"
    
  • Client 应用的 Program 文件中,将范围的受众(应用 ID URI)设置为匹配服务器 API 应用的受众:

    options.ProviderOptions.DefaultAccessTokenScopes
        .Add("https://{TENANT}.onmicrosoft.com/{SERVER API APP CLIENT ID OR CUSTOM VALUE}/{DEFAULT SCOPE}");
    

    在前面的范围中,应用 ID URI/受众是值的 https://{TENANT}.onmicrosoft.com/{SERVER API APP CLIENT ID OR CUSTOM VALUE} 部分,其中不包括尾部斜杠 (/),并且不包括范围名称 ({DEFAULT SCOPE})。

    示例:

    options.ProviderOptions.DefaultAccessTokenScopes
        .Add("https://contoso.onmicrosoft.com/41451fa7-82d9-4673-8fa5-69eff5a761fd/API.Access");
    

    在前面的范围中,应用 ID URI/受众是值的 https://contoso.onmicrosoft.com/41451fa7-82d9-4673-8fa5-69eff5a761fd 部分,其中不包括尾部斜杠 (/),并且不包括范围名称 (API.Access)。

使用自定义应用 ID URI

如果应用 ID URI 是自定义值,必须手动更新 Client 应用中的默认访问令牌范围 URI,并将受众添加到 Server 应用的 ME-ID 配置。

重要

使用 api://{SERVER API APP CLIENT ID} 的默认应用 ID URI 时,以下配置不是必需的

urn://custom-app-id-uri 的示例应用 ID URI 和 API.Access 的范围名称:

  • Client 应用的 Program 文件中:

    options.ProviderOptions.DefaultAccessTokenScopes.Add(
        "urn://custom-app-id-uri/API.Access");
    
  • Server 应用的 appsettings.json 中,添加 Audience 条目,仅包含应用 ID URI,不包含尾部斜杠:

    "Audience": "urn://custom-app-id-uri"
    

疑难解答

日志记录

若要为 Blazor WebAssembly 身份验证启用调试或跟踪日志记录,请参阅 ASP.NET CoreBlazor 日志记录客户端身份验证日志记录部分,其中项目版本选择器设置为 ASP.NET Core 7.0 或更高版本。

常见错误

  • 应用或 Identity 提供者 (IP) 配置错误

    最常见的错误是因为配置不正确导致的。 下面是几个示例:

    • 根据具体情景的要求,缺少或不正确的颁发机构、实例、租户 ID、租户域、客户端 ID 或重定向 URI 会阻止应用对客户端进行身份验证。
    • 不正确的请求范围会阻止客户端访问服务器 Web API 终结点。
    • 服务器 API 权限不正确或缺失会阻止客户端访问服务器 Web API 终结点。
    • 在不同于 IP 应用注册的重定向 URI 中配置的应用的端口运行应用。 请注意,Microsoft Entra ID 和在 localhost 开发测试地址上运行的应用不需要端口,但应用的端口配置和运行应用的端口必须与非 localhost 地址匹配。

    本文指导的配置部分显示了正确的配置示例。 请仔细查看本文的每个部分,以查找应用和 IP 配置错误。

    如果配置看起来是正确的:

    • 分析应用程序日志。

    • 通过浏览器的开发人员工具,检查客户端应用和 IP 或服务器应用之间的网络流量。 通常,在发出请求后,IP 或服务器应用会向客户端返回一条确切的错误消息或包含线索的消息,其中指出了导致问题的原因。 有关开发人员工具指导,请参阅以下文章:

    • 对于使用 JS ON Web 令牌 (JWT) 的 Blazor 版本,请解码用于对客户端进行身份验证或访问服务器 Web API 的令牌内容,具体取决于问题发生的位置。 有关详细信息,请参阅检查 JSON Web 令牌 (JWT) 的内容

    文档团队会响应文章中的文档反馈和 bug(从“此页面”反馈部分提交问题),但无法提供产品支持。 可以借助多个公共支持论坛来帮助排查应用问题。 建议如下:

    上述论坛并非 Microsoft 所拥有或者不受 Microsoft 控制。

    对于非安全、非敏感且非机密的可重现框架 bug 报告,请向 ASP.NET Core 产品团队提交问题。 请务必先彻底调查问题原因,并确定无法自行解决问题,在公共支持论坛的社区帮助下同样无法解决问题后,再向该产品团队提交问题。 如果应用问题是由简单的配置错误引起或涉及第三方服务,该产品团队无法对此进行故障排除。 如果报告包含敏感或机密内容,或者描述了可能会被攻击者利用的潜在产品安全缺陷,请参阅报告安全问题和 bug(dotnet/aspnetcore GitHub 存储库)

  • ME-ID 的客户端未获得授权

    信息:Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] 授权失败。 不符合以下要求:DenyAnonymousAuthorizationRequirement:要求用户经过身份验证。

    ME-ID 返回的登录回叫错误:

    • 错误:unauthorized_client
    • 说明:AADB2C90058: The provided application is not configured to allow public clients.

    若要解决该错误:

    1. 在 Azure 门户中访问应用的清单
    2. allowPublicClient 属性设置为 nulltrue

Cookie 和站点数据

Cookie 和站点数据在经过应用更新后仍可保持不变,并且会干扰测试和故障排除。 在更改应用代码、更改提供程序的用户帐户或更改提供程序的应用配置时,请清除以下内容:

  • 用户登录 cookie
  • 应用 cookie
  • 缓存和存储的站点数据

防止存留的 cookie 和站点数据干扰测试和故障排除的一种方法是:

  • 配置浏览器
    • 使用浏览器测试是否可以配置为在每次关闭浏览器时删除所有 cookie 和站点数据。
    • 对于应用、测试用户或提供程序配置的任何更改,请确保浏览器是手动关闭的或由 IDE 关闭的。
  • 在 Visual Studio 中使用自定义命令以 InPrivate 或无痕模式打开浏览器:
    • 通过 Visual Studio 的“运行”按钮打开“浏览工具”对话框 。
    • 选择“添加”按钮。
    • 在“程序”字段中提供浏览器的路径。 以下可执行路径是适用于 Windows 10 的典型安装位置。 如果浏览器安装在其他位置,或者未使用 Windows 10,请提供浏览器可执行文件的路径。
      • Microsoft Edge:C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe
      • Google Chrome:C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
      • Mozilla Firefox:C:\Program Files\Mozilla Firefox\firefox.exe
    • 在“参数”字段中,提供浏览器用来在 InPrivate 或无痕模式下执行打开操作的命令行选项。 某些浏览器需要应用的 URL。
      • Microsoft Edge:请使用 -inprivate
      • Google Chrome:使用 --incognito --new-window {URL},其中占位符 {URL} 是要打开的 URL(例如,https://localhost:5001)。
      • Mozilla Firefox:使用 -private -url {URL},其中占位符 {URL} 是要打开的 URL(例如,https://localhost:5001)。
    • 在“友好名称”字段中提供名称。 例如 Firefox Auth Testing
    • 选择“确定”按钮。
    • 若要避免在每次迭代使用应用进行测试时必须选择浏览器配置文件,请使用“设置为默认值”按钮将配置文件设置为默认值。
    • 对于应用、测试用户或提供程序配置的任何更改,请确保浏览器是由 IDE 关闭的。

应用升级

正常运行的应用在开发计算机上升级 .NET Core SDK 或在应用内更改包版本后可能会立即出现故障。 在某些情况下,不同的包可能在执行主要升级时中断应用。 可以按照以下说明来修复其中大部分问题:

  1. 从命令 shell 执行 dotnet nuget locals all --clear 以清空本地系统的 NuGet 包缓存。
  2. 删除项目的 binobj 文件夹。
  3. 还原并重新生成项目。
  4. 在重新部署应用前,在服务器上删除部署文件夹中的所有文件。

注意

不支持使用与应用的目标框架不兼容的包版本。 有关包的信息,请使用 NuGet GalleryFuGet Package Explorer 进行了解。

运行 Server 应用

在对托管的 Blazor WebAssembly 解决方案进行测试和故障排除时,请确保从 Server 项目运行应用。

检查用户

可直接在应用中使用以下 User 组件或将其用作进一步自定义的基础。

User.razor

@page "/user"
@attribute [Authorize]
@using System.Text.Json
@using System.Security.Claims
@inject IAccessTokenProvider AuthorizationService

<h1>@AuthenticatedUser?.Identity?.Name</h1>

<h2>Claims</h2>

@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
{
    <p class="claim">@(claim.Type): @claim.Value</p>
}

<h2>Access token</h2>

<p id="access-token">@AccessToken?.Value</p>

<h2>Access token claims</h2>

@foreach (var claim in GetAccessTokenClaims())
{
    <p>@(claim.Key): @claim.Value.ToString()</p>
}

@if (AccessToken != null)
{
    <h2>Access token expires</h2>

    <p>Current time: <span id="current-time">@DateTimeOffset.Now</span></p>
    <p id="access-token-expires">@AccessToken.Expires</p>

    <h2>Access token granted scopes (as reported by the API)</h2>

    @foreach (var scope in AccessToken.GrantedScopes)
    {
        <p>Scope: @scope</p>
    }
}

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

    public ClaimsPrincipal AuthenticatedUser { get; set; }
    public AccessToken AccessToken { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        var state = await AuthenticationState;
        var accessTokenResult = await AuthorizationService.RequestAccessToken();

        if (!accessTokenResult.TryGetToken(out var token))
        {
            throw new InvalidOperationException(
                "Failed to provision the access token.");
        }

        AccessToken = token;

        AuthenticatedUser = state.User;
    }

    protected IDictionary<string, object> GetAccessTokenClaims()
    {
        if (AccessToken == null)
        {
            return new Dictionary<string, object>();
        }

        // header.payload.signature
        var payload = AccessToken.Value.Split(".")[1];
        var base64Payload = payload.Replace('-', '+').Replace('_', '/')
            .PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');

        return JsonSerializer.Deserialize<IDictionary<string, object>>(
            Convert.FromBase64String(base64Payload));
    }
}

检查 JSON Web 令牌 (JWT) 的内容

若要对 JSON Web 令牌 (JWT) 进行解码,请使用 Microsoft 的 jwt.ms 工具。 UI 中的值永远不会离开浏览器。

编码 JWT 示例(为便于显示,已经缩短):

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1j ... bQdHBHGcQQRbW7Wmo6SWYG4V_bU55Ug_PW4pLPr20tTS8Ct7_uwy9DWrzCMzpD-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-yBMKV2_nXA25Q

工具针对向 Azure AAD B2C 进行身份验证的应用解码的 JWT 示例:

{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
  "exp": 1610059429,
  "nbf": 1610055829,
  "ver": "1.0",
  "iss": "https://mysiteb2c.b2clogin.com/5cc15ea8-a296-4aa3-97e4-226dcc9ad298/v2.0/",
  "sub": "5ee963fb-24d6-4d72-a1b6-889c6e2c7438",
  "aud": "70bde375-fce3-4b82-984a-b247d823a03f",
  "nonce": "b2641f54-8dc4-42ca-97ea-7f12ff4af871",
  "iat": 1610055829,
  "auth_time": 1610055822,
  "idp": "idp.com",
  "tfp": "B2C_1_signupsignin"
}.[Signature]

其他资源