使用 OpenID Connect (OIDC) 保护 ASP.NET Core Blazor Web App

注意

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

重要

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

有关当前版本,请参阅 本文的 .NET 9 版本

本文介绍如何使用 GitHub 存储库中的示例应用(.NET 8 或更高版本),通过 Blazor Web App 来保护 dotnet/blazor-samples)。

对于Microsoft Entra ID 或 Azure AD B2C,可以从 AddMicrosoftIdentityWebApp(Identity、API 文档)使用Microsoft.Identity.Web,该文档会添加 OIDC 和身份验证处理程序以及相应的默认值。 本文中的示例应用和指南不使用 Microsoft Identity Web。 本指南演示如何为任何 OIDC 提供程序 手动 配置 OIDC 处理程序。 有关实现 Microsoft Identity Web 的详细信息,请参阅使用 Microsoft Entra ID 保护 ASP.NET 核心Blazor Web App

本文的此版本介绍如何在应用中实现 OIDC,而无需采用 后端为前端(BFF)模式,该应用采用全局交互式自动呈现(服务器和 .Client 项目)。 BFF 模式是一种用于向外部服务发出经过身份验证请求的设计模式。 如果应用的规范要求采用 BFF 模式 ,请将项目版本选择器更改为 BFF 模式。

采用以下规范:

  • Blazor Web App使用具有全局交互性的自动呈现模式
  • 服务器和客户端应用使用自定义身份验证状态提供程序服务来捕获用户的身份验证状态并在服务器和客户端之间传递它。
  • 此应用是任何 OIDC 身份验证流的起点。 OIDC 在应用中手动配置,不依赖于 Microsoft Entra IDMicrosoft Identity Web 包,示例应用也不需要 Microsoft Azure 托管。 不过,示例应用可以与 Entra、Microsoft Identity Web 一起使用,并托管在 Azure 中。
  • 自动非交互式令牌刷新。
  • 单独的 Web API 项目演示天气数据的安全 Web API 调用。

有关使用适用于 .NET的 Microsoft 身份验证库、Microsoft webMicrosoft Entra ID的替代体验,请参阅 使用 Microsoft Entra ID保护 ASP.NET Core

示例解决方案

示例应用包括以下项目:

  • BlazorWebAppOidc:Blazor Web App 的服务器端项目,其中包含一个示例的 最小 API 终结点用于天气数据。
  • BlazorWebAppOidc.Client:Blazor Web App 的客户端项目。
  • MinimalApiJwt:后端 Web API,包含适用于天气数据的 Minimal API 端点。

使用以下链接通过示例存储库中的 Blazor 最新版本文件夹访问示例。 此示例位于 BlazorWebAppOidc .NET 8 或更高版本的文件夹中。

Aspire/Aspire.AppHost 项目启动解决方案。

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

示例解决方案功能:

  • 借助自定义 cookie 刷新器 (CookieOidcRefresher.cs) 进行的自动非交互式令牌刷新。

  • 天气数据由项目文件 () 中的/weather-forecast最小 API 终结点 (ProgramProgram.csMinimalApiJwt 处理。 终结点要求通过调用 RequireAuthorization 进行授权。 对于添加到项目的任何控制器,请将 [Authorize] 属性 添加到控制器或作。 有关通过 授权策略 要求跨应用授权以及选择在公共终结点子集退出授权的详细信息,请参阅 Razor Pages OIDC 指南

  • 应用安全地为天气数据调用 Web API:

    • ** 在服务器上呈现 Weather 组件时,该组件使用服务器上的 ServerWeatherForecasterMinimalApiJwt 项目的 Web API 获取天气数据,采用一个附加了从 DelegatingHandler 获取的访问令牌的 TokenHandlerHttpContext)将其附加到请求中。
    • 当组件在客户端呈现时,该组件使用 ClientWeatherForecaster 服务实现,该实现利用预配置的 HttpClient(位于客户端项目的 Program 文件中),从服务器项目调用 ServerWeatherForecaster 的 Web API。
  • PersistingAuthenticationStateProvider 类 (PersistingAuthenticationStateProvider.cs) 是服务器端 AuthenticationStateProvider,它使用 PersistentComponentState 将身份验证状态传输到客户端,然后该状态在 WebAssembly 应用程序的生存期内得到修复。

有关使用服务抽象的 Blazor Web App(Web) API 调用的详细信息,请参阅 从 ASP.NET Core Blazor 应用调用 Web API

Microsoft Entra ID 应用注册

我们建议对应用和 Web API 使用单独的注册,即使应用和 Web API 位于同一解决方案中也是如此。 以下指南适用于 BlazorWebAppOidc 示例解决方案的应用和 MinimalApiJwt Web API,但相同的指南通常适用于应用和 Web API 的任何基于 Entra 的注册。

首先注册 Web API(MinimalApiJwt),以便在注册应用时授予对 Web API 的访问权限。 Web API 的租户 ID 和客户端 ID 用于在其 Program 文件中配置 Web API。 注册 Web API 后,在应用注册>中公开 Web API公开具有作用域名称的 Weather.GetAPI。 记录应用 ID URI,以便在应用的配置中使用。

接下来,将应用(BlazorWebAppOidc/BlazorWebApOidc.Client)注册到 Web 平台配置,并设置重定向 URI 为 ,无需端口。 应用的租户 ID 和客户端 ID 以及 Web API 的基址、应用 ID URI 和天气范围名称用于在其 Program 文件中配置应用。 授予 API 权限,以在 应用注册>API 权限中访问 Web API。 如果应用的安全规范要求它,可以授予组织访问 Web API 的管理员同意。 授权用户和组被分配到应用注册>企业应用程序中的应用注册。

在 Entra 或 Azure 门户的 隐式授权和混合流 应用注册配置中,不要选中授权终结点返回 访问令牌ID 令牌的复选框。 OpenID Connect 处理程序使用授权终结点返回的代码自动请求相应的令牌。

在 Entra 或 Azure 门户的应用注册中创建客户端密码(管理>证书和机密>新客户端密码)。 请保留客户端机密,以在下一部分中使用。

本文后面提供了有关特定设置的其他 Entra 配置指南。

建立客户端密码

本部分仅适用于Blazor Web App服务器项目(BlazorWebAppOidc项目)。

警告

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

对于本地开发测试,请使用 机密管理器工具 将服务器项目的客户端机密存储在 Blazor 配置密钥 Authentication:Schemes:MicrosoftOidc:ClientSecret下。

尚未为机密管理器工具初始化Blazor服务器项目。 使用命令 shell(如 Visual Studio 中的开发人员 PowerShell 命令 shell)执行以下命令。 在执行命令之前,请将包含 cd 该命令的目录更改为服务器项目的目录。 该命令将建立用户机密标识符(<UserSecretsId> 在服务器应用的项目文件中):

dotnet user-secrets init

执行以下命令以设置客户端密码。 {SECRET} 占位符是应用注册时获得的客户端密钥。

dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SECRET}"

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

MinimalApiJwt 项目

MinimalApiJwt 项目是用于多个前端项目的后端 Web API。 该项目为天气数据配置 最小 API 终结点。

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

该项目包括用于在开发环境中生成 OpenAPI 文档Swagger UI 的包和配置。 有关详细信息,请参阅 使用生成的 OpenAPI 文档

该项目为天气数据创建 最小 API 终结点:

app.MapGet("/weather-forecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
}).RequireAuthorization();

在项目的 JwtBearerOptions 文件中,在 AddJwtBearer 调用的 Program 中配置项目。

Authority 设置用于进行 OIDC 调用的权限。 建议对 MinimalApiJwt 项目使用单独的应用注册。 颁发机构与标识提供者返回的 JWT 的发行者(iss)匹配。

jwtOptions.Authority = "{AUTHORITY}";

授权机构的格式取决于正在使用的租户类型。 以下 Microsoft Entra ID 的示例使用租户 ID aaaabbbb-0000-cccc-1111-dddd2222eeee

ME-ID 租户权限示例:

jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";

AAD B2C 租户权限示例:

jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/";

对任何收到的 OIDC 令牌设置Audience以确定其受众。

jwtOptions.Audience = "{APP ID URI}";

注意

使用 Microsoft Entra ID 时,将值与在 Entra 或 Azure 门户中,添加在“公开 API”下作用域时配置的Weather.Get的路径进行匹配。 不要在值中包含范围名称“Weather.Get”“。

