使用 Microsoft Entra ID 保护 ASP.NET Core Blazor Web App

本文介绍如何使用示例应用保护Blazor Web App具有 Microsoft identity 平台/Microsoft Identity Web 包Microsoft Entra ID 的安全。

涵盖以下规范:

  • 使用Blazor Web App具有全局交互性的自动呈现模式(InteractiveAuto)。
  • 服务器项目调用 AddAuthenticationStateSerialization 以添加用于 PersistentComponentState 将身份验证状态流向客户端的服务器端身份验证状态提供程序。 客户端调用 AddAuthenticationStateDeserialization 反序列化并使用服务器传递的身份验证状态。 身份验证状态在 WebAssembly 应用程序的生存期内是固定的。
  • 应用根据 Identity Microsoft Web 包使用 Microsoft Entra ID
  • 自动非交互式令牌刷新由框架管理。
  • 应用使用服务器端和客户端服务抽象来显示生成的天气数据:
    • 在服务器上呈现 Weather 组件以显示天气数据时,组件使用 ServerWeatherForecaster 服务器上的组件直接获取天气数据(而不是通过 Web API 调用)。
    • Weather在客户端上呈现组件时,该组件使用ClientWeatherForecaster服务实现,该实现使用预配置HttpClient(在客户端项目的Program文件中)对服务器项目的最小 API(/weather-forecast)进行 Web API 调用,以获取天气数据。 最小 API 终结点从 ServerWeatherForecaster 类获取天气数据,并将其返回到客户端供组件呈现。

示例应用

此示例应用包含两个项目:

  • BlazorWebAppEntra:Blazor Web App 的服务器端项目,包含用于天气数据的示例最小 API 终结点。
  • BlazorWebAppEntra.Client:Blazor Web App 的客户端项目。

使用以下链接通过存储库根目录中的最新版本文件夹访问示例应用。 项目位于 BlazorWebAppEntra .NET 9 或更高版本的文件夹中。

查看或下载示例代码如何下载

服务器端 Blazor Web App 项目 (BlazorWebAppEntra)

BlazorWebAppEntra 项目是 Blazor Web App 的服务器端项目。

BlazorWebAppEntra.http 文件可用于测试天气数据请求。 请注意,BlazorWebAppEntra 项目必须正在运行以测试终结点,并且终结点已硬编码到文件中。 有关详细信息,请参阅在 Visual Studio 2022 中使用 .http 文件

客户端 Blazor Web App 项目 (BlazorWebAppEntra.Client)

BlazorWebAppEntra.Client 项目是 Blazor Web App 的客户端项目。

如果用户需要在客户端呈现期间登录或注销,则会启动完整页面重新加载。

配置

本部分介绍如何配置示例应用。

AddMicrosoftIdentityWebApp 来自 Microsoft Identity WebMicrosoft.Identity.Web NuGet 包API 文档)由 AzureAd 服务器项目 appsettings.json 文件部分配置。

在 Entra 或 Azure 门户 中的应用注册中,将 Web 平台配置与重定向 URI https://localhost/signin-oidc 配合使用(不需要端口)。 确认选择隐式授予和混合流下的 ID 令牌和访问令牌。 OpenID Connect 处理程序使用授权终结点返回的代码自动请求相应的令牌。

建立客户端密码

使用机密管理器工具将服务器应用的客户端密码存储在配置密钥AzureAd:ClientSecret下。

在 Entra 或 Azure 门户(管理>证书和机密>新客户端机密)中的应用 Entra ID 注册中创建客户端密码。 在以下指南中使用新机密的值

在服务器项目的目录中执行以下命令,例如 Visual Studio 中的开发人员 PowerShell 命令 shell。 占 {SECRET} 位符是从应用的注册中获取的客户端密码:

dotnet user-secrets set "AzureAd:ClientSecret" "{SECRET}"

如果使用 Visual Studio,可以通过右键单击解决方案资源管理器中的服务器项目并选择“管理用户机密”来确认是否已设置机密

配置应用

在服务器项目的应用设置文件(appsettings.json)中,提供应用的 AzureAd 分区配置。 从 Entra 或 Azure 门户 中的应用注册中获取应用程序(客户端)ID、租户(发布者)域和目录(租户)ID:

