使用 OpenID Connect (OIDC) 保护 ASP.NET Core Blazor Web App
注意
此版本不是本文的最新版本。 对于当前版本,请参阅本文的 .NET 9 版本。
本文介绍如何使用 GitHub 存储库中的示例应用(.NET 8 或更高版本),通过 OpenID Connect (OIDC) 来保护 Blazor Web App(如何下载)。
本版本的文章介绍了如何实现 OIDC,而无需采用 前端服务器架构(BFF)模式,同时使用了采用全球交互式自动渲染(包括服务器和 .Client
项目)的应用程序。 BFF 模式是一种用于向外部服务发出经过身份验证请求的设计模式。 如果应用的规范要求采用 BFF 模式,请将文章版本选择器更改为“采用 BFF 模式的 OIDC”。
涵盖以下规范:
- Blazor Web App 使用具有全局交互性的自动呈现模式。
- 服务器和客户端应用使用自定义身份验证状态提供程序服务来捕获用户的身份验证状态并在服务器和客户端之间传递它。
- 此应用是任何 OIDC 身份验证流的起点。 OIDC 在应用中手动配置,不依赖于 Microsoft Entra ID 或 Microsoft Identity Web 包,示例应用也不需要 Microsoft Azure 托管。 不过,示例应用可以与 Entra、Microsoft Identity Web 一起使用,并托管在 Azure 中。
- 自动非交互式令牌刷新。
- 安全地调用服务器项目中的 (Web) API 以获取数据。
有关使用适用于 .NET 的 Microsoft 身份验证库、Microsoft Identity Web 和 Microsoft Entra ID 的替代体验,请参阅使用 Microsoft Entra ID 保护 ASP.NET Core Blazor Web App。
示例应用
此示例应用包含两个项目:
BlazorWebAppOidc
:Blazor Web App 的服务器端项目,包含天气数据示例的Minimal API 端点。-
BlazorWebAppOidc.Client
:Blazor Web App 的客户端项目。
使用以下链接通过存储库根目录中的最新版本文件夹访问示例应用。 这些项目位于 .NET 8 或更高版本的 BlazorWebAppOidc
文件夹中。
服务器端 Blazor Web App 项目 (BlazorWebAppOidc
)
BlazorWebAppOidc
项目是 Blazor Web App 的服务器端项目。
BlazorWebAppOidc.http
文件可用于测试天气数据请求。 请注意,BlazorWebAppOidc
项目必须正在运行以测试终结点,并且终结点已硬编码到文件中。 有关详细信息,请参阅在 Visual Studio 2022 中使用 .http 文件。
配置
本部分介绍如何配置示例应用。
注意
对于 Microsoft Entra ID 或 Azure AD B2C,可以从 Microsoft Identity Web(Microsoft.Identity.Web
NuGet 包、API 文档)使用 AddMicrosoftIdentityWebApp,这将以适当的默认设置添加 OIDC 和 Cookie 身份验证处理程序。 本部分中的示例应用和指南不使用 Microsoft Identity Web。 该指南演示了如何为任何 OIDC 提供程序手动配置 OIDC 处理程序。 若要详细了解如何实现 Microsoft Identity Web,请参阅链接的资源。
建立客户端密码
警告
不要在客户端代码中存储应用机密、连接字符串、凭据、密码、个人标识号(PIN)、专用 C#/.NET 代码或私钥/令牌,这始终不安全。 在测试/暂存和生产环境中,服务器端 Blazor 代码和 Web API 应使用安全身份验证流,以避免在项目代码或配置文件中维护凭据。 在本地开发测试之外,我们建议避免使用环境变量来存储敏感数据,因为环境变量不是最安全的方法。 对于本地开发测试, 建议使用机密管理器工具 来保护敏感数据。 有关详细信息,请参阅 安全维护敏感数据和凭据。
对于本地开发测试,请使用 机密管理器工具 将服务器应用的客户端机密存储在配置密钥 Authentication:Schemes:MicrosoftOidc:ClientSecret
下。
注意
如果应用使用 Microsoft Entra ID 或 Azure AD B2C,请在 Entra 或 Azure 门户的应用注册中,选择管理>证书和机密>新客户端机密,并创建一个客户端机密。 在以下指南中,使用新机密的值。
示例应用尚未为机密管理器工具初始化。 使用命令 shell(如 Visual Studio 中的开发人员 PowerShell 命令 shell)执行以下命令。 在执行命令之前,请将包含 cd
该命令的目录更改为服务器项目的目录。 该命令将建立用户机密标识符(<UserSecretsId>
在服务器应用的项目文件中):
dotnet user-secrets init
执行以下命令以设置客户端密码。 {SECRET}
占位符是应用注册时获得的客户端密钥。
dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SECRET}"
如果使用 Visual Studio,可以通过右键单击解决方案资源管理器中的服务器项目并选择“管理用户机密”来确认是否已设置机密。
配置应用
调用 OpenIdConnectOptions 时,可以在项目的 Program
文件中找到以下 AddOpenIdConnect 配置:
SignInScheme:设置与身份验证成功后负责保留用户标识的中间件对应的身份验证方案。 OIDC 处理程序需要使用能够跨请求保留用户凭据的登录方案。 以下行仅用于演示目的。 如果省略,则 DefaultSignInScheme 用作回退值。
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
openid
和profile
的范围 (Scope)(可选):默认情况下也会配置openid
和profile
范围,因为 OIDC 处理程序需要它们才能运行,但可能需要重新添加这些范围(如果Authentication:Schemes:MicrosoftOidc:Scope
配置中包含范围)。 有关常规配置指南,请参阅 ASP.NET Core 中的配置和 ASP.NET Core Blazor 配置。oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
SaveTokens:定义在授权成功后,是否应在 AuthenticationProperties 中存储访问令牌和刷新令牌。 此属性设置为
true
,以便存储非交互式令牌刷新的刷新令牌。oidcOptions.SaveTokens = true;
脱机访问范围 (Scope):刷新令牌需要
offline_access
范围。oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);
Authority 和 ClientId:设置 OIDC 调用的颁发机构和客户端 ID。
oidcOptions.Authority = "{AUTHORITY}"; oidcOptions.ClientId = "{CLIENT ID}";
示例:
- 颁发机构 (
{AUTHORITY}
):https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/
(使用租户 IDaaaabbbb-0000-cccc-1111-dddd2222eeee
) - 客户端 ID (
{CLIENT 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";
Microsoft Azure “common” 授权示例:
“通用权限”应用于多租户应用。 还可以对单租户应用使用“通用”颁发机构,但需要自定义 IssuerValidator,如本部分后面所示。
oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";
- 颁发机构 (
ResponseType:将 OIDC 处理程序配置为仅执行授权代码流。 在这种模式下,隐式授权和混合流是不必要的。 OIDC 处理程序使用从授权终结点返回的代码自动请求适当的令牌。
oidcOptions.ResponseType = OpenIdConnectResponseType.Code;
注意
在 Entra 或 Azure 门户的“隐式授权和混合流”应用注册配置中,不要选择允许授权终结点返回“访问令牌”或“ID 令牌”的任一复选框。
MapInboundClaims 以及 NameClaimType 和 RoleClaimType 的配置:许多 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 路径(登录回叫路径)和退出登录后重定向路径(退出登录回叫路径)匹配。 在 Microsoft 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 处理程序会重定向到 SignedOutRedirectUri 或 RedirectUri(如果指定)。在应用的 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 门户中设置前通道注销 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 提供程序都使用默认颁发者验证器,但我们需要考虑使用由
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
) 进行的自动非交互式令牌刷新。 - 服务器项目调用 AddAuthenticationStateSerialization 来添加一个服务器端身份验证状态提供程序,该提供程序使用 PersistentComponentState 将身份验证状态传递给客户端。 客户端调用 AddAuthenticationStateDeserialization 反序列化并使用服务器传递的身份验证状态。 身份验证状态在 WebAssembly 应用程序的生存期内是固定的。
- 向 Blazor Web App 请求天气数据的示例请求由
/weather-forecast
文件 (Program
) 中的最小 API 终结点 (Program.cs
) 处理。 终结点要求通过调用 RequireAuthorization 进行授权。 对于添加到项目中的任何控制器,请将[Authorize]
属性添加到控制器或操作中。 有关通过 授权策略 实现应用内授权要求,并在部分公共终结点选择退出授权的详细信息,请参阅 Razor Pages OIDC 指南。 - 该应用安全地调用服务器项目中的 (Web) API 来获取天气数据:
- 在服务器上呈现
Weather
组件时,该组件使用服务器上的ServerWeatherForecaster
直接获取天气数据(而不是通过 Web API 调用)。 - 在客户端上呈现该组件时,该组件使用
ClientWeatherForecaster
服务实现,而该实现使用预配置的 HttpClient(在客户端项目的Program
文件中)对服务器项目进行 Web API 调用。 服务器项目的/weather-forecast
文件中定义的最小 API 终结点 (Program
) 从ServerWeatherForecaster
获取天气数据,并将数据返回到客户端。
- 在服务器上呈现
- 通过自定义 cookie 刷新器 (
CookieOidcRefresher.cs
) 实现的自动非交互式令牌刷新。 PersistingAuthenticationStateProvider
类 (PersistingAuthenticationStateProvider.cs
) 是服务器端 AuthenticationStateProvider,它使用 PersistentComponentState 将身份验证状态传输到客户端,然后该状态在 WebAssembly 应用程序的生存期内得到修复。- 向 Blazor Web App 请求天气数据的示例请求由
/weather-forecast
文件 (Program
) 中的最小 API 终结点 (Program.cs
) 处理。 终结点要求通过调用 RequireAuthorization 进行授权。 对于添加到项目中的任何控制器,请将[Authorize]
属性添加到控制器或操作中。 - 该应用安全地调用服务器项目中的 (Web) API 来获取天气数据:
- 在服务器上呈现
Weather
组件时,该组件使用服务器上的ServerWeatherForecaster
直接获取天气数据(而不是通过 Web API 调用)。 - 在客户端上呈现该组件时,该组件使用
ClientWeatherForecaster
服务实现,而该实现使用预配置的 HttpClient(在客户端项目的Program
文件中)对服务器项目进行 Web API 调用。 服务器项目的/weather-forecast
文件中定义的最小 API 终结点 (Program
) 从ServerWeatherForecaster
获取天气数据,并将数据返回到客户端。
- 在服务器上呈现
有关在 Blazor Web App 中使用服务抽象的 (Web) API 调用的详细信息,请参阅从 ASP.NET Core Blazor 应用调用 Web API。
客户端 Blazor Web App 项目 (BlazorWebAppOidc.Client
)
BlazorWebAppOidc.Client
项目是 Blazor Web App 的客户端项目。
客户端调用 AddAuthenticationStateDeserialization 反序列化并使用服务器传递的身份验证状态。 身份验证状态在 WebAssembly 应用程序的生存期内是固定的。
PersistentAuthenticationStateProvider
类 (PersistentAuthenticationStateProvider.cs
) 是客户端组件,通过在服务器上呈现页面时查找页面中保留的数据来确定用户的身份验证状态。 身份验证状态在 WebAssembly 应用程序的生存期内是固定的。
如果用户需要登录或退出登录,则需要重新加载整个页面。
示例应用仅提供用于显示目的的用户名和电子邮件。 它不包括在发出后续请求时向服务器进行身份验证的令牌,它是使用 cookie 单独工作的(其包含在对服务器的 HttpClient 请求中)。
本版本的文章介绍了如何在不采用 Backend for Frontend (BFF) 模式的情况下实现 OIDC,并使用采用全局交互式服务器呈现(单个项目)的应用。 BFF 模式很适合用于向外部服务发出经过验证的请求。 如果应用的规范要求采用全局交互式自动渲染的 BFF 模式,请将文章版本选择器更改为具有 BFF 模式的 OIDC。
涵盖以下规范:
- Blazor Web App 使用具有全局交互的服务器呈现模式。
- 此应用是任何 OIDC 身份验证流的起点。 OIDC 在应用中手动配置,不依赖于 Microsoft Entra ID 或 Microsoft Identity Web 包,示例应用也不需要 Microsoft Azure 托管。 不过,示例应用可以与 Entra、Microsoft Identity Web 一起使用,并托管在 Azure 中。
- 自动非交互式令牌刷新。
有关使用适用于 .NET 的 Microsoft 身份验证库、Microsoft Identity Web 和 Microsoft Entra ID 的替代体验,请参阅使用 Microsoft Entra ID 保护 ASP.NET Core Blazor Web App。
示例应用
示例应用由单个服务器端 Blazor Web App 项目 (BlazorWebAppOidcServer
) 组成。
使用以下链接通过存储库根目录中的最新版本文件夹访问示例应用。 该项目位于 .NET 8 或更高版本的 BlazorWebAppOidcServer
文件夹中。
配置
本部分介绍如何配置示例应用。
注意
对于 Microsoft Entra ID 或 Azure AD B2C,可以使用 AddMicrosoftIdentityWebAppMicrosoft Identity Web(Microsoft.Identity.Web
NuGet 包、API 文档),该包会使用相应的默认值添加 OIDC 和 Cookie 身份验证处理程序。 本部分中的示例应用和指南不使用 Microsoft Identity Web。 该指南演示了如何为任何 OIDC 提供程序手动配置 OIDC 处理程序。 若要详细了解如何实现 Microsoft Identity Web,请参阅链接的资源。
建立客户端密码
警告
不要在客户端代码中存储应用机密、连接字符串、凭据、密码、个人标识号(PIN)、专用 C#/.NET 代码或私钥/令牌,这始终不安全。 在测试/暂存和生产环境中,服务器端 Blazor 代码和 Web API 应使用安全身份验证流,以避免在项目代码或配置文件中维护凭据。 在本地开发测试之外,我们建议避免使用环境变量来存储敏感数据,因为环境变量不是最安全的方法。 对于本地开发测试, 建议使用机密管理器工具 来保护敏感数据。 有关详细信息,请参阅 安全维护敏感数据和凭据。
对于本地开发测试,请使用机密管理器工具将应用的客户端机密存储在配置密钥 Authentication:Schemes:MicrosoftOidc:ClientSecret
下。
注意
如果应用使用 Microsoft Entra ID 或 Azure AD B2C,请在 Entra 或 Azure 门户中应用注册的管理>证书和机密>新建客户端机密部分创建一个客户端机密。 在以下指南中,使用新机密的值。
机密管理工具尚未初始化示例应用程序。 使用命令 shell(如 Visual Studio 中的开发人员 PowerShell 命令 shell)执行以下命令。 在执行命令之前,请将 cd
命令的目录更改为项目的目录。 该命令将建立用户机密标识符(在应用的项目文件中的 <UserSecretsId>
):
dotnet user-secrets init
执行以下命令以设置客户端密码。 占位符 {SECRET}
是从应用注册中获取的客户端密钥。
dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SECRET}"
如果使用 Visual Studio,可以通过右键单击解决方案资源管理器中的项目并选择管理用户机密来确认机密是否已设置。
配置应用
调用 OpenIdConnectOptions 时,可以在项目的 Program
文件中找到以下 AddOpenIdConnect 配置:
SignInScheme:设置与身份验证成功后负责保留用户标识的中间件对应的身份验证方案。 OIDC 处理程序需要使用能够跨请求保留用户凭据的登录方案。 以下行仅用于演示目的。 如果省略,则 DefaultSignInScheme 用作回退值。
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
openid
和profile
的范围 (Scope)(可选):默认情况下也会配置openid
和profile
范围,因为 OIDC 处理程序需要它们才能运行,但可能需要重新添加这些范围(如果Authentication:Schemes:MicrosoftOidc:Scope
配置中包含范围)。 有关常规配置指南,请参阅 ASP.NET Core 中的配置和 ASP.NET Core Blazor 配置。oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
SaveTokens:定义在授权成功后,是否应在 AuthenticationProperties 中存储访问令牌和刷新令牌。 此属性设置为
true
,以便存储非交互式令牌刷新的刷新令牌。oidcOptions.SaveTokens = true;
脱机访问范围 (Scope):刷新令牌需要
offline_access
范围。oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);
Authority 和 ClientId:设置 OIDC 调用的颁发机构和客户端 ID。
oidcOptions.Authority = "{AUTHORITY}"; oidcOptions.ClientId = "{CLIENT ID}";
示例:
- 颁发机构 (
{AUTHORITY}
):https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/
(使用租户 IDaaaabbbb-0000-cccc-1111-dddd2222eeee
) - 客户端 ID (
{CLIENT 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";
Microsoft Azure“通用”权限示例:
“通用授权”应该用于多租户应用。 你也可以对单租户应用使用“通用”权限,但需要自定义 IssuerValidator,如本部分后面所示。
oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";
- 颁发机构 (
ResponseType:将 OIDC 处理程序配置为仅执行授权代码流。 在这种模式下,隐式授权和混合流是不必要的。 OIDC 处理程序使用从授权终结点返回的代码自动请求适当的令牌。
oidcOptions.ResponseType = OpenIdConnectResponseType.Code;
注意
在 Entra 或 Azure 门户的“隐式授权和混合流”应用注册配置中,不要勾选任何一个与授权终结点相关的复选框,该复选框用于返回“访问令牌”或“ID 令牌”。
MapInboundClaims 以及 NameClaimType 和 RoleClaimType 的配置:许多 OIDC 服务器使用“
name
”和“role
”,而不使用 ClaimTypes 中的 SOAP/WS-Fed 默认值。 当 MapInboundClaims 设置为false
时,处理程序不执行声明映射,应用直接使用 JWT 中的声明名称。 以下示例将角色声明类型设置为“roles
”,该类型适用于 Microsoft Entra ID (ME-ID)。 有关详细信息,请参阅标识提供者的文档。注意
MapInboundClaims 在大多数 OIDC 提供程序中必须设置为
false
,以防止重命名声明。oidcOptions.MapInboundClaims = false; oidcOptions.TokenValidationParameters.NameClaimType = "name"; oidcOptions.TokenValidationParameters.RoleClaimType = "roles";
路径配置:路径必须与向 OIDC 提供程序注册应用程序时配置的重定向 URI 路径(登录回叫路径)和退出登录后重定向路径(退出登录回叫路径)匹配。 在 Microsoft 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 处理程序会重定向到 SignedOutRedirectUri 或 RedirectUri(如果指定)。在应用的 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 门户中设置前通道注销 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 提供程序都使用默认颁发者验证器,但我们需要考虑使用由
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
) 进行的自动非交互式令牌刷新。 Weather
组件使用[Authorize]
属性来防止未经授权的访问。 有关通过 授权策略 实现应用内授权要求,并在部分公共终结点选择退出授权的详细信息,请参阅 Razor Pages OIDC 指南。
本文的这个版本介绍了如何在使用服务于前端的后端 (BFF) 模式的情况下实现 OIDC。 如果应用的规范没有要求采用 BFF 模式,请将文章版本选择器更改为“没有采用 BFF 模式的 OIDC”。
涵盖以下规范:
- Blazor Web App 使用具有全局交互性的自动呈现模式。
- 服务器和客户端应用使用自定义身份验证状态提供程序服务来捕获用户的身份验证状态并在服务器和客户端之间传递它。
- 此应用是任何 OIDC 身份验证流的起点。 OIDC 在应用中手动配置,不依赖于 Microsoft Entra ID 或 Microsoft Identity Web 包,示例应用也不需要 Microsoft Azure 托管。 不过,示例应用可以与 Entra、Microsoft Identity Web 一起使用,并托管在 Azure 中。
- 自动非交互式令牌刷新。
- 采用服务于前端的后端 (BFF) 模式,使用 .NET Aspire 进行服务发现,使用 YARP 将请求代理到后端应用上的天气预报终结点。
- 后端 Web API 使用 JWT 持有者身份验证来验证登录 Blazor Web App 中的 cookie 保存的 JWT 令牌。
- Aspire 改善了生成 .NET 云原生应用的体验。 它提供了一套一致的、固定的工具和模式,用于生成和运行分布式应用。
- YARP (Yet Another Reverse Proxy) 是一个用于创建反向代理服务器的库。
有关 .NET Aspire 的详细信息,请参阅 .NET Aspire 正式发布:简化 .NET 云原生开发(2024 年 5 月)。
先决条件
.NET Aspire 需要 17.10 版或更高版本的 Visual Studio。
示例应用
此示例应用包含五个项目:
- .NET Aspire:
Aspire.AppHost
:用于管理应用的高级编排问题。Aspire.ServiceDefaults
:包含可根据需要进行扩展和自定义的默认 .NET Aspire 应用配置。
MinimalApiJwt
:后台 Web API,包含一个用于天气数据的Minimal API示例端点。BlazorWebAppOidc
:Blazor Web App 的服务器端项目。-
BlazorWebAppOidc.Client
:Blazor Web App 的客户端项目。
使用以下链接通过存储库根目录中的最新版本文件夹访问示例应用。 这些项目位于 .NET 8 或更高版本的 BlazorWebAppOidcBff
文件夹中。
.NET Aspire 项目
若要详细了解如何使用 .NET Aspire 以及详细了解示例应用的 .AppHost
和 .ServiceDefaults
项目,请参阅 .NET Aspire 文档。
确认已满足 .NET Aspire 的先决条件。 有关详细信息,请参阅
示例应用仅配置一个不安全的 HTTP 启动配置文件 (http
),供在开发测试期间使用。 有关详细信息,包括不安全和安全启动设置配置文件的示例,请参阅 在 .NET Aspire 中允许不安全传输(.NET Aspire 文档)。
服务器端 Blazor Web App 项目 (BlazorWebAppOidc
)
BlazorWebAppOidc
项目是 Blazor Web App 的服务器端项目。 该项目使用 YARP 来代理对后端 Web API 项目 (MinimalApiJwt
) 中的天气预报终结点发出的请求,并将 access_token
存储在身份验证 cookie 中。
BlazorWebAppOidc.http
文件可用于测试天气数据请求。 请注意,BlazorWebAppOidc
项目必须正在运行以测试终结点,并且终结点已硬编码到文件中。 有关详细信息,请参阅在 Visual Studio 2022 中使用 .http 文件。
配置
本部分介绍如何配置示例应用。
注意
对于 Microsoft Entra ID 或 Azure AD B2C,可以使用 AddMicrosoftIdentityWebAppMicrosoft Identity Web(Microsoft.Identity.Web
NuGet 包、API 文档),该包会使用相应的默认值添加 OIDC 和 Cookie 身份验证处理程序。 本部分中的示例应用和指南不使用 Microsoft Identity Web。 该指南演示了如何为任何 OIDC 提供程序手动配置 OIDC 处理程序。 若要详细了解如何实现 Microsoft Identity Web,请参阅链接的资源。
建立客户端密码
警告
不要在客户端代码中存储应用机密、连接字符串、凭据、密码、个人标识号(PIN)、专用 C#/.NET 代码或私钥/令牌,这始终不安全。 在测试/暂存和生产环境中,服务器端 Blazor 代码和 Web API 应使用安全身份验证流,以避免在项目代码或配置文件中维护凭据。 在本地开发测试之外,我们建议避免使用环境变量来存储敏感数据,因为环境变量不是最安全的方法。 对于本地开发测试, 建议使用机密管理器工具 来保护敏感数据。 有关详细信息,请参阅 安全维护敏感数据和凭据。
对于本地开发测试,请使用 机密管理器工具 将服务器应用的客户端机密存储在配置密钥 Authentication:Schemes:MicrosoftOidc:ClientSecret
下。
注意
如果应用使用 Microsoft Entra ID 或 Azure AD B2C,请在 Entra 或 Azure 门户内的应用注册中,依次选择管理>证书和密码>新建客户端密码以创建新的客户端密码。 在以下指南中,使用新机密的值。
机密管理器工具的示例应用程序尚未初始化。 使用命令 shell(如 Visual Studio 中的开发人员 PowerShell 命令 shell)执行以下命令。 在执行命令之前,请将包含 cd
该命令的目录更改为服务器项目的目录。 该命令将建立用户机密标识符(<UserSecretsId>
在服务器应用的项目文件中):
dotnet user-secrets init
执行以下命令以设置客户端密码。 占位符 {SECRET}
是从应用的注册中获取的客户端密钥。
dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SECRET}"
如果使用 Visual Studio,可以通过右键单击解决方案资源管理器中的服务器项目并选择“管理用户机密”来确认是否已设置机密。
配置应用
调用 OpenIdConnectOptions 时,可以在项目的 Program
文件中找到以下 AddOpenIdConnect 配置:
SignInScheme:设置与身份验证成功后负责保留用户标识的中间件对应的身份验证方案。 OIDC 处理程序需要使用能够跨请求保留用户凭据的登录方案。 以下行仅用于演示目的。 如果省略,则 DefaultSignInScheme 用作回退值。
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
openid
和profile
的范围 (Scope)(可选):默认情况下也会配置openid
和profile
范围,因为 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):后端 Web API 项目 (
MinimalApiJwt
) 需要此项以使用持有者 JWT 来验证访问令牌。oidcOptions.Scope.Add("{APP ID URI}/{API NAME}");
注意
使用 Microsoft Entra ID 时,
Weather.Get
范围在 Azure 或 Entra 门户中的公开 API 下面配置。示例:
- App ID URI (
{APP ID URI}
):https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}
- 目录名称 (
{DIRECTORY NAME}
):contoso
- 应用程序(客户端)ID (
{CLIENT ID}
):00001111-aaaa-2222-bbbb-3333cccc4444
- 目录名称 (
- 为来自
MinimalApiJwt
({API NAME}
) 的天气数据配置的范围:Weather.Get
oidcOptions.Scope.Add("https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444/Weather.Get");
前面的示例与在 AAD B2C 租户类型的租户中注册的应用相关。 如果应用在 ME-ID 租户中注册,则应用 ID URI 不同,因此范围不同。
示例:
- 应用 ID URI (
{APP ID URI}
):api://{CLIENT ID}
,应用程序(客户端)ID ({CLIENT ID}
):00001111-aaaa-2222-bbbb-3333cccc4444
- 为来自
MinimalApiJwt
({API NAME}
) 的天气数据配置的范围:Weather.Get
oidcOptions.Scope.Add("api://00001111-aaaa-2222-bbbb-3333cccc4444/Weather.Get");
- App ID URI (
Authority 和 ClientId:设置 OIDC 调用的授权机构和客户端 ID。
oidcOptions.Authority = "{AUTHORITY}"; oidcOptions.ClientId = "{CLIENT ID}";
示例:
- 颁发机构 (
{AUTHORITY}
):https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/
(使用租户 IDaaaabbbb-0000-cccc-1111-dddd2222eeee
) - 客户端 ID (
{CLIENT 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";
Microsoft Azure“通用”权限示例:
“通用”颁发机构应该用于多租户应用。 还可以对单租户应用程序使用“通用”授权,但需要自定义 IssuerValidator,如本部分后面所示。
oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";
- 颁发机构 (
ResponseType:将 OIDC 处理程序配置为仅执行授权代码流。 在这种模式下,隐式授权和混合流是不必要的。 OIDC 处理程序使用从授权终结点返回的代码自动请求适当的令牌。
oidcOptions.ResponseType = OpenIdConnectResponseType.Code;
注意
使用 Microsoft Entra ID 时,请不要在隐式授权和混合流应用注册配置中选中返回访问令牌或 ID 令牌的授权终结点的复选框。
MapInboundClaims 以及 NameClaimType 和 RoleClaimType 的配置:许多 OIDC 服务器使用“
name
”和“role
”,而不使用 ClaimTypes 中的 SOAP/WS-Fed 默认值。 当 MapInboundClaims 设置为false
时,处理程序不执行声明映射,应用直接使用 JWT 中的声明名称。 以下示例将角色声明类型设置为“roles
”,该类型适用于 Microsoft Entra ID (ME-ID)。 有关详细信息,请参阅标识提供者的文档。注意
MapInboundClaims 必须为大多数 OIDC 提供程序设置为
false
,这会阻止重命名声明。oidcOptions.MapInboundClaims = false; oidcOptions.TokenValidationParameters.NameClaimType = "name"; oidcOptions.TokenValidationParameters.RoleClaimType = "roles";
路径配置:路径必须与向 OIDC 提供程序注册应用程序时配置的重定向 URI 路径(登录回叫路径)和退出登录后重定向路径(退出登录回叫路径)匹配。 在 Microsoft 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 处理程序会重定向到 SignedOutRedirectUri 或 RedirectUri(如果指定)。在应用的 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 门户中设置前通道注销 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 提供程序都使用默认颁发者验证器,但我们需要考虑使用由
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
) 进行的自动非交互式令牌刷新。 - 服务器项目调用 AddAuthenticationStateSerialization 添加服务器端身份验证状态提供程序,该提供程序使用 PersistentComponentState 将身份验证状态传递到客户端。 客户端调用 AddAuthenticationStateDeserialization 反序列化并使用服务器传递的身份验证状态。 身份验证状态在 WebAssembly 应用程序的生存期内是固定的。
- 对 Blazor Web App 的请求被代理到后端 Web API 项目 (
MinimalApiJwt
)。MapForwarder
文件中的Program
添加了使用传出请求的默认配置、自定义转换和默认 HTTP 客户端将与指定模式匹配的 HTTP 请求直接转发到特定目标的功能:- 在服务器上呈现
Weather
组件时,该组件使用ServerWeatherForecaster
类通过用户的访问令牌代理对天气数据的请求。 IHttpContextAccessor.HttpContext 确定 HttpContext 是否可供GetWeatherForecastAsync
方法使用。 有关详细信息,请参阅 ASP.NET Core Razor 组件。 - 在客户端上呈现该组件时,该组件使用
ClientWeatherForecaster
服务实现,而该实现使用预配置的 HttpClient(在客户端项目的Program
文件中)对服务器项目进行 Web API 调用。 在服务器项目的/weather-forecast
文件中定义的最小 API 终结点 (Program
) 使用用户的访问令牌来转换请求,从而获取天气数据。
- 在服务器上呈现
- 借助自定义 cookie 刷新器 (
CookieOidcRefresher.cs
) 实现自动化的非交互式令牌刷新。 PersistingAuthenticationStateProvider
类 (PersistingAuthenticationStateProvider.cs
) 是服务器端 AuthenticationStateProvider,它使用 PersistentComponentState 将身份验证状态传输到客户端,然后该状态在 WebAssembly 应用程序的生存期内得到修复。- 对 Blazor Web App 的请求被代理到后端 Web API 项目 (
MinimalApiJwt
)。MapForwarder
文件中的Program
添加了使用传出请求的默认配置、自定义转换和默认 HTTP 客户端将与指定模式匹配的 HTTP 请求直接转发到特定目标的功能:- 在服务器上呈现
Weather
组件时,该组件使用ServerWeatherForecaster
类通过用户的访问令牌代理对天气数据的请求。 IHttpContextAccessor.HttpContext 确定 HttpContext 是否可供GetWeatherForecastAsync
方法使用。 有关详细信息,请参阅 ASP.NET Core Razor 组件。 - 在客户端上呈现该组件时,该组件使用
ClientWeatherForecaster
服务实现,而该实现使用预配置的 HttpClient(在客户端项目的Program
文件中)对服务器项目进行 Web API 调用。 在服务器项目的Program
文件中定义的最小 API 端点 (/weather-forecast
) 处理包含用户访问令牌的请求,以获取天气数据。
- 在服务器上呈现
有关在 Blazor Web App 中使用服务抽象的 (Web) API 调用的详细信息,请参阅从 ASP.NET Core Blazor 应用调用 Web API。
客户端 Blazor Web App 项目 (BlazorWebAppOidc.Client
)
BlazorWebAppOidc.Client
项目是 Blazor Web App 的客户端项目。
客户端调用 AddAuthenticationStateDeserialization 反序列化并使用服务器传递的身份验证状态。 身份验证状态在 WebAssembly 应用程序的生存期内是固定的。
PersistentAuthenticationStateProvider
类 (PersistentAuthenticationStateProvider.cs
) 是一个客户端组件,通过查找在服务器上呈现页面时保留在页面中的数据来确定用户的身份验证状态。 身份验证状态在 WebAssembly 应用程序的生存期内是固定的。
如果用户需要登录或退出登录,则需要重新加载整个页面。
示例应用仅提供用于显示目的的用户名和电子邮件。 它不包括在发出后续请求时向服务器进行身份验证的令牌,它是使用 cookie 单独工作的(其包含在对服务器的 HttpClient 请求中)。
后端 Web API 项目 (MinimalApiJwt
)
MinimalApiJwt
项目是用于多个前端项目的后端 Web API。 该项目为天气数据配置了一个最小 API 终结点。 来自 Blazor Web App 服务器端项目 (BlazorWebAppOidc
) 的请求会被代理到 MinimalApiJwt
项目。
配置
在项目的 Program
文件中,在 AddJwtBearer 调用的 JwtBearerOptions 中配置项目:
Audience:为收到的任何 OIDC 令牌设置访问群体。
jwtOptions.Audience = "{APP ID URI}";
备注
使用 Microsoft Entra ID 时,请将值仅与在 Azure 或 Entra 门户中公开 API 下面添加
Weather.Get
范围时配置的应用程序 ID URI 的路径匹配。示例:
App ID URI (
{APP ID URI}
):https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}
:- 目录名称 (
{DIRECTORY NAME}
):contoso
- 应用程序(客户端)ID (
{CLIENT ID}
):00001111-aaaa-2222-bbbb-3333cccc4444
jwtOptions.Audience = "https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444";
前面的示例与在 AAD B2C 租户类型的租户中注册的应用相关。 如果应用在 ME-ID 租户中注册,则应用 ID URI 不同,因此受众不同。
示例:
应用 ID URI (
{APP ID URI}
):api://{CLIENT ID}
,应用程序(客户端)ID ({CLIENT ID}
):00001111-aaaa-2222-bbbb-3333cccc4444
jwtOptions.Audience = "api://00001111-aaaa-2222-bbbb-3333cccc4444";
- 目录名称 (
Authority:设置颁发机构以便进行 OIDC 调用。 将值与在
BlazorWebAppOidc/Program.cs
中为 OIDC 处理程序配置的颁发机构进行匹配:jwtOptions.Authority = "{AUTHORITY}";
示例:
颁发机构 (
{AUTHORITY}
):https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/
(使用租户 IDaaaabbbb-0000-cccc-1111-dddd2222eeee
)jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/";
前面的示例与在 AAD B2C 租户类型的租户中注册的应用相关。 如果应用在 ME-ID 租户中注册,则颁发机构应与标识提供者返回的 JWT 的颁发者 (
iss
) 匹配:jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";
天气数据的最小 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
)。 当用户注销应用时,标识提供者会将用户返回到他们注销的页面。如果用户从安全页面注销,则会返回到同一安全页,并通过身份验证过程发送回。 当用户需要定期更改帐户时,这种身份验证流程是合理的。
或者,使用以下 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 @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>
令牌刷新
自定义cookie刷新程序 (CookieOidcRefresher.cs
) 实现会在用户的声明过期时自动更新声明。 当前实现需要从令牌终结点接收 ID 令牌,以换取刷新令牌。 然后,这个 ID 令牌中的声明会用来替换用户的声明。
示例实现不包括在令牌刷新时从 UserInfo 终结点请求声明的代码。 有关详细信息,请参阅 BlazorWebAppOidc AddOpenIdConnect with GetClaimsFromUserInfoEndpoint = true doesn't propogate role claims to client
(dotnet/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) 注册应用的应用程序角色部分。
在 Program.cs
的 OpenIdConnectOptions 中配置角色声明类型 (TokenValidationParameters.RoleClaimType):
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 内置 Administrator 角色。
本节中介绍的方法会将 ME-ID 配置为在身份验证 cookie 标头中发送组和角色。 如果用户只是少数安全组和角色的成员,则以下方法应该适用于大多数托管平台,而不会遇到标头过长的问题,例如,具有默认标头长度限制为 16 KB 的 (MaxRequestBytes
) IIS 托管。 如果标头长度由于组或角色成员身份较高而出现问题,我们建议不要遵循本节中的指导,而是支持实现 Microsoft Graph 单独从 ME-ID 获取用户的组和角色,这种方法不会夸大身份验证 cookie 的大小。 有关详细信息,请参阅错误请求 - 请求太长 - IIS 服务器 (dotnet/aspnetcore
#57545)。
在 Program.cs
的 OpenIdConnectOptions 中配置角色声明类型 (TokenValidationParameters.RoleClaimType)。 将值设置为 roles
:
oidcOptions.TokenValidationParameters.RoleClaimType = "roles";
尽管没有 ME-ID Premium 帐户就无法将角色分配给组,但你可以将角色分配给用户,并为具有标准 Azure 帐户的用户接收角色声明。 本部分中的指南不需要 ME-ID Premium 帐户。
使用默认目录时,请按照将应用角色添加到应用程序并在令牌(ME-ID 文档)中接收这些角色中的指南来配置和分配角色。 如果不使用默认目录,请在Azure 门户中编辑应用的清单,以在清单文件的 appRoles
条目中手动建立应用的角色。 有关详细信息,请参阅配置角色声明(ME-ID 文档)。
用户的 Azure 安全组显示在 groups
声明中,并且用户的内置 ME-ID 管理员角色分配显示在知名 ID (wids
) 声明中。 这两种声明类型的值都是 GUID。 应用收到后,这些声明可用于在 Razor 组件中建立角色和策略授权。
在 Azure 门户的应用清单中,将 groupMembershipClaims
属性设置为 All
。 值 All
会导致 ME-ID 发送已登录用户的所有安全/分配组(groups
声明)和角色(wids
声明)。 若要设置 groupMembershipClaims
特性:
- 打开 Azure 门户中的应用注册。
- 在边栏中选择管理>清单。
- 查找
groupMembershipClaims
属性。 - 将值设置为
All
("groupMembershipClaims": "All"
)。 - 选择“保存”按钮。
将 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 始终会发送值为b79fbf4d-3ef9-4689-8143-76b194e85509
的wids
声明,以便用于租户的非来宾帐户,并且不涉及管理员角色。 在 Azure 门户中,选择“角色和管理员”后,点击省略号 (…),然后选择所列角色的“说明”,将显示管理员角色 GUID(角色模板 ID)。> 角色模板 ID 也列在 Microsoft Entra 内置角色(Entra 文档)中。
故障排除
日志记录
服务器应用是一个标准 ASP.NET Core 应用。 请参阅 ASP.NET Core 记录指南,以便在服务器应用中启用较低的记录级别。
若要为 Blazor WebAssembly 身份验证启用调试或跟踪日志记录,请参阅 ASP.NET Core Blazor 日志记录 的 客户端身份验证日志记录 部分,并将文章版本选择器设置为 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 或服务器应用会向客户端返回一条确切的错误消息或包含线索的消息,其中指出了导致问题的原因。 有关开发人员工具指导,请参阅以下文章:
- Google Chrome(Google 文档)
- Microsoft Edge
- Mozilla Firefox(Mozilla 文档)
文档团队会响应文章中的文档反馈和错误(可以从“此页面”反馈部分提交问题),但无法提供产品技术支持。 可以借助多个公共支持论坛来帮助排查应用问题。 建议如下:
上述论坛并非 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.
若要解决该错误:
- 在 Azure 门户中访问应用的清单。
- 将
allowPublicClient
属性设置为null
或true
。
- 错误:
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
- Microsoft Edge:
- 在“参数”字段中,提供浏览器用来在 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
)。
- Microsoft Edge:请使用
- 在“友好名称”字段中提供名称。 例如
Firefox Auth Testing
。 - 选择“确定”按钮。
- 若要避免在每次迭代使用应用进行测试时必须选择浏览器配置文件,请使用“设置为默认值”按钮将配置文件设置为默认值。
- 请确保浏览器被 IDE 关闭,以便进行对应用、测试用户或提供程序配置的任何更改。
应用升级
正常运行的应用在开发计算机上升级 .NET Core SDK 或在应用内更改包版本后可能会立即出现故障。 在某些情况下,不兼容的包可能在执行重大升级时导致应用程序故障。 可以按照以下说明来修复其中大部分问题:
- 从命令 shell 执行
dotnet nuget locals all --clear
以清空本地系统的 NuGet 包缓存。 - 删除项目的
bin
和obj
文件夹。 - 还原并重建该项目。
- 在重新部署应用前,在服务器上删除部署文件夹中的所有文件。
注意
不支持使用与应用的目标框架不兼容的包版本。 要了解有关包的信息,请使用 NuGet 库。
运行服务器应用
在对 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;
}
}
其他资源
-
AzureAD/microsoft-identity-web
GitHub 存储库:有关为 Microsoft Entra ID 实现 Microsoft Identity Web 以及为 ASP.NET Core 应用实现 Azure Active Directory B2C 的有用指南,其中包括指向示例应用和相关 Azure 文档的链接。 目前,Azure 文档尚未明确解决 Blazor Web App 问题,但 ME-ID 和 Azure 托管的 Blazor Web App 设置和配置与任何 ASP.NET Core Web 应用相同。 -
AuthenticationStateProvider
服务 - 管理 Blazor Web App 中的身份验证状态
- 使用 OIDC 在 Blazor交互式服务器中发出 http 请求期间刷新令牌(
dotnet/aspnetcore
#55213)