受众的格式取决于所用租户的类型。 以下示例对于 Microsoft Entra ID 使用租户 ID contoso 和客户端 ID 11112222-bbbb-3333-cccc-4444dddd5555

ME-ID 租户应用 ID URI 示例:

jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";

AAD B2C 租户应用 ID URI 示例:

jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";

Blazor Web App 服务器项目 (BlazorWebAppOidc

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

DelegatingHandler (TokenHandler) 负责管理将用户的访问令牌附加到传出请求中。 令牌处理程序仅在静态服务器端呈现(静态 SSR)期间执行,因此在此方案中使用 HttpContext 是安全的。 有关详细信息,请参阅 ASP.NET Core 应用中的 IHttpContextAccessor/HttpContextBlazorASP.NET Core 服务器端以及其他安全方案Blazor Web App

TokenHandler.cs:

public class TokenHandler(IHttpContextAccessor httpContextAccessor) : 
    DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (httpContextAccessor.HttpContext is null)
        {
            throw new Exception("HttpContext not available");
        }

        var accessToken = await httpContextAccessor.HttpContext
            .GetTokenAsync("access_token");

        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);

        return await base.SendAsync(request, cancellationToken);
    }
}

在项目的 Program 文件中,将令牌处理程序 (TokenHandler) 注册为服务,并指定为消息处理程序 (AddHttpMessageHandler),以使用命名的 HTTP 客户端 MinimalApiJwt ("") 发出安全请求至后端 ExternalApi Web API。

builder.Services.AddScoped<TokenHandler>();

builder.Services.AddHttpClient("ExternalApi",
      client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiUri"] ?? 
          throw new Exception("Missing base address!")))
      .AddHttpMessageHandler<TokenHandler>();

在项目的 appsettings.json 文件中,配置外部 API URI:

"ExternalApiUri": "{BASE ADDRESS}"

示例:

"ExternalApiUri": "https://localhost:7277"

调用 OpenIdConnectOptions 时,可以在项目的 Program 文件中找到以下 AddOpenIdConnect 配置:

PushedAuthorizationBehavior:控制 推送的授权请求(PAR)支持。 默认情况下,如果标识提供者的发现文档(通常位于.well-known/openid-configuration)宣告支持 PAR,则此设置将使用 PAR。 如果要要求对应用提供 PAR 支持,可以分配值 PushedAuthorizationBehavior.Require。 Microsoft Entra 不支持 PAR,并且没有计划让 Entra 将来支持它。

oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.UseIfAvailable;

SignInScheme:设置与身份验证成功后负责保留用户标识的中间件对应的身份验证方案。 OIDC 处理程序需要使用能够跨请求保留用户凭据的登录方案。 以下行仅用于演示目的。 如果省略,则 DefaultSignInScheme 用作回退值。

oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;

openidprofile 的范围 (Scope)(可选):默认情况下也会配置 openidprofile 范围,因为 OIDC 处理程序需要它们才能运行,但可能需要重新添加这些范围(如果 Authentication:Schemes:MicrosoftOidc:Scope 配置中包含范围)。 有关常规配置指南,请参阅 ASP.NET CoreASP.NET Core Blazor 配置中的配置

oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);

SaveTokens:定义在授权成功后,是否应在 AuthenticationProperties 中存储访问令牌和刷新令牌。 此属性设置为 true,以便存储非交互式令牌刷新的刷新令牌。

oidcOptions.SaveTokens = true;

脱机访问范围 (Scope):刷新令牌需要 offline_access 范围。

oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);

AuthorityClientId:设置 OIDC 调用的颁发机构和客户端 ID。

oidcOptions.Authority = "{AUTHORITY}";
oidcOptions.ClientId = "{CLIENT ID}";

以下示例使用的租户 ID 为aaaabbbb-0000-cccc-1111-dddd2222eeee,客户端 ID 为00001111-aaaa-2222-bbbb-3333cccc4444

oidcOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/";
oidcOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";

对于多租户应用,应该使用通用授权。 还可以对单租户应用使用“通用”颁发机构,但需要自定义 IssuerValidator,如本部分后面所示。

oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";

ResponseType:将 OIDC 处理程序配置为仅执行授权代码流。 在这种模式下,隐式授权和混合流是不必要的。 OIDC 处理程序使用从授权终结点返回的代码自动请求适当的令牌。

oidcOptions.ResponseType = OpenIdConnectResponseType.Code;

MapInboundClaims 以及 NameClaimTypeRoleClaimType 的配置:许多 OIDC 服务器使用“name”和“role”,而不使用 ClaimTypes 中的 SOAP/WS-Fed 默认值。 当 MapInboundClaims 设置为 false 时,处理程序不执行声明映射,应用直接使用 JWT 中的声明名称。 以下示例将角色声明类型设置为“roles”,该类型适用于 Microsoft Entra ID (ME-ID)。 有关详细信息,请参阅标识提供者的文档。

注意

对于大多数 OIDC 提供程序,必须将 MapInboundClaims 设置为 false,以阻止重命名声明。

oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = "name";
oidcOptions.TokenValidationParameters.RoleClaimType = "roles";

路径配置:路径必须与向 OIDC 提供程序注册应用程序时配置的重定向 URI 路径(登录回叫路径)和退出登录后重定向路径(退出登录回叫路径)匹配。 在 Azure 门户中,路径在应用注册的 “身份验证” 边栏选项卡中配置。 登录路径和退出登录路径都必须注册为重定向 URI。 默认值为 /signin-oidc/signout-callback-oidc

CallbackPath:应用的基路径内的请求路径,用于返回用户代理。

在应用的 OIDC 提供程序注册中配置用户登出回调路径。 在下面的示例中,{PORT} 占位符是应用的端口:

https://localhost:{PORT}/signin-oidc

注意

使用 Microsoft Entra ID 时,localhost 地址不需要端口。 大多数其他 OIDC 提供程序都需要正确的端口。

SignedOutCallbackPath(配置密钥:“SignedOutCallbackPath”):OIDC 处理程序截获的应用基路径中的请求路径,用户代理在从标识提供程序中注销后首先会返回至此路径。 示例应用不会为路径设置值,因为使用了默认值“/signout-callback-oidc”。 拦截请求后,OIDC 处理程序会重定向到 SignedOutRedirectUriRedirectUri(如果指定)。

在应用的 OIDC 提供程序注册中配置用户登出回调路径。 在下面的示例中,{PORT} 占位符是应用的端口:

https://localhost:{PORT}/signout-callback-oidc

注意

使用 Microsoft Entra ID 时,请在 Entra 或 Azure 门户的 Web 平台配置的 重定向 URI 条目中设置路径。 使用 Entra 时,localhost 地址不需要端口。 大多数其他 OIDC 提供程序都需要正确的端口。 如果不将注销回调路径 URI 添加到 Entra 中应用的注册中,Entra 将拒绝重定向用户回到应用,并且只会要求他们关闭浏览器窗口。

RemoteSignOutPath:在此路径上收到的请求会导致处理程序使用退出登录方案调用退出登录。

在下面的示例中,{PORT} 占位符是应用的端口:

https://localhost/signout-oidc

注意

使用 Microsoft Entra ID 时,在 Entra 或 Azure 门户中设置 Front-channel 注销 URL。 使用 Entra 时,localhost 地址不需要端口。 大多数其他 OIDC 提供程序都需要正确的端口。

oidcOptions.CallbackPath = new PathString("{PATH}");
oidcOptions.SignedOutCallbackPath = new PathString("{PATH}");
oidcOptions.RemoteSignOutPath = new PathString("{PATH}");

示例(默认值):

oidcOptions.CallbackPath = new PathString("/signin-oidc");
oidcOptions.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
oidcOptions.RemoteSignOutPath = new PathString("/signout-oidc");

