使用 Microsoft 帐户保护 ASP.NET Core Blazor WebAssembly 独立应用
注意
此版本不是本文的最新版本。 有关当前版本,请参阅本文的 .NET 9 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
本文介绍如何创建使用带有 Microsoft Entra (ME-ID) 的 Microsoft 帐户进行身份验证的独立 Blazor WebAssembly 应用。
阅读本文后,有关其他安全场景的介绍,请参阅 ASP.NET Core Blazor WebAssembly 其他安全场景。
演练
演练的子节介绍了如何:
- 在 Azure 中创建租户
- 在 Azure 中注册应用
- 创建 Blazor 应用
- 运行应用
在 Azure 中创建租户
按照快速入门:设置租户中的指南操作,在 ME-ID 中创建租户。
在 Azure 中注册应用
注册 ME-ID 应用:
- 在 Azure 门户中导航到“Microsoft Entra ID”。 在边栏中选择“应用注册”。 选择“新建注册”按钮。
- 提供应用的“名称”(例如 Blazor 独立 ME-ID MS 帐户)。
- 在“支持的帐户类型”中,选择“任何组织目录中的帐户(任何 Microsoft Entra ID 目录 - 多租户)”。
- 将“重定向 URI”下拉列表设置为“单页应用程序(SPA)”,并提供以下重定向 URI:
https://localhost/authentication/login-callback
。 如果知道 Azure 默认主机(例如azurewebsites.net
)或自定义域主机(例如contoso.com
)的生产重定向 URI,还可以在提供localhost
重定向 URI 的同时添加添加生产重定向 URI。 请确保在添加的任何生产重定向 URI 中包含非:443
端口的端口号。 - 如果使用未经验证的发布者域,请清除“权限”>“授予对 openid 和 offline_access 权限的管理员同意”复选框。 如果验证了发布者域,则不会出现此复选框。
- 选择“注册”。
注意
不需要为 localhost
ME-ID 重定向 URI 提供端口号。 有关详细信息,请参阅重定向 URI(回复 URL)限制和局限:Localhost 异常(Entra 文档)。
记录应用程序(客户端)ID(例如 00001111-aaaa-2222-bbbb-3333cccc4444
)。
在“身份验证”>“平台配置”>“单页应用程序”中:
- 确认存在
https://localhost/authentication/login-callback
的重定向 URI。 - 在“隐式授权”部分中,请确保没有选中“访问令牌”和“ID 令牌”的复选框。 对于使用 MSAL v2.0 或更高版本的 Blazor 应用,不建议使用隐式授权。 有关详细信息,请参阅保护 ASP.NET Core Blazor WebAssembly。
- 此体验可接受应用的其余默认值。
- 如果进行了更改,请选择“保存”按钮。
创建 Blazor 应用
创建应用。 将以下命令中的占位符替换为前面记录的信息,然后在命令行界面中执行以下命令:
dotnet new blazorwasm -au SingleOrg --client-id "{CLIENT ID}" --tenant-id "common" -o {PROJECT NAME}
占位符 | Azure 门户中的名称 | 示例 |
---|---|---|
{PROJECT NAME} |
— | BlazorSample |
{CLIENT ID} |
应用程序(客户端)ID | 00001111-aaaa-2222-bbbb-3333cccc4444 |
使用 -o|--output
选项指定的输出位置将创建一个项目文件夹(如果该文件夹不存在)并成为项目名称的一部分。
为 openid
和 offline_access
DefaultAccessTokenScopes 添加一对 MsalProviderOptions:
builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
});
运行应用
若要运行应用,请使用以下方法之一:
- Visual Studio
- 选择“运行”按钮。
- 从菜单栏中,依次使用“调试”>“开始调试” 。
- 按 F5 。
- .NET CLI 命令 shell:从应用的文件夹中执行
dotnet watch
(或dotnet run
)命令。
应用的组成部分
本部分介绍从 Blazor WebAssembly 项目模板生成的应用的组成部分以及如何配置应用。 如果使用演练部分的指南创建基本工作应用程序,则本部分中没有可用于该应用的特定指南。 本部分中的指南有助于更新应用以对用户进行身份验证和授权。 更新应用的另一种方法是根据演练部分的指南创建新的应用,然后将应用的组件、类和资源移动到新应用。
身份验证包
创建应用以使用工作或学校帐户 (SingleOrg
) 时,应用会自动接收 Microsoft 身份验证库 (Microsoft.Authentication.WebAssembly.Msal
) 的包引用。 此包提供了一组基元,可帮助应用验证用户身份并获取令牌以调用受保护的 API。
如果向应用添加身份验证,请手动将 Microsoft.Authentication.WebAssembly.Msal
包添加到应用中。
注意
有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。
Microsoft.Authentication.WebAssembly.Msal
包会间接将 Microsoft.AspNetCore.Components.WebAssembly.Authentication
包传递到应用中。
身份验证服务支持
使用由 Microsoft.Authentication.WebAssembly.Msal
包提供的 AddMsalAuthentication 扩展方法在服务容器中注册用户身份验证支持。 此方法设置应用与 Identity 提供者 (IP) 交互所需的所有服务。
在 Program
文件中:
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});
AddMsalAuthentication 方法接受回叫,以配置验证应用所需的参数。 注册应用时,可以从 ME-ID 配置中获取配置应用所需的值。
wwwroot/appsettings.json
配置
配置由 wwwroot/appsettings.json
文件提供:
{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/common",
"ClientId": "{CLIENT ID}",
"ValidateAuthority": true
}
}
示例:
{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/common",
"ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
"ValidateAuthority": true
}
}
访问令牌作用域
Blazor WebAssembly 模板不会自动将应用配置为请求安全 API 的访问令牌。 要将访问令牌作为登录流程的一部分进行预配,请将作用域添加到 MsalProviderOptions 的默认访问令牌作用域中:
builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
使用 AdditionalScopesToConsent
指定其他作用域:
options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE URI}");
注意
当用户首次使用在 Microsoft Azure 中注册的应用时,AdditionalScopesToConsent 无法通过 Microsoft Entra ID 同意 UI 为 Microsoft Graph 预配委派的用户权限。 有关详细信息,请参阅将 Graph API 和 ASP.NET Core Blazor WebAssembly 结合使用。
有关详细信息,请参阅“其他方案”一文的以下部分:
登录模式
框架默认为弹出式登录模式;如果无法打开弹出窗口,则回到重定向登录模式。 通过将 MsalProviderOptions 的 LoginMode
属性设置为 redirect
,将 MSAL 配置为使用重定向登录模式:
builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.LoginMode = "redirect";
});
默认设置为 popup
,字符串值不区分大小写。
导入文件
整个应用通过 _Imports.razor
文件提供 Microsoft.AspNetCore.Components.Authorization 命名空间:
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using {APPLICATION ASSEMBLY}
@using {APPLICATION ASSEMBLY}.Shared
索引页
索引页 (wwwroot/index.html
) 包含一个脚本,用于在 JavaScript 中定义 AuthenticationService
。 AuthenticationService
处理 OIDC 协议的低级别详细信息。 应用从内部调用脚本中定义的方法以执行身份验证操作。
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
应用组件
App
组件 (App.razor
) 类似于 Blazor Server 应用中的 App
组件:
- AuthorizeRouteView 组件确保当前用户有权访问给定页面或以其他方式呈现
RedirectToLogin
组件。 RedirectToLogin
组件管理将未经授权的用户重定向到登录页。
- CascadingAuthenticationState 组件管理向应用的 rest 公开 AuthenticationState。
- AuthorizeRouteView 组件确保当前用户有权访问给定页面或以其他方式呈现
RedirectToLogin
组件。 RedirectToLogin
组件管理将未经授权的用户重定向到登录页。
由于不同版本的 ASP.NET Core 中的框架发生了更改,因此本部分不会显示 App
组件 (App.razor
) 的 Razor 标记。 若要检查给定版本的组件的标记,请使用以下方法之一:
创建一个应用,预配为从要使用的 ASP.NET Core 版本的默认 Blazor WebAssembly 项目模板进行身份验证。 在生成的应用中检查
App
组件 (App.razor
)。在引用源中检查
App
组件 (App.razor
)。 从分支选择器中选择版本,并在存储库的ProjectTemplates
文件夹中搜索该组件,因为经过多年它已移动。注意
指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)。
RedirectToLogin 组件
RedirectToLogin
组件 (RedirectToLogin.razor
):
- 管理将未经授权的用户重定向到登录页。
- 保留用户尝试访问的当前 URL,以便在身份验证成功时可以通过以下方式将其返回到该页:
- .NET 7 或更高版本中的 ASP.NET Core 的导航历史记录状态。
- .NET 6 或更早版本中的 ASP.NET Core 的查询字符串。
在引用源中检查 RedirectToLogin
组件。 组件的位置随时间而更改,因此请使用 GitHub 搜索工具来查找该组件。
注意
指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)。
LoginDisplay 组件
LoginDisplay
组件 (LoginDisplay.razor
) 在 MainLayout
组件 (MainLayout.razor
) 中呈现并管理以下行为:
- 对于经过身份验证的用户:
- 显示当前用户名。
- 提供指向 ASP.NET Core Identity 中的用户配置文件页面的链接。
- 提供用于注销应用的按钮。
- 对于匿名用户:
- 提供用于注册的选项。
- 提供用于登录的选项。
由于不同版本的 ASP.NET Core 中的框架发生了更改,因此本部分不会显示 LoginDisplay
组件的 Razor 标记。 若要检查给定版本的组件的标记,请使用以下方法之一:
创建一个应用,预配为从要使用的 ASP.NET Core 版本的默认 Blazor WebAssembly 项目模板进行身份验证。 在生成的应用中检查
LoginDisplay
组件。在引用源中检查
LoginDisplay
组件。 组件的位置随时间而更改,因此请使用 GitHub 搜索工具来查找该组件。 使用Hosted
(为true
)的模板化内容。注意
指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)。
身份验证组件
Authentication
组件 (Pages/Authentication.razor
) 生成的页面定义了处理各种身份验证阶段所需的路由。
- 由
Microsoft.AspNetCore.Components.WebAssembly.Authentication
包提供。 - 管理在每个身份验证阶段执行适当的操作。
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />
@code {
[Parameter]
public string? Action { get; set; }
}
注意
.NET 6 或更高版本的 ASP.NET Core 支持空引用类型 (NRT) 和 .NET 编译器 Null 状态静态分析。 在 .NET 6 中发布 ASP.NET Core 之前,string
类型没有 null 类型指定(?
)。
疑难解答
日志记录
若要为 Blazor WebAssembly 身份验证启用调试或跟踪日志记录,请参阅 ASP.NET CoreBlazor 日志记录的客户端身份验证日志记录部分,其中项目版本选择器设置为 ASP.NET Core 7.0 或更高版本。
常见错误
应用或 Identity 提供者 (IP) 配置错误
最常见的错误是因为配置不正确导致的。 下面是几个示例:
- 根据具体情景的要求,缺少或不正确的颁发机构、实例、租户 ID、租户域、客户端 ID 或重定向 URI 会阻止应用对客户端进行身份验证。
- 不正确的请求范围会阻止客户端访问服务器 Web API 终结点。
- 服务器 API 权限不正确或缺失会阻止客户端访问服务器 Web API 终结点。
- 在不同于 IP 应用注册的重定向 URI 中配置的应用的端口运行应用。 请注意,Microsoft Entra ID 和在
localhost
开发测试地址上运行的应用不需要端口,但应用的端口配置和运行应用的端口必须与非localhost
地址匹配。
本文指导的配置部分显示了正确的配置示例。 请仔细查看本文的每个部分,以查找应用和 IP 配置错误。
如果配置看起来是正确的:
分析应用程序日志。
通过浏览器的开发人员工具,检查客户端应用和 IP 或服务器应用之间的网络流量。 通常,在发出请求后,IP 或服务器应用会向客户端返回一条确切的错误消息或包含线索的消息,其中指出了导致问题的原因。 有关开发人员工具指导,请参阅以下文章:
- Google Chrome(Google 文档)
- Microsoft Edge
- Mozilla Firefox(Mozilla 文档)
对于使用 JSON Web 令牌 (JWT) 的 Blazor 版本,根据问题发生的位置,对用于验证客户端或访问服务器 Web API 的令牌内容进行解码。 有关更多信息,请参阅检查 JSON Web 令牌 (JWT) 得内容。
文档团队会响应文章中的文档反馈和 bug(从“此页面”反馈部分提交问题),但无法提供产品支持。 可以借助多个公共支持论坛来帮助排查应用问题。 建议如下:
上述论坛并非 Microsoft 所拥有或者不受 Microsoft 控制。
对于非安全、非敏感且非机密的可重现框架 bug 报告,请向 ASP.NET Core 产品团队提交问题。 请务必先彻底调查问题原因,并确定无法自行解决问题,在公共支持论坛的社区帮助下同样无法解决问题后,再向该产品团队提交问题。 如果应用问题是由简单的配置错误引起或涉及第三方服务,该产品团队无法对此进行故障排除。 如果报告包含敏感或机密内容,或者描述了可能会被网络攻击者利用的潜在产品安全缺陷,请参阅报告安全问题和 bug(
dotnet/aspnetcore
GitHub 存储库)。ME-ID 的客户端未获得授权
信息:Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] 授权失败。 不符合以下要求:DenyAnonymousAuthorizationRequirement:要求用户经过身份验证。
ME-ID 返回的登录回叫错误:
- 错误:
unauthorized_client
- 说明:
AADB2C90058: The provided application is not configured to allow public clients.
若要解决该错误:
- 在 Azure 门户中访问应用的清单。
- 将
allowPublicClient
属性设置为null
或true
。
- 错误:
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
- 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 Gallery 或 FuGet Package Explorer 进行了解。
运行 Server
应用
在对托管的 Blazor WebAssembly 解决方案进行测试和故障排除时,请确保从 Server
项目运行应用。
检查用户
可直接在应用中使用以下 User
组件或将其用作进一步自定义的基础。
User.razor
:
@page "/user"
@attribute [Authorize]
@using System.Text.Json
@using System.Security.Claims
@inject IAccessTokenProvider AuthorizationService
<h1>@AuthenticatedUser?.Identity?.Name</h1>
<h2>Claims</h2>
@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
{
<p class="claim">@(claim.Type): @claim.Value</p>
}
<h2>Access token</h2>
<p id="access-token">@AccessToken?.Value</p>
<h2>Access token claims</h2>
@foreach (var claim in GetAccessTokenClaims())
{
<p>@(claim.Key): @claim.Value.ToString()</p>
}
@if (AccessToken != null)
{
<h2>Access token expires</h2>
<p>Current time: <span id="current-time">@DateTimeOffset.Now</span></p>
<p id="access-token-expires">@AccessToken.Expires</p>
<h2>Access token granted scopes (as reported by the API)</h2>
@foreach (var scope in AccessToken.GrantedScopes)
{
<p>Scope: @scope</p>
}
}
@code {
[CascadingParameter]
private Task<AuthenticationState> AuthenticationState { get; set; }
public ClaimsPrincipal AuthenticatedUser { get; set; }
public AccessToken AccessToken { get; set; }
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
var state = await AuthenticationState;
var accessTokenResult = await AuthorizationService.RequestAccessToken();
if (!accessTokenResult.TryGetToken(out var token))
{
throw new InvalidOperationException(
"Failed to provision the access token.");
}
AccessToken = token;
AuthenticatedUser = state.User;
}
protected IDictionary<string, object> GetAccessTokenClaims()
{
if (AccessToken == null)
{
return new Dictionary<string, object>();
}
// header.payload.signature
var payload = AccessToken.Value.Split(".")[1];
var base64Payload = payload.Replace('-', '+').Replace('_', '/')
.PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');
return JsonSerializer.Deserialize<IDictionary<string, object>>(
Convert.FromBase64String(base64Payload));
}
}
检查 JSON Web 令牌 (JWT) 的内容
若要对 JSON Web 令牌 (JWT) 进行解码,请使用 Microsoft 的 jwt.ms 工具。 UI 中的值永远不会离开浏览器。
编码 JWT 示例(为便于显示,已经缩短):
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1j ... bQdHBHGcQQRbW7Wmo6SWYG4V_bU55Ug_PW4pLPr20tTS8Ct7_uwy9DWrzCMzpD-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-yBMKV2_nXA25Q
工具针对向 Azure AAD B2C 进行身份验证的应用解码的 JWT 示例:
{
"typ": "JWT",
"alg": "RS256",
"kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
"exp": 1610059429,
"nbf": 1610055829,
"ver": "1.0",
"iss": "https://mysiteb2c.b2clogin.com/11112222-bbbb-3333-cccc-4444dddd5555/v2.0/",
"sub": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
"aud": "00001111-aaaa-2222-bbbb-3333cccc4444",
"nonce": "bbbb0000-cccc-1111-dddd-2222eeee3333",
"iat": 1610055829,
"auth_time": 1610055822,
"idp": "idp.com",
"tfp": "B2C_1_signupsignin"
}.[Signature]