使用 Azure Active Directory B2C 保护 ASP.NET Core Blazor WebAssembly 独立应用

Note

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

Warning

不再支持此版本的 ASP.NET Core。 有关详细信息,请参阅 .NET 和 .NET 核心支持策略。 有关当前发布版本,请参阅此文章的 .NET 10 版本

Note

自 2025 年 5 月 1 日起,Azure Active Directory B2C 不再作为新客户的服务提供。 有关详细信息,请参阅 Azure AD B2C:常见问题解答(常见问题解答)

本文介绍如何创建一个使用 Blazor WebAssembly 进行身份验证的 独立应用程序

有关阅读本文后的其他安全方案覆盖范围,请参阅 ASP.NET Core Blazor WebAssembly 其他安全方案

Walkthrough

操作指南的小节解释了如何:

  • 在 Azure 中创建租户
  • 在 Azure 中注册应用
  • 创建 Blazor 应用
  • 运行应用

在 Azure 中创建租户

按照 Tutorial:创建 Azure Active Directory B2C 租户中的指南创建 AAD B2C 租户。

在继续阅读本文的指南之前,请确保已为 AAD B2C 租户选择正确的目录

在 Azure 中注册应用

注册 AAD B2C 应用:

  1. 在 Azure 门户中导航到 Azure AD B2C。 在边栏中选择 应用注册。 选择“新建注册”按钮。
  2. 提供应用的名称(例如 Blazor 独立 AAD B2C)。
  3. 对于 支持的帐户类型,请选择多租户选项:来自任何组织目录或任何身份提供者的帐户。 用于通过 Azure AD B2C 对用户进行身份验证。
  4. 将“重定向 URI”下拉列表设置为“单页应用程序(SPA)”,并提供以下重定向 URI:。 如果知道Azure默认主机的生产重定向 URI(例如,azurewebsites.net)或自定义域主机(例如,contoso.com),则还可以在提供 localhost 重定向 URI 的同时添加生产重定向 URI。 请确保在添加的任何生产重定向 URI 中包含非 :443 端口的端口号。
  5. 如果使用未经验证的发布者域,请确认已选中“权限”>“授予对 openid 和 offline_access 权限的管理员同意”复选框。 如果验证了发布者域,则不会出现此复选框。
  6. 选择“注册”。

Note

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

记录以下信息:

  • 应用程序(客户端)ID(例如 00001111-aaaa-2222-bbbb-3333cccc4444)。
  • AAD B2C 实例(例如,https://contoso.b2clogin.com/,包括尾部斜杠):实例是 Azure B2C 应用注册的协议和主机,可以通过在 Azure 门户中的 应用注册 页面中打开 终结点 窗口找到。
  • AAD B2C 主/发布者/租户域名(例如,contoso.onmicrosoft.com):该域名可在 Azure 门户的 品牌化 边栏中以 发布者域 的形式用于已注册的应用程序。

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

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

Home>Azure AD B2C>User flow

创建注册和登录用户流

至少选择“应用程序声明”>“显示名称”用户属性以填充 context.User.Identity?.Name 组件 (/)中的 context.User.Identity.NameLoginDisplayShared/LoginDisplay.razor

记录为应用创建的注册和登录用户流名称(例如 B2C_1_signupsignin)。

创建 Blazor 应用

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

dotnet new blazorwasm -au IndividualB2C --aad-b2c-instance "{AAD B2C INSTANCE}" --client-id "{CLIENT ID}" --domain "{TENANT DOMAIN}" -o {PROJECT NAME} -ssp "{SIGN UP OR SIGN IN POLICY}"
Placeholder Azure门户名称 Example
{AAD B2C INSTANCE} Instance https://contoso.b2clogin.com/(包括尾部反斜杠)
{PROJECT NAME} BlazorSample
{CLIENT ID} 应用程序(客户端)ID 00001111-aaaa-2222-bbbb-3333cccc4444
{SIGN UP OR SIGN IN POLICY} 注册/登录用户流 B2C_1_signupsignin1
{TENANT DOMAIN} 主域/发布者域/租户域 contoso.onmicrosoft.com

使用 -o|--output 选项指定的输出位置将创建一个项目文件夹(如果该文件夹不存在)并成为项目名称的一部分。

MsalProviderOptionsopenidoffline_access 添加一对 DefaultAccessTokenScopes

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

创建应用后,应该能够:

运行应用

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

  • Visual Studio
    • 选择“运行”按钮。
    • 从菜单中选择调试>开始调试
    • F5
  • .NET CLI 命令行界面:从应用的文件夹中执行 dotnet watch (或 dotnet run) 命令。

远程身份验证路径

使用应用的 Program 文件中 RemoteAuthenticationOptions<TRemoteAuthenticationProviderOptions>.AuthenticationPaths 属性上的 RemoteAuthenticationApplicationPathsOptions 来定制远程身份验证路径。 有关框架的默认路径值,请参阅 dotnet/aspnetcore 引用源

Note

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

如果应用 自定义远程身份验证路径,请采用以下任一方法:

应用的组成部分

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

身份验证包

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

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

Note

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

Microsoft.Authentication.WebAssembly.Msal可传递地将 Microsoft.AspNetCore.Components.WebAssembly.Authentication添加到应用。

身份验证服务支持

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

Program 文件中:

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
});

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

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