Microsoft Azure 仅使用“通用”终结点):TokenValidationParameters.IssuerValidator许多 OIDC 提供程序适用于默认的颁发者验证程序,但我们需要考量由租户 ID ({TENANT ID}) 返回的颁发者参数化的情况https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration。 有关详细信息,请参阅 SecurityTokenInvalidIssuerException 和 OpenID Connect 和 Azure AD“common”终结点(AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#1731)。

仅适用于将 Microsoft Entra ID 或 Azure AD B2C 与“通用”终结点配合使用的应用:

var microsoftIssuerValidator = AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
oidcOptions.TokenValidationParameters.IssuerValidator = microsoftIssuerValidator.Validate;

Blazor Web App 客户端项目 (BlazorWebAppOidc.Client

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

客户端调用 AddAuthenticationStateDeserialization 反序列化并使用服务器传递的身份验证状态。 身份验证状态在 WebAssembly 应用程序的生存期内是固定的。

PersistentAuthenticationStateProvider 类 (PersistentAuthenticationStateProvider.cs) 是客户端组件,通过在服务器上呈现页面时查找页面中保留的数据来确定用户的身份验证状态。 身份验证状态在 WebAssembly 应用程序的生存期内是固定的。

如果用户需要登录或退出登录,则需要重新加载整个页面。

示例应用仅提供用于显示目的的用户名和电子邮件。

对于Microsoft Entra ID 或 Azure AD B2C,可以从 AddMicrosoftIdentityWebApp(Identity、API 文档)使用Microsoft.Identity.Web,该文档会添加 OIDC 和身份验证处理程序以及相应的默认值。 本文中的示例应用和指南不使用 Microsoft Identity Web。 本指南演示如何为任何 OIDC 提供程序 手动 配置 OIDC 处理程序。 有关实现 Microsoft Identity Web 的详细信息,请参阅使用 Microsoft Entra ID 保护 ASP.NET 核心Blazor Web App

本文的这一部分介绍了如何在一个采用全局交互式服务器渲染(单个项目)的应用中,不采用 前端后端(BFF)模式 实现 OIDC。 BFF 模式是一种用于向外部服务发出经过身份验证请求的设计模式。 如果应用的规范要求采用具有全局交互式自动呈现的 BFF 模式,请将文章版本选择器更改为 BFF 模式。

采用以下规范:

  • Blazor Web App使用具有全局交互性的服务器渲染模式
  • 此应用是任何 OIDC 身份验证流的起点。 OIDC 在应用中手动配置,不依赖于 Microsoft Entra IDMicrosoft Identity Web 包,示例应用也不需要 Microsoft Azure 托管。 不过,示例应用可以与 Entra、Microsoft Identity Web 一起使用,并托管在 Azure 中。
  • 自动非交互式令牌刷新。
  • 单独的 Web API 项目演示天气数据的安全 Web API 调用。

有关使用适用于 .NET的 Microsoft 身份验证库、Microsoft webMicrosoft Entra ID的替代体验,请参阅 使用 Microsoft Entra ID保护 ASP.NET Core

示例解决方案

示例应用包括以下项目:

  • BlazorWebAppOidcServer: Blazor Web App 服务器端项目(全局交互式服务器渲染)。
  • MinimalApiJwt:后端 Web API,包含适用于天气数据的 Minimal API 端点。

使用以下链接通过示例存储库中的 Blazor 最新版本文件夹访问示例。 此示例位于 BlazorWebAppOidcServer .NET 8 或更高版本的文件夹中。

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

Microsoft Entra ID 应用注册

我们建议对应用和 Web API 使用单独的注册,即使应用和 Web API 位于同一解决方案中也是如此。 以下指南适用于 BlazorWebAppOidcServer 示例解决方案的应用和 MinimalApiJwt Web API,但相同的指南通常适用于应用和 Web API 的任何基于 Entra 的注册。

首先注册 Web API(MinimalApiJwt),以便在注册应用时授予对 Web API 的访问权限。 Web API 的租户 ID 和客户端 ID 用于在其 Program 文件中配置 Web API。 注册 Web API 后,在应用注册>中公开 Web API公开具有作用域名称的 Weather.GetAPI。 记录应用 ID URI,以便在应用的配置中使用。

接下来,将应用(BlazorWebAppOidcServer)注册到 Web 平台配置和 重定向 URIhttps://localhost/signin-oidc (不需要端口)。 应用的租户 ID 和客户端 ID 以及 Web API 的基址、应用 ID URI 和天气范围名称用于在其 Program 文件中配置应用。 授予 API 权限,以在 应用注册>API 权限中访问 Web API。 如果应用的安全规范要求它,可以授予组织访问 Web API 的管理员同意。 授权用户和组被分配到应用注册>企业应用程序中的应用注册。

在 Entra 或 Azure 门户的 隐式授权和混合流 应用注册配置中,不要选中授权终结点返回 访问令牌ID 令牌的复选框。 OpenID Connect 处理程序使用授权终结点返回的代码自动请求相应的令牌。

在 Entra 或 Azure 门户的应用注册中创建客户端密码(管理>证书和机密>新客户端密码)。 请保留客户端机密,以在下一部分中使用。

本文后面提供了有关特定设置的其他 Entra 配置指南。

建立客户端密码

本部分仅适用于Blazor Web App服务器项目(BlazorWebAppOidcServer项目)。

警告

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

对于本地开发测试,请使用 机密管理器工具 将服务器项目的客户端机密存储在 Blazor 配置密钥 Authentication:Schemes:MicrosoftOidc:ClientSecret下。

尚未为机密管理器工具初始化Blazor服务器项目。 使用命令 shell(如 Visual Studio 中的开发人员 PowerShell 命令 shell)执行以下命令。 在执行命令之前,请将包含 cd 该命令的目录更改为服务器项目的目录。 该命令将建立用户机密标识符(在应用的项目文件中的 <UserSecretsId>):

dotnet user-secrets init

执行以下命令以设置客户端密码。 {SECRET} 占位符是应用注册时获得的客户端密钥。

dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SECRET}"

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

MinimalApiJwt 项目

MinimalApiJwt 项目是用于多个前端项目的后端 Web API。 该项目为天气数据配置 最小 API 终结点。

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

该项目包括用于在开发环境中生成 OpenAPI 文档Swagger UI 的包和配置。 有关详细信息,请参阅 使用生成的 OpenAPI 文档

该项目为天气数据创建 最小 API 终结点:

app.MapGet("/weather-forecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
}).RequireAuthorization();

在项目的 JwtBearerOptions 文件中,在 AddJwtBearer 调用的 Program 中配置项目。

Authority 设置用于进行 OIDC 调用的权限。 建议对 MinimalApiJwt 项目使用单独的应用注册。 颁发机构与标识提供者返回的 JWT 的发行者(iss)匹配。

jwtOptions.Authority = "{AUTHORITY}";

授权机构的格式取决于正在使用的租户类型。 以下 Microsoft Entra ID 的示例使用租户 ID aaaabbbb-0000-cccc-1111-dddd2222eeee

ME-ID 租户权限示例:

jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";

AAD B2C 租户权限示例:

jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/";

对任何收到的 OIDC 令牌设置Audience以确定其受众。

jwtOptions.Audience = "{APP ID URI}";

注意

使用 Microsoft Entra ID 时,将值与在 Entra 或 Azure 门户中,添加在“公开 API”下作用域时配置的Weather.Get的路径进行匹配。 不要在值中包含范围名称“Weather.Get”“。

受众的格式取决于所用租户的类型。 以下示例对于 Microsoft Entra ID 使用租户 ID contoso 和客户端 ID 11112222-bbbb-3333-cccc-4444dddd5555

ME-ID 租户应用 ID URI 示例:

jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";

AAD B2C 租户应用 ID URI 示例:

jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";

BlazorWebAppOidcServer 项目

自动非交互式令牌刷新由自定义 cookie 刷新器管理(CookieOidcRefresher.cs)。

DelegatingHandler (TokenHandler) 负责管理将用户的访问令牌附加到传出请求中。 令牌处理程序仅在静态服务器端呈现(静态 SSR)期间执行,因此在此方案中使用 HttpContext 是安全的。 有关详细信息,请参阅 ASP.NET Core 应用中的 IHttpContextAccessor/HttpContextBlazorASP.NET Core 服务器端以及其他安全方案Blazor Web App

TokenHandler.cs:

public class TokenHandler(IHttpContextAccessor httpContextAccessor) : 
    DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (httpContextAccessor.HttpContext is null)
        {
            throw new Exception("HttpContext not available");
        }

        var accessToken = await httpContextAccessor.HttpContext
            .GetTokenAsync("access_token");

        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);

        return await base.SendAsync(request, cancellationToken);
    }
}

在项目的 Program 文件中,将令牌处理程序 (TokenHandler) 注册为服务,并指定为消息处理程序 (AddHttpMessageHandler),以使用命名的 HTTP 客户端 MinimalApiJwt ("") 发出安全请求至后端 ExternalApi Web API。

builder.Services.AddScoped<TokenHandler>();

builder.Services.AddHttpClient("ExternalApi",
      client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiUri"] ?? 
          throw new Exception("Missing base address!")))
      .AddHttpMessageHandler<TokenHandler>();

