使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用

注意

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

警告

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

重要

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

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

本文介绍如何创建使用 Microsoft Entra ID (ME-ID) 进行身份验证的独立 Blazor WebAssembly 应用

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

演练

演练的子节介绍了如何:

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

在 Azure 中创建租户

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

在 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. 如果使用未经验证的发布者域,请清除“权限”>“授予对 openid 和 offline_access 权限的管理员同意”复选框。 如果验证了发布者域,则不会出现此复选框。
  6. 选择“注册”。

注意

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

记录以下信息:

  • 应用程序(客户端)ID(例如 00001111-aaaa-2222-bbbb-3333cccc4444
  • 目录(租户)ID(例如 aaaabbbb-0000-cccc-1111-dddd2222eeee

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

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

创建 Blazor 应用

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

dotnet new blazorwasm -au SingleOrg --client-id "{CLIENT ID}" -o {PROJECT NAME} --tenant-id "{TENANT ID}"
占位符 Azure 门户中的名称 示例
{PROJECT NAME} " BlazorSample
{CLIENT ID} 应用程序(客户端)ID 00001111-aaaa-2222-bbbb-3333cccc4444
{TENANT ID} 目录(租户)ID aaaabbbb-0000-cccc-1111-dddd2222eeee

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

添加 MsalProviderOptions 以通过 DefaultAccessTokenScopes 获取 User.Read 权限:

builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.DefaultAccessTokenScopes
        .Add("https://graph.microsoft.com/User.Read");
});

运行应用

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

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

应用的组成部分

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

身份验证包

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

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

注意

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

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

身份验证服务支持

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

Program 文件中:

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

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

wwwroot/appsettings.json 配置

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

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

示例:

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/e86c78e2-...-918e0565a45e",
    "ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
    "ValidateAuthority": true
  }
}

访问令牌作用域

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

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 结合使用

有关详细信息,请参阅以下资源:

登录模式

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

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

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

导入文件

整个应用通过 _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.Authentication.WebAssembly.Msal/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 或服务器应用会向客户端返回一条确切的错误消息或包含线索的消息,其中指出了导致问题的原因。 有关开发人员工具指导,请参阅以下文章:

    • 对于使用 JSON 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/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]

其他资源