"AzureAd": {
  "CallbackPath": "/signin-oidc",
  "ClientId": "{CLIENT ID}",
  "Domain": "{DOMAIN}",
  "Instance": "https://login.microsoftonline.com/",
  "ResponseType": "code",
  "TenantId": "{TENANT ID}"
},

前面的示例中的占位符:

  • {CLIENT ID}:应用程序(客户端)ID。
  • {DOMAIN}:租户(发布者)域。
  • {TENANT ID}:目录(租户)ID。

示例:

"AzureAd": {
  "CallbackPath": "/signin-oidc",
  "ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "Domain": "contoso.onmicrosoft.com",
  "Instance": "https://login.microsoftonline.com/",
  "ResponseType": "code",
  "TenantId": "aaaabbbb-0000-cccc-1111-dddd2222eeee"
},

回调路径 (CallbackPath) 必须与在 Entra 或 Azure 门户 中注册应用程序时配置的重定向 URI(登录回调路径)匹配。 路径在 应用的注册的“身份验证 ”边栏选项卡中配置。 默认值 CallbackPath/signin-oidc 已注册的重定向 URI https://localhost/signin-oidc (不需要端口)。

警告

不要在客户端代码中存储应用机密、连接字符串、凭据、密码、个人标识号(PIN)、专用 C#/.NET 代码或私钥/令牌,这始终不安全 在测试/暂存和生产环境中,服务器端 Blazor 代码和 Web API 应使用安全身份验证流,以避免在项目代码或配置文件中维护凭据。 在本地开发测试之外,我们建议避免使用环境变量来存储敏感数据,因为环境变量不是最安全的方法。 对于本地开发测试, 建议使用机密管理器工具 来保护敏感数据。 有关详细信息,请参阅 安全维护敏感数据和凭据

在注销时重定向到 home 页面

当用户在应用中导航时,LogInOrOut 组件 (Layout/LogInOrOut.razor) 会将返回 URL (ReturnUrl) 的隐藏字段设置为当前 URL (currentURL) 的值。 当用户从应用退出登录时,identity 提供程序会使其返回到他们退出登录的页。

如果用户从安全页面注销,则他们在注销后会返回到同一安全页面,但仅通过身份验证过程发送回。 当用户需要频繁切换帐户时,此行为是正常的。 但是,备用应用规范可能会要求用户在注销后返回到应用 home 的页面或其他页面。以下示例演示如何将应用的 home 页设置为注销操作的返回 URL。

以下示例演示了 LogInOrOut 组件的重要更改。 无需为页面设置home/提供隐藏字段ReturnUrl,因为这是默认路径。 不再实现 IDisposable。 不再注入 NavigationManager。 整个 @code 块已被删除。

Layout/LogInOrOut.razor:

@using Microsoft.AspNetCore.Authorization

<div class="nav-item px-3">
    <AuthorizeView>
        <Authorized>
            <form action="authentication/logout" method="post">
                <AntiforgeryToken />
                <button type="submit" class="nav-link">
                    <span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true">
                    </span> Logout @context.User.Identity?.Name
                </button>
            </form>
        </Authorized>
        <NotAuthorized>
            <a class="nav-link" href="authentication/login">
                <span class="bi bi-person-badge-nav-menu" aria-hidden="true"></span> 
                Login
            </a>
        </NotAuthorized>
    </AuthorizeView>
</div>

疑难解答

日志记录

服务器应用是一个标准 ASP.NET Core 应用。 请参阅 ASP.NET Core 记录指南,以便在服务器应用中启用较低的记录级别。

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

    文档团队会响应文章中的文档反馈和 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 进行了解。

运行服务器应用

在对 Blazor Web App 进行测试和故障排除时,请确保在服务器项目中运行该应用。

检查用户

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

UserClaims.razor:

@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

<PageTitle>User Claims</PageTitle>

<h1>User Claims</h1>

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

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

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

    protected override async Task OnInitializedAsync()
    {
        if (AuthState == null)
        {
            return;
        }

        var authState = await AuthState;
        claims = authState.User.Claims;
    }
}

其他资源