Weather组件使用特性[Authorize]来防止未经授权的访问。 有关通过 授权策略 要求跨应用授权以及选择在公共终结点子集退出授权的详细信息,请参阅 Razor Pages OIDC 指南

ExternalApi HTTP 客户端用于向安全 Web API 发出天气数据请求。 在 OnInitializedAsync 生命周期事件Weather.razor

using var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
var client = ClientFactory.CreateClient("ExternalApi");
using var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();

forecasts = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
    throw new IOException("No weather forecast!");

在项目的 appsettings.json 文件中,配置外部 API URI:

"ExternalApiUri": "{BASE ADDRESS}"

示例:

"ExternalApiUri": "https://localhost:7277"

调用 OpenIdConnectOptions 时,可以在项目的 Program 文件中找到以下 AddOpenIdConnect 配置:

PushedAuthorizationBehavior:控制 推送的授权请求(PAR)支持。 默认情况下,如果标识提供者的发现文档(通常位于.well-known/openid-configuration)宣告支持 PAR,则此设置将使用 PAR。 如果要要求对应用提供 PAR 支持,可以分配值 PushedAuthorizationBehavior.Require。 Microsoft Entra 不支持 PAR,并且没有计划让 Entra 将来支持它。

oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.UseIfAvailable;

SignInScheme:设置与身份验证成功后负责保留用户标识的中间件对应的身份验证方案。 OIDC 处理程序需要使用能够跨请求保留用户凭据的登录方案。 以下行仅用于演示目的。 如果省略,则 DefaultSignInScheme 用作回退值。

oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;

openidprofile 的范围 (Scope)(可选):默认情况下也会配置 openidprofile 范围,因为 OIDC 处理程序需要它们才能运行,但可能需要重新添加这些范围(如果 Authentication:Schemes:MicrosoftOidc:Scope 配置中包含范围)。 有关常规配置指南,请参阅 ASP.NET CoreASP.NET Core Blazor 配置中的配置

oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);

配置 Weather.Get 的范围,以访问天气数据的外部 Web API。 以下示例基于在 ME-ID 租户域中使用 Entra ID。 在以下示例中, {APP ID URI} 占位符位于公开 Web API 的 Entra 或 Azure 门户中。 对于任何其他标识提供者,请使用适当的作用域。

oidcOptions.Scope.Add("{APP ID URI}/Weather.Get");

范围的格式取决于正在使用的租户类型。 在以下示例中,租户域为 contoso.onmicrosoft.com,客户端 ID 为 11112222-bbbb-3333-cccc-4444dddd5555

ME-ID 租户应用 ID URI 示例:

oidcOptions.Scope.Add("api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get");

AAD B2C 租户应用 ID URI 示例:

oidcOptions.Scope.Add("https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get");

SaveTokens:定义在授权成功后,是否应在 AuthenticationProperties 中存储访问令牌和刷新令牌。 此属性设置为 true,以便存储非交互式令牌刷新的刷新令牌。

oidcOptions.SaveTokens = true;

脱机访问范围 (Scope):刷新令牌需要 offline_access 范围。

oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);

AuthorityClientId:设置 OIDC 调用的颁发机构和客户端 ID。

oidcOptions.Authority = "{AUTHORITY}";
oidcOptions.ClientId = "{CLIENT ID}";

以下示例使用的租户 ID 为aaaabbbb-0000-cccc-1111-dddd2222eeee,客户端 ID 为00001111-aaaa-2222-bbbb-3333cccc4444

oidcOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/";
oidcOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";

对于多租户应用,应该使用通用授权。 还可以对单租户应用使用“通用”颁发机构,但需要自定义 IssuerValidator,如本部分后面所示。

oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";

ResponseType:将 OIDC 处理程序配置为仅执行授权代码流。 在这种模式下,隐式授权和混合流是不必要的。 OIDC 处理程序使用从授权终结点返回的代码自动请求适当的令牌。

oidcOptions.ResponseType = OpenIdConnectResponseType.Code;

MapInboundClaims 以及 NameClaimTypeRoleClaimType 的配置:许多 OIDC 服务器使用“name”和“role”,而不使用 ClaimTypes 中的 SOAP/WS-Fed 默认值。 当 MapInboundClaims 设置为 false 时,处理程序不执行声明映射,应用直接使用 JWT 中的声明名称。 以下示例将角色声明类型设置为“roles”,该类型适用于 Microsoft Entra ID (ME-ID)。 有关详细信息,请参阅标识提供者的文档。

注意

对于大多数 OIDC 提供程序,必须将 MapInboundClaims 设置为 false,以阻止重命名声明。

oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = "name";
oidcOptions.TokenValidationParameters.RoleClaimType = "roles";

路径配置:路径必须与向 OIDC 提供程序注册应用程序时配置的重定向 URI 路径(登录回叫路径)和退出登录后重定向路径(退出登录回叫路径)匹配。 在 Azure 门户中,路径在应用注册的 “身份验证” 边栏选项卡中配置。 登录路径和退出登录路径都必须注册为重定向 URI。 默认值为 /signin-oidc/signout-callback-oidc

CallbackPath:应用的基路径内的请求路径,用于返回用户代理。

在应用的 OIDC 提供程序注册中配置用户登出回调路径。 在下面的示例中,{PORT} 占位符是应用的端口:

https://localhost:{PORT}/signin-oidc

注意

使用 Microsoft Entra ID 时,localhost 地址不需要端口。 大多数其他 OIDC 提供程序都需要正确的端口。

SignedOutCallbackPath(配置密钥:“SignedOutCallbackPath”):OIDC 处理程序截获的应用基路径中的请求路径,用户代理在从标识提供程序中注销后首先会返回至此路径。 示例应用不会为路径设置值,因为使用了默认值“/signout-callback-oidc”。 拦截请求后,OIDC 处理程序会重定向到 SignedOutRedirectUriRedirectUri(如果指定)。

在应用的 OIDC 提供程序注册中配置用户登出回调路径。 在下面的示例中,{PORT} 占位符是应用的端口:

https://localhost:{PORT}/signout-callback-oidc

注意

使用 Microsoft Entra ID 时,请在 Entra 或 Azure 门户的 Web 平台配置的 重定向 URI 条目中设置路径。 使用 Entra 时,localhost 地址不需要端口。 大多数其他 OIDC 提供程序都需要正确的端口。 如果不将注销回调路径 URI 添加到 Entra 中应用的注册中,Entra 将拒绝重定向用户回到应用,并且只会要求他们关闭浏览器窗口。

RemoteSignOutPath:在此路径上收到的请求会导致处理程序使用退出登录方案调用退出登录。

在下面的示例中,{PORT} 占位符是应用的端口:

https://localhost/signout-oidc

注意

使用 Microsoft Entra ID 时,在 Entra 或 Azure 门户中设置 Front-channel 注销 URL。 使用 Entra 时,localhost 地址不需要端口。 大多数其他 OIDC 提供程序都需要正确的端口。

oidcOptions.CallbackPath = new PathString("{PATH}");
oidcOptions.SignedOutCallbackPath = new PathString("{PATH}");
oidcOptions.RemoteSignOutPath = new PathString("{PATH}");

示例(默认值):

oidcOptions.CallbackPath = new PathString("/signin-oidc");
oidcOptions.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
oidcOptions.RemoteSignOutPath = new PathString("/signout-oidc");

