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

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

本文的这个版本介绍了如何在不采用服务于前端的后端 (BFF) 模式的情况下实现 OIDC。 BFF 模式用于向外部服务发出经验证的请求。 如果应用的规范要求采用 BFF 模式,请将文章版本选择器更改为“采用 BFF 模式的 OIDC”。

涵盖以下规范:

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

示例应用

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

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

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

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

服务器端 Blazor Web 应用项目 (BlazorWebAppOidc)

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

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

注意

服务器项目使用 IHttpContextAccessor/HttpContext,但始终不会用于交互式呈现的组件。 有关详细信息,请参阅 ASP.NET Core Blazor 交互式服务器端呈现的威胁缓解指南

配置

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

注意

对于 Microsoft Entra ID 和 Azure AD B2C,你可以使用 Microsoft Identity Web 中的 AddMicrosoftIdentityWebAppMicrosoft.Identity.Web NuGet 包API 文档),这会添加具有适当默认值的 OIDC 和 Cookie 身份验证处理程序。 本部分中的示例应用和指南不使用 Microsoft Identity Web。 该指南演示了如何为任何 OIDC 提供程序手动配置 OIDC 处理程序。 若要详细了解如何实现 Microsoft Identity Web,请参阅链接的资源。

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

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

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

    oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
    
  • SaveTokens:定义在授权成功后,是否应在 AuthenticationProperties 中存储访问令牌和刷新令牌。 此属性默认设置为 false,以减小最终身份验证 cookie 的大小。

    oidcOptions.SaveTokens = false;
    
  • 脱机访问范围 (Scope):刷新令牌需要 offline_access 范围。

    oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);
    
  • AuthorityClientId:设置 OIDC 调用的颁发机构和客户端 ID。

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

    示例:

    • 颁发机构 ({AUTHORITY}):https://login.microsoftonline.com/a3942615-d115-4eb7-bc84-9974abcf5064/v2.0/(使用租户 ID a3942615-d115-4eb7-bc84-9974abcf5064
    • 客户端 ID ({CLIENT ID}):4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f
    oidcOptions.Authority = "https://login.microsoftonline.com/a3942615-d115-4eb7-bc84-9974abcf5064/v2.0/";
    oidcOptions.ClientId = "4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f";
    

    Microsoft Azure“通用”颁发机构示例:

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

    oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";
    
  • ClientSecret:OIDC 客户端机密。

    以下示例仅用于测试和演示目的。 不要将客户端机密存储在应用的程序集中,也不要将机密签入源代码管理中。 将客户端机密存储在用户机密Azure Key Vault环境变量中。

    身份验证方案配置自动从 builder.Configuration["Authentication:Schemes:{SCHEME NAME}:{PropertyName}"] 读取,其中的 {SCHEME NAME} 占位符是方案,默认为 MicrosoftOidc。 由于配置是预先配置的,因此可以通过 Authentication:Schemes:MicrosoftOidc:ClientSecret 配置密钥自动读取客户端机密。 在使用环境变量的服务器上,将环境变量命名为 Authentication__Schemes__MicrosoftOidc__ClientSecret

    set Authentication__Schemes__MicrosoftOidc__ClientSecret={CLIENT SECRET}
    

    可以直接设置 ClientSecret仅用于演示和测试)。 不要直接为已部署的生产应用设置值。 若要稍微提高安全性,请有条件地编译DEBUG 符号的行:

    #if DEBUG
    oidcOptions.ClientSecret = "{CLIENT SECRET}";
    #endif
    

    示例:

    客户端机密 ({CLIENT SECRET}):463471c8c4...f90d674bc9(为了显示而进行了缩短)

    #if DEBUG
    oidcOptions.ClientSecret = "463471c8c4...137f90d674bc9";
    #endif
    
  • ResponseType:将 OIDC 处理程序配置为仅执行授权代码流。 在这种模式下,隐式授权和混合流是不必要的。

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

    oidcOptions.ResponseType = OpenIdConnectResponseType.Code;
    
  • MapInboundClaims 以及 NameClaimTypeRoleClaimType 的配置:许多 OIDC 服务器使用“name”和“role”,而不使用 ClaimTypes 中的 SOAP/WS-Fed 默认值。 当 MapInboundClaims 设置为 false 时,处理程序不执行声明映射,应用直接使用 JWT 中的声明名称。 以下示例手动映射名称和角色声明:

注意

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

oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Name;
oidcOptions.TokenValidationParameters.RoleClaimType = "role";
  • 路径配置:路径必须与向 OIDC 提供程序注册应用程序时配置的重定向 URI 路径(登录回叫路径)和退出登录后重定向路径(退出登录回叫路径)匹配。 在 Microsoft Azure 门户中,路径是在应用注册的“身份验证”边栏选项卡中配置的。 登录路径和退出登录路径都必须注册为重定向 URI。 默认值为 /signin-oidc/signout-callback-oidc

    • CallbackPath:应用的基路径内的请求路径,将在其中返回用户代理。

      在 Entra 或 Microsoft Azure 门户中,设置 Web 平台配置的“重定向 URI”中的路径:

      https://localhost/signin-oidc

      注意

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

    • SignedOutCallbackPath:应用基路径内的请求路径,在其中,从标识提供者退出登录后会返回用户代理。

      在 Entra 或 Microsoft Azure 门户中,设置 Web 平台配置的“重定向 URI”中的路径:

      https://localhost/signout-callback-oidc

      注意

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

      注意

      如果使用 Microsoft Identity Web,则提供者当前仅在使用 microsoftonline.com 颁发机构 (https://login.microsoftonline.com/{TENANT ID}/v2.0/) 时重定向回 SignedOutCallbackPath。 如果你可以使用 Microsoft Identity Web 的“通用”颁发机构,则不存在此限制。 有关详细信息,请参阅当颁发机构 URL 包含租户 ID 时 postLogoutRedirectUri 不起作用 (AzureAD/microsoft-authentication-library-for-js #5783)

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

      在 Entra 或 Microsoft Azure 门户中,设置“前端通道退出登录 URL”:

      https://localhost/signout-oidc

      注意

      使用 Microsoft Entra ID 时,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 提供程序都使用默认颁发者验证器,但我们需要考虑使用由 https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration 返回的租户 ID ({TENANT ID}) 参数化的颁发者。 有关详细信息,请参阅 OpenID Connect 和 Azure AD“通用”终结点的 SecurityTokenInvalidIssuerException (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;
    

示例应用代码

检查示例应用的以下功能:

  • 借助自定义 cookie 刷新器 (CookieOidcRefresher.cs) 进行的自动非交互式令牌刷新。
  • PersistingAuthenticationStateProvider 类 (PersistingAuthenticationStateProvider.cs) 是服务器端 AuthenticationStateProvider,它使用 PersistentComponentState 将身份验证状态传输到客户端,然后该状态在 WebAssembly 应用程序的生存期内得到修复。
  • 向 Blazor Web 应用请求天气数据的示例请求由 Program 文件 (Program.cs) 中的最小 API 终结点 (/weather-forecast) 处理。 终结点要求通过调用 RequireAuthorization 进行授权。 对于添加到项目中的任何控制器,请将 [Authorize] 属性添加到控制器或操作中。
  • 该应用安全地调用服务器项目中的 (Web) API 来获取天气数据:
    • 在服务器上呈现 Weather 组件时,该组件使用服务器上的 ServerWeatherForecaster 直接获取天气数据(而不是通过 Web API 调用)。
    • 在客户端上呈现该组件时,该组件使用 ClientWeatherForecaster 服务实现,而该实现使用预配置的 HttpClient(在客户端项目的 Program 文件中)对服务器项目进行 Web API 调用。 服务器项目的 Program 文件中定义的最小 API 终结点 (/weather-forecast) 从 ServerWeatherForecaster 获取天气数据,并将数据返回到客户端。

如需详细了解在 Blazor Web 应用中使用服务抽象的 (Web) API 调用,请参阅从 ASP.NET Core Blazor 应用调用 Web API

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

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

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

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

示例应用仅提供用于显示目的的用户名和电子邮件。 它不包括在发出后续请求时向服务器进行身份验证的令牌,它是使用 cookie 单独工作的(其包含在对服务器的 HttpClient 请求中)。

本文的这个版本介绍了如何在使用服务于前端的后端 (BFF) 模式的情况下实现 OIDC。 如果应用的规范没有要求采用 BFF 模式,请将文章版本选择器更改为“没有采用 BFF 模式的 OIDC”。

涵盖以下规范:

  • Blazor Web 应用使用具有全局交互性的自动呈现模式
  • 服务器和客户端应用使用自定义身份验证状态提供程序服务来捕获用户的身份验证状态并在服务器和客户端之间传递它。
  • 此应用是任何 OIDC 身份验证流的起点。 OIDC 在应用中手动配置,不依赖于 Microsoft Entra IDMicrosoft Identity Web 包,示例应用也不需要 Microsoft Azure 托管。 不过,示例应用可以与 Entra、Microsoft Identity Web 一起使用,并托管在 Azure 中。
  • 自动非交互式令牌刷新。
  • 采用服务于前端的后端 (BFF) 模式,使用 .NET Aspire 进行服务发现,使用 YARP 将请求代理到后端应用上的天气预报终结点。
    • 后端 Web API 使用 JWT 持有者身份验证来验证登录 cookie 中的 Blazor Web 应用保存的 JWT 令牌。
    • Aspire 改善了生成 .NET 云原生应用的体验。 它提供了一套一致的、固定的工具和模式,用于生成和运行分布式应用。
    • YARP (Yet Another Reverse Proxy) 是一个用于创建反向代理服务器的库。

预览包警告

警告

本文中描述的由 BlazorWebAppOidcBff 示例应用使用的技术和包目前为预览版。 目前不支持本文的内容、API 和示例应用,不建议将其用于生产。 示例应用和指南如有更改,恕不另行通知。

先决条件

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

示例应用

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

  • .NET Aspire:
    • Aspire.AppHost:用于管理应用的高级编排问题。
    • Aspire.ServiceDefaults:包含可根据需要进行扩展和自定义的默认 .NET Aspire 应用配置。
  • MinimalApiJwt:后端 Web API,包含天气数据的示例最小 API 终结点。
  • BlazorWebAppOidc:Blazor Web 应用的服务器端项目。
  • BlazorWebAppOidc.Client:Blazor Web 应用的客户端项目。

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

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

.NET Aspire 项目

若要详细了解如何使用 .NET Aspire 以及详细了解示例应用的 .AppHost.ServiceDefaults 项目,请参阅 .NET Aspire 文档

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

服务器端 Blazor Web 应用项目 (BlazorWebAppOidc)

BlazorWebAppOidc 项目是 Blazor Web 应用的服务器端项目。 该项目使用 YARP 来代理对后端 Web API 项目 (MinimalApiJwt) 中的天气预报终结点发出的请求,并将 access_token 存储在身份验证 cookie 中。

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

注意

服务器项目使用 IHttpContextAccessor/HttpContext,但始终不会用于交互式呈现的组件。 有关详细信息,请参阅 ASP.NET Core Blazor 交互式服务器端呈现的威胁缓解指南

配置

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

注意

对于 Microsoft Entra ID 和 Azure AD B2C,你可以使用 Microsoft Identity Web 中的 AddMicrosoftIdentityWebAppMicrosoft.Identity.Web NuGet 包API 文档),这会添加具有适当默认值的 OIDC 和 Cookie 身份验证处理程序。 本部分中的示例应用和指南不使用 Microsoft Identity Web。 该指南演示了如何为任何 OIDC 提供程序手动配置 OIDC 处理程序。 若要详细了解如何实现 Microsoft Identity Web,请参阅链接的资源。

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

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

    oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    
  • openidprofile 的范围 (Scope)(可选):默认情况下也会配置 openidprofile 范围,因为 OIDC 处理程序需要它们才能运行,但可能需要重新添加这些范围(如果 Authentication:Schemes:MicrosoftOidc:Scope 配置中包含范围)。 有关常规配置指南,请参阅 ASP.NET Core 中的配置ASP.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 范围是在 Azure 或 Entra 门户中的“公开 API”下配置的。 这对于后端 Web API 项目 (MinimalApiJwt) 使用持有者 JWT 来验证访问令牌是必要的。

    oidcOptions.Scope.Add("{APP ID URI}/{API NAME}");
    

    示例:

    • App ID URI ({APP ID URI}):https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}
      • 目录名称 ({DIRECTORY NAME}):contoso
      • 应用程序(客户端)ID ({CLIENT ID}):4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f
    • 为来自 MinimalApiJwt ({API NAME}) 的天气数据配置的范围:Weather.Get
    oidcOptions.Scope.Add("https://contoso.onmicrosoft.com/4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f/Weather.Get");
    

    前面的示例与在 AAD B2C 租户类型的租户中注册的应用相关。 如果应用在 ME-ID 租户中注册,则应用 ID URI 不同,因此范围不同。

    示例:

    • 应用 ID URI ({APP ID URI}):api://{CLIENT ID},应用程序(客户端)ID ({CLIENT ID}):4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f
    • 为来自 MinimalApiJwt ({API NAME}) 的天气数据配置的范围:Weather.Get
    oidcOptions.Scope.Add("api://4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f/Weather.Get");
    
  • AuthorityClientId:设置 OIDC 调用的颁发机构和客户端 ID。

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

    示例:

    • 颁发机构 ({AUTHORITY}):https://login.microsoftonline.com/a3942615-d115-4eb7-bc84-9974abcf5064/v2.0/(使用租户 ID a3942615-d115-4eb7-bc84-9974abcf5064
    • 客户端 ID ({CLIENT ID}):4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f
    oidcOptions.Authority = "https://login.microsoftonline.com/a3942615-d115-4eb7-bc84-9974abcf5064/v2.0/";
    oidcOptions.ClientId = "4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f";
    

    Microsoft Azure“通用”颁发机构示例:

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

    oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";
    
  • ClientSecret:OIDC 客户端机密。

    以下示例仅用于测试和演示目的。 不要将客户端机密存储在应用的程序集中,也不要将机密签入源代码管理中。 将客户端机密存储在用户机密Azure Key Vault环境变量中。

    身份验证方案配置自动从 builder.Configuration["Authentication:Schemes:{SCHEME NAME}:{PropertyName}"] 读取,其中的 {SCHEME NAME} 占位符是方案,默认为 MicrosoftOidc。 由于配置是预先配置的,因此可以通过 Authentication:Schemes:MicrosoftOidc:ClientSecret 配置密钥自动读取客户端机密。 在使用环境变量的服务器上,将环境变量命名为 Authentication__Schemes__MicrosoftOidc__ClientSecret

    set Authentication__Schemes__MicrosoftOidc__ClientSecret={CLIENT SECRET}
    

    可以直接设置 ClientSecret仅用于演示和测试)。 不要直接为已部署的生产应用设置值。 若要稍微提高安全性,请有条件地编译DEBUG 符号的行:

    #if DEBUG
    oidcOptions.ClientSecret = "{CLIENT SECRET}";
    #endif
    

    示例:

    客户端机密 ({CLIENT SECRET}):463471c8c4...f90d674bc9(为了显示而进行了缩短)

    #if DEBUG
    oidcOptions.ClientSecret = "463471c8c4...137f90d674bc9";
    #endif
    
  • ResponseType:将 OIDC 处理程序配置为仅执行授权代码流。 在这种模式下,隐式授权和混合流是不必要的。

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

    oidcOptions.ResponseType = OpenIdConnectResponseType.Code;
    
  • MapInboundClaims 以及 NameClaimTypeRoleClaimType 的配置:许多 OIDC 服务器使用“name”和“role”,而不使用 ClaimTypes 中的 SOAP/WS-Fed 默认值。 当 MapInboundClaims 设置为 false 时,处理程序不执行声明映射,应用直接使用 JWT 中的声明名称。 以下示例手动映射名称和角色声明:

