使用身份验证库保护 ASP.NET Core Blazor WebAssembly 独立应用

注意

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

重要

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

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

本文介绍如何使用 Blazor WebAssembly 身份验证库保护 ASP.NET Core Blazor WebAssembly 独立应用。

Blazor WebAssembly 身份验证库 (Authentication.js) 仅支持通过 Microsoft 身份验证库(MSAL、msal.js的 Proof Key for Code Exchange (PKCE) 授权代码流。 若要实现其他授予流,请访问 MSAL 指南以直接实现 MSAL,但我们不支持或建议对 Blazor 应用使用 PKCE 以外的授权流。

本主题中的指南不适用于 Microsoft Entra (ME-ID) 和 Azure Active Directory B2C (AAD B2C)。 如需相应指南,请参阅使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用使用 Azure Active Directory B2C 保护 ASP.NET Core Blazor WebAssembly 独立应用

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

演练

演练的子节介绍了如何:

  • 注册应用程序
  • 创建 Blazor 应用
  • 运行应用

注册应用程序

按照 IP 维护者提供的指南,向 OpenID Connect (OIDC)Identity 提供程序 (IP) 注册应用。

记录以下信息:

  • 颁发机构(例如 https://accounts.google.com/)。
  • 应用程序(客户端)ID(例如 2...7-e...q.apps.googleusercontent.com)。
  • 其他 IP 配置(请参阅 IP 文档)。

注意

IP 必须使用 OIDC。 例如,Facebook 的 IP 不是符合 OIDC 的提供程序,因此本主题中的指南不适用于 Facebook IP。 有关详细信息,请参阅保护 ASP.NET Core Blazor WebAssembly

创建 Blazor 应用

若要创建使用 Microsoft.AspNetCore.Components.WebAssembly.Authentication 库的独立 Blazor WebAssembly 应用,请按照适用于所选工具的指南操作。 如果添加对身份验证的支持,请参阅本文中的应用组成部分部分,了解有关设置和配置应用的指南。

新建具有身份验证机制的 Blazor WebAssembly 项目:

选择“应用 Blazor WebAssembly”模板后,将“身份验证类型”设置为“个人帐户”。

选择“应用 Blazor WebAssembly”模板后,将“身份验证类型”设置为“个人帐户”。 确认未选中“ASP.NET Core 托管”复选框。

“个人帐户”选项使用 ASP.NET Core 的 Identity 系统。 此选择将添加身份验证支持,并且最终不会将用户存储在数据库中。 本文的以下部分提供了更多详细信息。

配置应用

按照 IP 指南配置应用。 该应用至少需要应用 wwwroot/appsettings.json 文件中的 Local:AuthorityLocal:ClientId 配置设置:

{
  "Local": {
    "Authority": "{AUTHORITY}",
    "ClientId": "{CLIENT ID}"
  }
}

在端口 5001 处的 localhost 地址上运行的应用的 Google OAuth 2.0 OIDC 示例:

{
  "Local": {
    "Authority": "https://accounts.google.com/",
    "ClientId": "2...7-e...q.apps.googleusercontent.com",
    "PostLogoutRedirectUri": "https://localhost:5001/authentication/logout-callback",
    "RedirectUri": "https://localhost:5001/authentication/login-callback",
    "ResponseType": "id_token"
  }
}

重定向 URI (https://localhost:5001/authentication/login-callback) 在 Google API 控制台的“凭据”>{NAME}>“授权重定向 URI”中注册,其中 {NAME} 是 Google API 控制台的“OAuth 2.0 客户端 ID”应用列表中的应用客户端名称。

注意

根据 OAuth 2.0 规范,某些 OIDC IP 无需为 localhost 重定向 URI 提供端口号。 某些 IP 允许环回地址的重定向 URI 省略端口。 其他 IP 则允许对端口号使用通配符(例如 *)。 有关其他信息,请参见 IP 文档。

运行应用

若要运行应用,请使用以下方法之一:

  • Visual Studio
    • 选择“运行”按钮。
    • 从菜单栏中,依次使用“调试”>“开始调试” 。
    • 按 F5
  • .NET CLI 命令 shell:从应用的文件夹中执行 dotnet run 命令。

应用的组成部分

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

身份验证包

创建应用以使用个人用户帐户时,该应用会自动接收 Microsoft.AspNetCore.Components.WebAssembly.Authentication 包的包引用。 此包提供了一组基元,可帮助应用验证用户身份并获取令牌以调用受保护的 API。

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

注意

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

身份验证服务支持

使用由 Microsoft.AspNetCore.Components.WebAssembly.Authentication 包提供的 AddOidcAuthentication 扩展方法,在服务容器中注册使用 OpenID Connect (OIDC) 对用户进行身份验证的支持。

AddOidcAuthentication 方法接受回叫,以配置使用 OIDC 验证应用所需的参数。 可以从 OIDC 兼容的 IP 中获取配置应用所需的值。 注册应用时获取值,通常在其在线门户中执行此操作。

对于新应用,请为以下配置中的 {AUTHORITY}{CLIENT ID} 占位符提供值。 提供与应用的 IP 一起使用所需的其他配置值。 例如用于 Google 的相关值,它需要 PostLogoutRedirectUriRedirectUriResponseType。 如果向应用添加身份验证,请使用占位符值和其他配置值将以下代码和配置手动添加到应用中。

Program 文件中:

builder.Services.AddOidcAuthentication(options =>
{
    builder.Configuration.Bind("Local", options.ProviderOptions);
});

wwwroot/appsettings.json 配置

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

{
  "Local": {
    "Authority": "{AUTHORITY}",
    "ClientId": "{CLIENT ID}"
  }
}

访问令牌作用域

Blazor WebAssembly 模板自动为 openidprofile 配置默认作用域。

Blazor WebAssembly 模板不会自动将应用配置为请求安全 API 的访问令牌。 若要将访问令牌预配为登录流的一部分,请将范围添加到 OidcProviderOptions 的默认令牌范围中。 如果向应用添加身份验证,请手动添加以下代码并配置范围 URI。

Program 文件中:

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

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

导入文件

整个应用通过 _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

索引页

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

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

应用组件

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

  • AuthorizeRouteView 组件确保当前用户有权访问给定页面或以其他方式呈现 RedirectToLogin 组件。
  • RedirectToLogin 组件管理将未经授权的用户重定向到登录页。

由于不同版本的 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 组件

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 组件

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)

身份验证组件

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

疑难解答

日志记录

若要为 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]

其他资源