Microsoft Azure 仅使用“通用”终结点):TokenValidationParameters.IssuerValidator许多 OIDC 提供程序适用于默认的颁发者验证程序,但我们需要考量由租户 ID ({TENANT ID}) 返回的颁发者参数化的情况https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration。 有关详细信息,请参阅 SecurityTokenInvalidIssuerException 和 OpenID Connect 和 Azure AD“common”终结点(AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#1731)。

仅适用于将 Microsoft Entra ID 或 Azure AD B2C 与“通用”终结点配合使用的应用:

var microsoftIssuerValidator = AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
oidcOptions.TokenValidationParameters.IssuerValidator = microsoftIssuerValidator.Validate;

对于Microsoft Entra ID 或 Azure AD B2C,可以从 AddMicrosoftIdentityWebApp(Identity、API 文档)使用Microsoft.Identity.Web,该文档会添加 OIDC 和身份验证处理程序以及相应的默认值。 本文中的示例应用和指南不使用 Microsoft Identity Web。 本指南演示如何为任何 OIDC 提供程序 手动 配置 OIDC 处理程序。 有关实现 Microsoft Identity Web 的详细信息,请参阅使用 Microsoft Entra ID 保护 ASP.NET 核心Blazor Web App

本文的此版本介绍如何使用 前端后端 (BFF) 模式实现 OIDC。 如果应用的规范不要求采用 BFF 模式,请将项目版本选择器更改为 非 BFF 模式(交互式自动呈现)非 BFF 模式(交互式服务器)(交互式服务器 呈现)。

先决条件

.NET Aspire 需要 Visual Studio 版本 17.10 或更高版本。

另请参阅快速入门的 “先决条件 ”部分 :生成第一个 .NET Aspire 应用

示例解决方案

示例应用包括以下项目:

  • .NET Aspire:
    • Aspire.AppHost:用于管理应用的高级协调控制问题。
    • Aspire.ServiceDefaults:包含可根据需要进行扩展和自定义的默认 .NET Aspire 应用配置。
  • MinimalApiJwt:后端 Web API,包含一个用于天气数据的Minimal API示例终结点。
  • BlazorWebAppOidc:Blazor Web App 的服务器端项目。 项目使用 YARP 将请求代理到后端 Web API 项目的天气预报终结点(MinimalApiJwt),同时将 access_token 存储在身份验证 cookie 中。
  • BlazorWebAppOidc.Client:Blazor Web App 的客户端项目。

使用以下链接通过示例存储库中的 Blazor 最新版本文件夹访问示例。 此示例位于 BlazorWebAppOidcBff .NET 8 或更高版本的文件夹中。

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

Blazor Web App使用具有全局交互性的自动呈现模式

服务器项目调用 AddAuthenticationStateSerialization 来添加一个服务器端身份验证状态提供程序,该提供程序使用 PersistentComponentState 将身份验证状态传递给客户端。 客户端调用 AddAuthenticationStateDeserialization 反序列化并使用服务器传递的身份验证状态。 身份验证状态在 WebAssembly 应用程序的生存期内是固定的。

PersistingAuthenticationStateProvider 类 (PersistingAuthenticationStateProvider.cs) 是服务器端 AuthenticationStateProvider,它使用 PersistentComponentState 将身份验证状态传输到客户端,然后该状态在 WebAssembly 应用程序的生存期内得到修复。

此应用是任何 OIDC 身份验证流的起点。 OIDC 在应用中手动配置,不依赖于 Microsoft Entra IDMicrosoft Identity Web 包,示例应用也不需要 Microsoft Azure 托管。 不过,示例应用可以与 Entra、Microsoft Identity Web 一起使用,并托管在 Azure 中。

借助自定义 cookie 刷新器 (CookieOidcRefresher.cs) 进行的自动非交互式令牌刷新。

前端的后端(BFF)模式采用.NET Aspire进行服务发现,使用YARP将请求代理到后端应用程序上的天气预报端点。

后端 Web API (MinimalApiJwt) 使用 JWT 持有者身份验证来验证登录Blazor Web App中保存的 cookie JWT 令牌。

Aspire 改善了生成 .NET 云原生应用的体验。 它提供了一套一致的、固定的工具和模式,用于生成和运行分布式应用。

YARP (Yet Another Reverse Proxy) 是一个用于创建反向代理服务器的库。 MapForwarderProgram 服务器项目的文件中,使用传出请求的默认配置、自定义转换和默认 HTTP 客户端的默认配置,将匹配指定模式的 HTTP 请求直接转发到特定目标:

  • 在服务器上呈现 Weather 组件时,该组件使用 ServerWeatherForecaster 类通过用户的访问令牌代理对天气数据的请求。 IHttpContextAccessor.HttpContext 确定 HttpContext 是否可供 GetWeatherForecastAsync 方法使用。 有关详细信息,请参阅 ASP.NET 核心 Razor 组件
  • 在客户端上呈现该组件时,该组件使用 ClientWeatherForecaster 服务实现,而该实现使用预配置的 HttpClient(在客户端项目的 Program 文件中)对服务器项目进行 Web API 调用。 在服务器项目的 /weather-forecast 文件中定义的最小 API 终结点 (Program) 使用用户的访问令牌来转换请求,从而获取天气数据。

有关详细信息.NET Aspire,请参阅简化 .NET Cloud-Native 开发的全面开放:2024 年 5 月.NET Aspire

有关使用服务抽象的 Blazor Web App(Web) API 调用的详细信息,请参阅 从 ASP.NET Core Blazor 应用调用 Web API

Microsoft Entra ID 应用注册

我们建议对应用和 Web API 使用单独的注册,即使应用和 Web API 位于同一解决方案中也是如此。 以下指南适用于 BlazorWebAppOidc 示例解决方案的应用和 MinimalApiJwt Web API,但相同的指南通常适用于应用和 Web API 的任何基于 Entra 的注册。

首先注册 Web API(MinimalApiJwt),以便在注册应用时授予对 Web API 的访问权限。 Web API 的租户 ID 和客户端 ID 用于在其 Program 文件中配置 Web API。 注册 Web API 后,在应用注册>中公开 Web API公开具有作用域名称的 Weather.GetAPI。 记录应用 ID URI,以便在应用的配置中使用。

接下来,将应用(BlazorWebAppOidc/BlazorWebApOidc.Client)注册到 Web 平台配置,并设置重定向 URI 为 ,无需端口。 应用的租户 ID 和客户端 ID 以及 Web API 的基址、应用 ID URI 和天气范围名称用于在其 Program 文件中配置应用。 授予 API 权限,以在 应用注册>API 权限中访问 Web API。 如果应用的安全规范要求它,可以授予组织访问 Web API 的管理员同意。 授权用户和组被分配到应用注册>企业应用程序中的应用注册。

在 Entra 或 Azure 门户的 隐式授权和混合流 应用注册配置中,不要选中授权终结点返回 访问令牌ID 令牌的复选框。 OpenID Connect 处理程序使用授权终结点返回的代码自动请求相应的令牌。

在 Entra 或 Azure 门户的应用注册中创建客户端密码(管理>证书和机密>新客户端密码)。 请保留客户端机密,以在下一部分中使用。

本文后面提供了有关特定设置的其他 Entra 配置指南。

建立客户端密码

本部分仅适用于Blazor Web App服务器项目(BlazorWebAppOidc项目)。

警告

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

对于本地开发测试,请使用 机密管理器工具 将服务器项目的客户端机密存储在 Blazor 配置密钥 Authentication:Schemes:MicrosoftOidc:ClientSecret下。

尚未为机密管理器工具初始化Blazor服务器项目。 使用命令 shell(如 Visual Studio 中的开发人员 PowerShell 命令 shell)执行以下命令。 在执行命令之前,请将包含 cd 该命令的目录更改为服务器项目的目录。 该命令将建立用户机密标识符(<UserSecretsId> 在服务器应用的项目文件中):

dotnet user-secrets init

执行以下命令以设置客户端密码。 {SECRET} 占位符是应用注册时获得的客户端密钥。

dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SECRET}"

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

.NET Aspire 项目

更多关于使用.NET Aspire的信息,以及示例应用的.AppHost.ServiceDefaults项目的详细信息,请参阅.NET Aspire文档

确认已满足 .NET Aspire 的先决条件。 有关详细信息,请参阅快速入门:构建您的第一个 应用程序的先决条件部分。

示例应用仅配置一个不安全的 HTTP 启动配置文件 (http),供在开发测试期间使用。 有关详细信息,包括不安全和安全启动设置配置文件的示例,请参阅(.NET Aspire文档)中.NET Aspire允许不安全的传输

MinimalApiJwt 项目

MinimalApiJwt 项目是用于多个前端项目的后端 Web API。 该项目为天气数据配置 最小 API 终结点。 来自 Blazor Web App 服务器端项目 (BlazorWebAppOidc) 的请求会被代理到 MinimalApiJwt 项目。

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

该项目包括用于在开发环境中生成 OpenAPI 文档Swagger UI 的包和配置。 有关详细信息,请参阅 使用生成的 OpenAPI 文档

安全天气预报数据终结点位于项目的 Program 文件中:

app.MapGet("/weather-forecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
}).RequireAuthorization();

RequireAuthorization 扩展方法需要路由定义授权。 对于添加到项目的任何控制器,请将 [Authorize] 属性 添加到控制器或作。

在项目的 JwtBearerOptions 文件中,在 AddJwtBearer 调用的 Program 中配置项目。