{
  "AzureAdB2C": {
    "Authority": "{AAD B2C INSTANCE}{TENANT DOMAIN}/{SIGN UP OR SIGN IN POLICY}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": false
  }
}

在前一配置中,{AAD B2C INSTANCE} 包括尾部反斜杠。

Example:

{
  "AzureAdB2C": {
    "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_signupsignin1",
    "ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
    "ValidateAuthority": false
  }
}

访问令牌作用域

Blazor WebAssembly 模板不会自动将应用配置为请求安全 API 的访问令牌。 要将访问令牌作为登录流程的一部分进行预配,请将作用域添加到 MsalProviderOptions 的默认访问令牌作用域中:

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

使用 AdditionalScopesToConsent 指定其他作用域:

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

Note

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

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

登录模式

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

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

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

导入文件

命名空间 Microsoft.AspNetCore.Components.Authorization 通过导入文件在整个应用中可用(_Imports.razor):

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

索引页

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

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

应用组件

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

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

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

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

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

    Note

    指向.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搜索工具查找组件。

LoginDisplay 组件

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

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

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

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

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

    Note

    指向.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; }
}

Note

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

自定义策略

Microsoft 身份验证库 (Microsoft.Authentication.WebAssembly.MsalNuGet 包) 不支持 AAD B2C 自定义策略

Troubleshoot

Logging

若要为 Blazor WebAssembly 身份验证启用调试或跟踪日志记录,请参阅 Blazor 中的 客户端身份验证日志记录 部分,并确保文章版本选择器设置为 .NET 7 或更高版本的 ASP.NET Core。

常见错误

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

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

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

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

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

    • 分析应用程序日志。

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

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

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

    上述论坛不是由 Microsoft 拥有或控制的。

    对于非安全、非敏感和非机密的可重现框架 bug 报告,请向 ASP.NET Core 产品团队提交问题。 在彻底调查问题原因并尝试自行解决未果,以及在公共支持论坛的社区帮助下仍无法解决问题后,才向产品团队提出问题。 如果应用问题是由简单的配置错误引起或涉及第三方服务,该产品团队无法对此进行故障排除。 如果报告本质上是敏感或机密的,或者描述了网络攻击者可能利用的产品中的潜在安全漏洞,请参阅报告安全问题和漏洞(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 门户中,访问 app 的清单
    2. allowPublicClient 属性设置为 nulltrue

Cookies 和站点数据

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

  • 用户登录的Cookies
  • 应用程序 Cookie
  • 缓存和存储的站点数据

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

  • 配置浏览器
    • 使用浏览器测试是否可以配置为在每次关闭浏览器时删除所有 cookie 和站点数据。
    • 对于应用、测试用户或提供程序配置的任何更改,请确保浏览器是手动关闭的或由 IDE 关闭的。
  • 使用自定义命令在 Visual Studio 的 InPrivate 或 Incognito 模式下打开浏览器:
    • 从 Visual Studio 的 Run 按钮打开 Browse With 对话框。
    • 选择“添加”按钮。
    • 在“程序”字段中提供浏览器的路径。 以下可执行路径是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 SDK 或更改应用中的包版本后,正常运行的应用可能会立即失败。 在某些情况下,不一致的包可能会在执行主要升级时导致应用程序崩溃。 可以按照以下说明来修复其中大部分问题:

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

Note

不支持使用与应用的目标框架不兼容的包版本。 有关软件包的信息,请访问 NuGet Gallery

运行 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 Token (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/11112222-bbbb-3333-cccc-4444dddd5555/v2.0/",
  "sub": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
  "aud": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "nonce": "bbbb0000-cccc-1111-dddd-2222eeee3333",
  "iat": 1610055829,
  "auth_time": 1610055822,
  "idp": "idp.com",
  "tfp": "B2C_1_signupsignin"
}.[Signature]

其他资源