注意

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

oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Name;
oidcOptions.TokenValidationParameters.RoleClaimType = "role";
  • 路径配置:路径必须与向 OIDC 提供程序注册应用程序时配置的重定向 URI 路径(登录回叫路径)和退出登录后重定向路径(退出登录回叫路径)匹配。 在 Microsoft Azure 门户中,路径是在应用注册的“身份验证”边栏选项卡中配置的。 登录路径和退出登录路径都必须注册为重定向 URI。 默认值为 /signin-oidc/signout-callback-oidc

    • CallbackPath:应用的基路径内的请求路径,将在其中返回用户代理。

      在 Entra 或 Microsoft Azure 门户中,设置 Web 平台配置的“重定向 URI”中的路径:

      https://localhost/signin-oidc

      注意

      localhost 地址不需要端口。

    • SignedOutCallbackPath:应用基路径内的请求路径,在其中,从标识提供者退出登录后会返回用户代理。

      在 Entra 或 Microsoft Azure 门户中,设置 Web 平台配置的“重定向 URI”中的路径:

      https://localhost/signout-callback-oidc

      注意

      localhost 地址不需要端口。

      注意

      如果使用 Microsoft Identity Web,则提供者当前仅在使用 microsoftonline.com 颁发机构 (https://login.microsoftonline.com/{TENANT ID}/v2.0/) 时重定向回 SignedOutCallbackPath。 如果你可以使用 Microsoft Identity Web 的“通用”颁发机构,则不存在此限制。 有关详细信息,请参阅当颁发机构 URL 包含租户 ID 时 postLogoutRedirectUri 不起作用 (AzureAD/microsoft-authentication-library-for-js #5783)

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

      在 Entra 或 Microsoft Azure 门户中,设置“前端通道退出登录 URL”:

      https://localhost/signout-oidc

      注意

      localhost 地址不需要端口。

    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 提供程序都使用默认颁发者验证器,但我们需要考虑使用由 https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration 返回的租户 ID ({TENANT ID}) 参数化的颁发者。 有关详细信息,请参阅 OpenID Connect 和 Azure AD“通用”终结点的 SecurityTokenInvalidIssuerException (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;
    

示例应用代码

检查示例应用的以下功能:

  • 借助自定义 cookie 刷新器 (CookieOidcRefresher.cs) 进行的自动非交互式令牌刷新。
  • PersistingAuthenticationStateProvider 类 (PersistingAuthenticationStateProvider.cs) 是服务器端 AuthenticationStateProvider,它使用 PersistentComponentState 将身份验证状态传输到客户端,然后该状态在 WebAssembly 应用程序的生存期内得到修复。
  • 对 Blazor Web 应用的请求将被代理到后端 Web API 项目 (MinimalApiJwt)。 Program 文件中的 MapForwarder 添加了使用传出请求的默认配置、自定义转换和默认 HTTP 客户端将与指定模式匹配的 HTTP 请求直接转发到特定目标的功能:
    • 在服务器上呈现 Weather 组件时,该组件使用 ServerWeatherForecaster 通过用户的访问令牌代理天气数据请求。
    • 在客户端上呈现该组件时,该组件使用 ClientWeatherForecaster 服务实现,而该实现使用预配置的 HttpClient(在客户端项目的 Program 文件中)对服务器项目进行 Web API 调用。 服务器项目的 Program 文件中定义的最小 API 终结点 (/weather-forecast) 使用用户的访问令牌转换请求以获取天气数据。

如需详细了解在 Blazor Web 应用中使用服务抽象的 (Web) API 调用,请参阅从 ASP.NET Core Blazor 应用调用 Web API

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

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

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

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

示例应用仅提供用于显示目的的用户名和电子邮件。 它不包括在发出后续请求时向服务器进行身份验证的令牌,它是使用 cookie 单独工作的(其包含在对服务器的 HttpClient 请求中)。

后端 Web API 项目 (MinimalApiJwt)

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

配置

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

  • Audience:设置任何收到的 OpenID Connect 令牌的“受众”。

    在 Azure 或 Entra 门户中:将该值与在“公开 API”下添加 Weather.Get 范围时配置的“应用程序 ID URI”的路径匹配:

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

    示例:

    App ID URI ({APP ID URI}):https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}

    • 目录名称 ({DIRECTORY NAME}):contoso
    • 应用程序(客户端)ID ({CLIENT ID}):4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f
    jwtOptions.Audience = "https://contoso.onmicrosoft.com/4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f";
    

    前面的示例与在 AAD B2C 租户类型的租户中注册的应用相关。 如果应用在 ME-ID 租户中注册,则应用 ID URI 不同,因此受众不同。

    示例:

    应用 ID URI ({APP ID URI}):api://{CLIENT ID},应用程序(客户端)ID ({CLIENT ID}):4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f

    jwtOptions.Audience = "api://4ba4de56-9cef-45d9-83fa-a4c18f9f5f0f";
    
  • Authority:设置用于进行 OpenID Connect 调用的颁发机构。 将值与在 BlazorWebAppOidc/Program.cs 中为 OIDC 处理程序配置的颁发机构进行匹配:

    jwtOptions.Authority = "{AUTHORITY}";
    

    示例:

    颁发机构 ({AUTHORITY}):https://login.microsoftonline.com/a3942615-d115-4eb7-bc84-9974abcf5064/v2.0/(使用租户 ID a3942615-d115-4eb7-bc84-9974abcf5064

    jwtOptions.Authority = "https://login.microsoftonline.com/a3942615-d115-4eb7-bc84-9974abcf5064/v2.0/";
    

    前面的示例与在 AAD B2C 租户类型的租户中注册的应用相关。 如果应用在 ME-ID 租户中注册,则颁发机构应与标识提供者返回的 JWT 的颁发者 (iss) 匹配:

    jwtOptions.Authority = "https://sts.windows.net/a3942615-d115-4eb7-bc84-9974abcf5064/";
    

天气数据的最小 API

项目的 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] 属性添加到控制器或操作中。

在注销时重定向到主页

当用户在应用中导航时,LogInOrOut 组件 (Layout/LogInOrOut.razor) 会将返回 URL (ReturnUrl) 的隐藏字段设置为当前 URL (currentURL) 的值。 当用户注销应用时,标识提供者会使其返回到他们注销的页面。

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

以下示例演示了 LogInOrOut 组件的重要更改。 ReturnUrl 的隐藏字段的 value 已设置为位于 / 的主页。 不再实现 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 />
                <input type="hidden" name="ReturnUrl" value="/" />
                <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>

加密 nonce

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

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

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

疑难解答

日志记录

服务器应用是一个标准 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 应用进行测试和故障排除时,请确保在服务器项目中运行应用。

检查用户

可直接在应用中使用以下 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.Count() > 0)
{
    <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;
    }
}

其他资源