Authority 设置用于进行 OIDC 调用的权限。 建议对 MinimalApiJwt 项目使用单独的应用注册。 颁发机构与标识提供者返回的 JWT 的发行者(iss)匹配。

jwtOptions.Authority = "{AUTHORITY}";

授权机构的格式取决于正在使用的租户类型。 以下 Microsoft Entra ID 的示例使用租户 ID aaaabbbb-0000-cccc-1111-dddd2222eeee

ME-ID 租户权限示例:

jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";

AAD B2C 租户权限示例:

jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/";

对任何收到的 OIDC 令牌设置Audience以确定其受众。

jwtOptions.Audience = "{APP ID URI}";

注意

使用 Microsoft Entra ID 时,将值与在 Entra 或 Azure 门户中,添加在“公开 API”下作用域时配置的Weather.Get的路径进行匹配。 不要在值中包含范围名称“Weather.Get”“。

受众的格式取决于所用租户的类型。 以下示例对于 Microsoft Entra ID 使用租户 ID contoso 和客户端 ID 11112222-bbbb-3333-cccc-4444dddd5555

ME-ID 租户应用 ID URI 示例:

jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";

AAD B2C 租户应用 ID URI 示例:

jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";

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

本部分介绍如何配置服务器端 Blazor 项目。

在项目的OpenIdConnectOptions文件中找到以下Program配置,调用AddOpenIdConnect时。

PushedAuthorizationBehavior:控制 推送的授权请求(PAR)支持。 默认情况下,如果标识提供者的发现文档(通常位于.well-known/openid-configuration)宣告支持 PAR,则此设置将使用 PAR。 如果要要求对应用提供 PAR 支持,可以分配值 PushedAuthorizationBehavior.Require。 Microsoft Entra 不支持 PAR,并且没有计划让 Entra 将来支持它。

oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.UseIfAvailable;

SignInScheme:设置与身份验证成功后负责保留用户标识的中间件对应的身份验证方案。 OIDC 处理程序需要使用能够跨请求保留用户凭据的登录方案。 以下行仅用于演示目的。 如果省略,则 DefaultSignInScheme 用作回退值。

oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;

openidprofile 的范围 (Scope)(可选):默认情况下也会配置 openidprofile 范围,因为 OIDC 处理程序需要它们才能运行,但可能需要重新添加这些范围(如果 Authentication:Schemes:MicrosoftOidc:Scope 配置中包含范围)。 有关常规配置指南,请参阅 ASP.NET CoreASP.NET Core Blazor 配置中的配置

oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);

SaveTokens:定义在授权成功后,是否应在 AuthenticationProperties 中存储访问令牌和刷新令牌。 该值设置为 true,目的是验证天气数据请求,这些天气数据来自后端 Web API 项目 (MinimalApiJwt)。

oidcOptions.SaveTokens = true;

脱机访问范围 (Scope):刷新令牌需要 offline_access 范围。

oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);

从 Web API 获取天气数据的范围(Scope):配置 Weather.Get 用于访问天气数据的外部 Web API 的范围。 在以下示例中, {APP ID URI} 占位符位于公开 Web API 的 Entra 或 Azure 门户中。 对于任何其他标识提供者,请使用适当的作用域。

oidcOptions.Scope.Add("{APP ID URI}/Weather.Get");

范围的格式取决于正在使用的租户类型。 在以下示例中,租户域为 contoso.onmicrosoft.com,客户端 ID 为 11112222-bbbb-3333-cccc-4444dddd5555

ME-ID 租户应用 ID URI 示例:

oidcOptions.Scope.Add("api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get");

AAD B2C 租户应用 ID URI 示例:

oidcOptions.Scope.Add("https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get");

AuthorityClientId:设置 OIDC 调用的颁发机构和客户端 ID。

oidcOptions.Authority = "{AUTHORITY}";
oidcOptions.ClientId = "{CLIENT ID}";

以下示例使用的租户 ID 为aaaabbbb-0000-cccc-1111-dddd2222eeee,客户端 ID 为00001111-aaaa-2222-bbbb-3333cccc4444

oidcOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/";
oidcOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";

对于多租户应用,应该使用通用授权。 还可以对单租户应用使用“通用”颁发机构,但需要自定义 IssuerValidator,如本部分后面所示。

oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";

ResponseType:将 OIDC 处理程序配置为仅执行授权代码流。 在这种模式下,隐式授权和混合流是不必要的。 OIDC 处理程序使用从授权终结点返回的代码自动请求适当的令牌。

oidcOptions.ResponseType = OpenIdConnectResponseType.Code;

MapInboundClaims 以及 NameClaimTypeRoleClaimType 的配置:许多 OIDC 服务器使用“name”和“role”,而不使用 ClaimTypes 中的 SOAP/WS-Fed 默认值。 当 MapInboundClaims 设置为 false 时,处理程序不执行声明映射,应用直接使用 JWT 中的声明名称。 以下示例将角色声明类型设置为“roles”,该类型适用于 Microsoft Entra ID (ME-ID)。 有关详细信息,请参阅标识提供者的文档。

注意

对于大多数 OIDC 提供程序,必须将 MapInboundClaims 设置为 false,以阻止重命名声明。

oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = "name";
oidcOptions.TokenValidationParameters.RoleClaimType = "roles";

路径配置:路径必须与向 OIDC 提供程序注册应用程序时配置的重定向 URI 路径(登录回叫路径)和退出登录后重定向路径(退出登录回叫路径)匹配。 在 Azure 门户中,路径在应用注册的 “身份验证” 边栏选项卡中配置。 登录路径和退出登录路径都必须注册为重定向 URI。 默认值为 /signin-oidc/signout-callback-oidc

在应用的 OIDC 提供程序注册中配置用户登出回调路径。 在下面的示例中,{PORT} 占位符是应用的端口:

https://localhost:{PORT}/signin-oidc

注意

使用 Microsoft Entra ID 时,localhost 地址不需要端口。 大多数其他 OIDC 提供程序都需要正确的端口。

SignedOutCallbackPath(配置密钥:“SignedOutCallbackPath”):OIDC 处理程序截获的应用基路径中的请求路径,用户代理在从标识提供程序中注销后首先会返回至此路径。 示例应用不会为路径设置值,因为使用了默认值“/signout-callback-oidc”。 拦截请求后,OIDC 处理程序会重定向到 SignedOutRedirectUriRedirectUri(如果指定)。

在应用的 OIDC 提供程序注册中配置用户登出回调路径。 在下面的示例中,{PORT} 占位符是应用的端口:

https://localhost:{PORT}/signout-callback-oidc

注意

使用 Microsoft Entra ID 时,请在 Entra 或 Azure 门户的 Web 平台配置的 重定向 URI 条目中设置路径。 使用 Entra 时,localhost 地址不需要端口。 大多数其他 OIDC 提供程序都需要正确的端口。 如果不将注销回调路径 URI 添加到 Entra 中应用的注册中,Entra 将拒绝重定向用户回到应用,并且只会要求他们关闭浏览器窗口。

RemoteSignOutPath:在此路径上收到的请求会导致处理程序使用退出登录方案调用退出登录。

在下面的示例中,{PORT} 占位符是应用的端口:

https://localhost/signout-oidc

注意

使用 Microsoft Entra ID 时,在 Entra 或 Azure 门户中设置 Front-channel 注销 URL。 使用 Entra 时,localhost 地址不需要端口。 大多数其他 OIDC 提供程序都需要正确的端口。

oidcOptions.CallbackPath = new PathString("{PATH}");
oidcOptions.SignedOutCallbackPath = new PathString("{PATH}");
oidcOptions.RemoteSignOutPath = new PathString("{PATH}");

示例(默认值):

oidcOptions.CallbackPath = new PathString("/signin-oidc");
oidcOptions.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
oidcOptions.RemoteSignOutPath = new PathString("/signout-oidc");

Microsoft Azure 仅使用“通用”终结点):TokenValidationParameters.IssuerValidator许多 OIDC 提供程序适用于默认的颁发者验证程序,但我们需要考量由租户 ID ({TENANT ID}) 返回的颁发者参数化的情况https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration。 有关详细信息,请参阅 SecurityTokenInvalidIssuerException 和 OpenID Connect 和 Azure AD“common”终结点(AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#1731)。

仅适用于将 Microsoft Entra ID 或 Azure AD B2C 与“通用”终结点配合使用的应用:

var microsoftIssuerValidator = AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
oidcOptions.TokenValidationParameters.IssuerValidator = microsoftIssuerValidator.Validate;

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

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

客户端调用 AddAuthenticationStateDeserialization 反序列化并使用服务器传递的身份验证状态。 身份验证状态在 WebAssembly 应用程序的生存期内是固定的。

PersistentAuthenticationStateProvider 类 (PersistentAuthenticationStateProvider.cs) 是客户端组件,通过在服务器上呈现页面时查找页面中保留的数据来确定用户的身份验证状态。 身份验证状态在 WebAssembly 应用程序的生存期内是固定的。

如果用户需要登录或退出登录,则需要重新加载整个页面。

示例应用仅提供用于显示目的的用户名和电子邮件。

仅序列化名称和角色声明

本部分仅适用于非 BFF 模式(交互式自动)和 BFF 模式(交互式自动)及其示例应用。

Program文件中,所有声明都通过将SerializeAllClaims设置为true来进行序列化。 如果只想为 CSR 序列化名称和角色声明,请删除该选项或将其设置为 false

使用 JSON 配置提供程序提供配置(应用设置)

示例解决方案项目在其Program文件中配置 OIDC 和 JWT 持有者身份验证,以便使用 C# 自动完成来发现配置设置。 专业应用通常使用 配置提供程序 来配置 OIDC 选项,例如默认 JSON 配置提供程序。 JSON 配置提供程序从应用设置文件 appsettings.json/appsettings.{ENVIRONMENT}.json加载配置,其中 {ENVIRONMENT} 占位符是应用的 运行时环境。 按照本部分中的指南使用应用设置文件进行配置。

appsettings.jsonBlazorWebAppOidc项目的应用设置文件BlazorWebAppOidcServer中,添加以下 JSON 配置:

"Authentication": {
  "Schemes": {
    "MicrosoftOidc": {
      "Authority": "https://login.microsoftonline.com/{TENANT ID (BLAZOR APP)}/v2.0/",
      "ClientId": "{CLIENT ID (BLAZOR APP)}",
      "CallbackPath": "/signin-oidc",
      "SignedOutCallbackPath": "/signout-callback-oidc",
      "RemoteSignOutPath": "/signout-oidc",
      "SignedOutRedirectUri": "/",
      "Scope": [
        "openid",
        "profile",
        "offline_access",
        "{APP ID URI (WEB API)}/Weather.Get"
      ]
    }
  }
},

更新上述配置中的占位符,以匹配应用在 Program 文件中使用的值:

  • {TENANT ID (BLAZOR APP)}:Blazor 应用的租户 ID。
  • {CLIENT ID (BLAZOR APP)}:应用的客户端 ID Blazor 。
  • {APP ID URI (WEB API)}:Web API 的应用 ID URI。

“通用”授权(https://login.microsoftonline.com/common/v2.0/)应使用于多租户应用。 若要对单租户应用使用“通用”颁发机构,请参阅“ 对单租户应用使用通用”颁发机构 部分。

更新上述配置中的任何其他值,以匹配文件中使用的 Program 自定义/非默认值。

身份验证生成器会自动选取配置。

Program 文件中删除以下行:

- oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
- oidcOptions.Scope.Add("...");
- oidcOptions.CallbackPath = new PathString("...");
- oidcOptions.SignedOutCallbackPath = new PathString("...");
- oidcOptions.RemoteSignOutPath = new PathString("...");
- oidcOptions.Authority = "...";
- oidcOptions.ClientId = "...";

ConfigureCookieOidc的方法CookieOidcServiceCollectionExtensions.cs中,删除以下行:

- oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);

MinimalApiJwt 项目中,将以下应用设置配置添加到 appsettings.json 该文件:

"Authentication": {
  "Schemes": {
    "Bearer": {
      "Authority": "https://sts.windows.net/{TENANT ID (WEB API)}/",
      "ValidAudiences": [ "{APP ID URI (WEB API)}" ]
    }
  }
},

更新上述配置中的占位符,以匹配应用在 Program 文件中使用的值:

  • {TENANT ID (WEB API)}:Web API 的租户 ID。
  • {APP ID URI (WEB API)}:Web API 的应用 ID URI。

权威机构格式采用以下模式:

  • ME-ID 租户类型: https://sts.windows.net/{TENANT ID}/
  • B2C 租户类型: https://login.microsoftonline.com/{TENANT ID}/v2.0/

受众格式采用以下模式({CLIENT ID} 是 Web API 的客户端 ID; {DIRECTORY NAME} 例如,目录名称 contoso):

  • ME-ID 租户类型: api://{CLIENT ID}
  • B2C 租户类型: https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}

JWT 持有者身份验证生成器会自动选取配置。

Program 文件中删除以下行:

- jwtOptions.Authority = "...";
- jwtOptions.Audience = "...";

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

对单租户应用使用“通用”授权机构

可以将“通用”颁发机构用于单租户应用,但必须执行以下步骤来实现自定义颁发者验证程序。

Microsoft.IdentityModel.Validators NuGet 包添加到BlazorWebAppOidcBlazorWebAppOidcServerBlazorWebAppOidcBff项目。

注意

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

Program 文件的开头,使 Microsoft.IdentityModel.Validators 命名空间可用:

using Microsoft.IdentityModel.Validators;

Program 配置 OIDC 选项的文件中使用以下代码:

var microsoftIssuerValidator = 
    AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
oidcOptions.TokenValidationParameters.IssuerValidator = 
    microsoftIssuerValidator.Validate;

有关详细信息,请参阅 SecurityTokenInvalidIssuerException 和 OpenID Connect 和 Azure AD“common”终结点(AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#1731)。

在注销时重定向到主页

LogInOrOut 组件 (Layout/LogInOrOut.razor) 将返回 URL (ReturnUrl) 的隐藏字段设置为当前 URL (currentURL)。 当用户注销应用时,标识提供者会将用户返回到他们注销的页面。如果用户从安全页面注销,则会返回到同一安全页,并通过身份验证过程发送回。 当用户需要定期更改帐户时,这种身份验证流程是合理的。

或者,使用以下 LogInOrOut 组件,该组件在注销时不提供返回的 URL。

Layout/LogInOrOut.razor:

<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
                </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>

令牌刷新

自定义cookie刷新程序 (CookieOidcRefresher.cs) 实现会在用户的声明过期时自动更新声明。 当前实现需要从令牌终结点接收 ID 令牌,以换取刷新令牌。 然后,这个 ID 令牌中的声明会用来替换用户的声明。

示例实现不包括用于在令牌刷新时从 UserInfo 终结点 请求声明的代码。 有关详细信息,请参阅 BlazorWebAppOidc AddOpenIdConnect with GetClaimsFromUserInfoEndpoint = true doesn't propogate [sic] role claims to clientdotnet/aspnetcore#58826)。

注意

某些标识提供者 仅在使用刷新令牌时返回访问令牌。 可以使用额外逻辑更新 CookieOidcRefresher,以继续使用在身份验证 cookie 中存储的前一组声明,或使用访问令牌从 UserInfo 端点请求声明。

加密 nonce

Nonce 是一个字符串值,该值将客户端的会话与 ID 令牌相关联,以缓解重播攻击

如果在身份验证开发和测试期间收到 nonce 错误,则无论对应用或测试用户所做的更改有多小,都请在每次测试运行时使用新的 InPrivate/Incognito 浏览器会话,因为过时的 cookie 数据可能会导致出现 nonce 错误。 有关详细信息,请参阅 Cookie 和站点数据 部分。

当刷新令牌交换为新的访问令牌时,不需要或使用 nonce。 在示例应用中,CookieOidcRefresher (CookieOidcRefresher.cs) 故意将 OpenIdConnectProtocolValidator.RequireNonce 设置为 false

未在 Microsoft Entra (ME-ID) 注册的应用的应用程序角色

本部分与不使用 Microsoft Entra ID(ME-ID) 作为标识提供者的应用相关。 使用 ME-ID 注册的应用程序,请参阅Microsoft Entra 注册应用的应用程序角色(ME-ID)部分

TokenValidationParameters.RoleClaimTypeOpenIdConnectOptions 中配置角色声明类型 (Program.cs):

oidcOptions.TokenValidationParameters.RoleClaimType = "{ROLE CLAIM TYPE}";

对于许多 OIDC 身份提供者,角色声明类型是 role。 请查看身份提供者的文档,以获取正确的值。

UserInfo 项目中 BlazorWebAppOidc.Client 类替换为以下类。

UserInfo.cs:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using System.Security.Claims;

namespace BlazorWebAppOidc.Client;

// Add properties to this class and update the server and client 
// AuthenticationStateProviders to expose more information about 
// the authenticated user to the client.
public sealed class UserInfo
{
    public required string UserId { get; init; }
    public required string Name { get; init; }
    public required string[] Roles { get; init; }

    public const string UserIdClaimType = "sub";
    public const string NameClaimType = "name";
    private const string RoleClaimType = "role";

    public static UserInfo FromClaimsPrincipal(ClaimsPrincipal principal) =>
        new()
        {
            UserId = GetRequiredClaim(principal, UserIdClaimType),
            Name = GetRequiredClaim(principal, NameClaimType),
            Roles = principal.FindAll(RoleClaimType).Select(c => c.Value)
                .ToArray(),
        };

    public ClaimsPrincipal ToClaimsPrincipal() =>
        new(new ClaimsIdentity(
            Roles.Select(role => new Claim(RoleClaimType, role))
                .Concat([
                    new Claim(UserIdClaimType, UserId),
                    new Claim(NameClaimType, Name),
                ]),
            authenticationType: nameof(UserInfo),
            nameType: NameClaimType,
            roleType: RoleClaimType));

    private static string GetRequiredClaim(ClaimsPrincipal principal,
        string claimType) =>
            principal.FindFirst(claimType)?.Value ??
            throw new InvalidOperationException(
                $"Could not find required '{claimType}' claim.");
}

此时, Razor 组件可以采用 基于角色的授权和基于策略的授权。 应用程序角色显示在 role 声明中,每个角色一个声明。

在 Microsoft Entra (ME-ID) 注册的应用的应用程序角色

使用本部分中的指南,使用 Microsoft Entra ID(ME-ID)为应用实现应用程序角色、ME-ID 安全组和 ME-ID 内置管理员角色。

本节中介绍的方法会将 ME-ID 配置为在身份验证 cookie 标头中发送组和角色。 如果用户只是少数安全组和角色的成员,则以下方法应该适用于大多数托管平台,而不会遇到标头过长的问题,例如,具有默认标头长度限制为 16 KB 的 (MaxRequestBytes) IIS 托管。 如果标头长度由于组或角色成员身份较高而出现问题,我们建议不要遵循本部分中的指导,以便实现 Microsoft Graph ,以便单独从 ME-ID 获取用户的组和角色,这种方法不会夸大身份验证 cookie的大小。 有关详细信息,请参阅 错误请求 - 请求太长 - IIS 服务器 (dotnet/aspnetcore #57545)

TokenValidationParameters.RoleClaimTypeOpenIdConnectOptions 中配置角色声明类型 (Program.cs)。 将值设置为 roles

oidcOptions.TokenValidationParameters.RoleClaimType = "roles";

尽管在没有 ME-ID 高级帐户的情况下无法 将角色分配给组 ,但可以向用户分配角色,并为具有标准 Azure 帐户的用户接收角色声明。 本部分中的指南不需要 ME-ID Premium 帐户。

使用默认目录时,请按照 向应用程序添加应用角色并在令牌中接收它们的指南(ME-ID 文档) 来配置和分配角色。 如果不使用默认目录,请在Azure 门户中编辑应用的清单,以在清单文件的 appRoles 条目中手动建立应用的角色。 有关详细信息,请参阅“配置角色声明”(ME-ID 文档)。

用户的 Azure 安全组以 groups 声明的形式呈现,并且用户的内置 ME-ID 管理员角色分配以 wids 众所周知的 ID 声明的形式呈现。 这两种声明类型的值都是 GUID。 应用收到后,这些声明可用于 在组件中 Razor 建立角色和策略授权

在 Azure 门户中的应用清单中,将 groupMembershipClaims 属性 设置为 All。 值 All 会导致 ME-ID 发送已登录用户的所有安全/分配组(groups 声明)和角色(wids 声明)。 若要设置 groupMembershipClaims 特性:

  1. 打开 Azure 门户中的应用注册。
  2. 在边栏中选择 “管理>清单 ”。
  3. 查找 groupMembershipClaims 属性。
  4. 将值设置为 All ("groupMembershipClaims": "All")。
  5. 选择“保存”按钮。

UserInfo 项目中 BlazorWebAppOidc.Client 类替换为以下类。

UserInfo.cs:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using System.Security.Claims;

namespace BlazorWebAppOidc.Client;

// Add properties to this class and update the server and client 
// AuthenticationStateProviders to expose more information about 
// the authenticated user to the client.
public sealed class UserInfo
{
    public required string UserId { get; init; }
    public required string Name { get; init; }
    public required string[] Roles { get; init; }
    public required string[] Groups { get; init; }
    public required string[] Wids { get; init; }

    public const string UserIdClaimType = "sub";
    public const string NameClaimType = "name";
    private const string RoleClaimType = "roles";
    private const string GroupsClaimType = "groups";
    private const string WidsClaimType = "wids";

    public static UserInfo FromClaimsPrincipal(ClaimsPrincipal principal) =>
        new()
        {
            UserId = GetRequiredClaim(principal, UserIdClaimType),
            Name = GetRequiredClaim(principal, NameClaimType),
            Roles = principal.FindAll(RoleClaimType).Select(c => c.Value)
                .ToArray(),
            Groups = principal.FindAll(GroupsClaimType).Select(c => c.Value)
                .ToArray(),
            Wids = principal.FindAll(WidsClaimType).Select(c => c.Value)
                .ToArray(),
        };

    public ClaimsPrincipal ToClaimsPrincipal() =>
        new(new ClaimsIdentity(
            Roles.Select(role => new Claim(RoleClaimType, role))
                .Concat(Groups.Select(role => new Claim(GroupsClaimType, role)))
                .Concat(Wids.Select(role => new Claim(WidsClaimType, role)))
                .Concat([
                    new Claim(UserIdClaimType, UserId),
                    new Claim(NameClaimType, Name),
                ]),
            authenticationType: nameof(UserInfo),
            nameType: NameClaimType,
            roleType: RoleClaimType));

    private static string GetRequiredClaim(ClaimsPrincipal principal,
        string claimType) =>
            principal.FindFirst(claimType)?.Value ??
            throw new InvalidOperationException(
                $"Could not find required '{claimType}' claim.");
}

此时, Razor 组件可以采用 基于角色的授权和基于策略的授权

  • 应用程序角色显示在 roles 声明中,每个角色一个声明。
  • 安全组显示在 groups 声明中,每个组一个声明。 创建安全组时,安全组 GUID 会显示在 Azure 门户中,并在选择 Identity>“概述>>”视图时列出。
  • 内置 ME-ID 管理员角色显示在 wids 声明中,每个角色一个声明。 ME-ID 始终会发送值为 widsb79fbf4d-3ef9-4689-8143-76b194e85509 声明,以便用于租户的非来宾帐户,并且不涉及管理员角色。 在 Azure 门户中,选择角色和管理员时,管理员角色的GUID(角色模板 ID)会显示出来,接着点击省略号(...)以查看列出的角色>。 角色模板 ID 也列在 Microsoft Entra 内置角色(Entra 文档)中。

故障排除

日志记录

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

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

常见错误

  • 使用 Microsoft Entra 外部 ID 注销期间,调试器因异常而中断

    以下异常将导致 Microsoft Entra 外部 ID 在注销时停止 Visual Studio 调试器:

    Uncaught TypeError TypeError: Failed to execute 'postMessage' on 'Window': The provided value cannot be converted to a sequence.

    Visual Studio 调试器在注销时因 JavaScript 异常中断

    异常是从 Entra JavaScript 代码引发的,因此这不是 ASP.NET Core 的问题。 此异常不会影响生产中的应用功能,因此可以在本地开发测试期间忽略该异常。

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

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

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

    本文中的配置部分包含正确配置的示例。 请仔细查看配置,以查找应用和 IP 配置错误。

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

    • 分析应用程序日志。

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

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

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

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

  • 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 或 Incognito 模式打开浏览器:
    • 通过 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 或 Incognito 模式下打开的命令行选项。 某些浏览器需要应用的 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 Gallery

从正确的项目启动解决方案

Blazor Web App:

  • 对于前端后端模式示例之一,请从 Aspire/Aspire.AppHost 项目启动解决方案。
  • 对于其中一个非 BFF 模式示例,请从 服务器项目启动解决方案。

Blazor Server:

服务器项目启动解决方案。

检查用户

可直接在应用中使用以下 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;
    }
}

其他资源