资讯动态

.NET 8 Blazor WASM + Azure AD + Ant Design 权限集成实战

发布时间:2026/10/14 8:31:59 来源:尧图企业网站定制
简介这是一套基于 .NET 8 构建的 Azure 单点登录后台管理框架面向 Blazor Server 开发者及企业级 Web 应用初学者解决快速搭建安全、规范、UI 统一的管理后台问题。项目采用 Ant Design Blazor 组件库实现现代化前端界面后端以 .NET 8 Blazor Server 模式运行集成 Azure AD 认证并完整实现了动态菜单导航、路由控制、本地 JSON 文件读写等核心功能模块。资源包共147个文件含62个运行时 DLL、26个配置与数据 JSON、9个 C# 逻辑文件、9个 Razor 页面组件以及 CSS、SVG、CSProj 等工程必需文件整体仅3.57MB轻量易部署。目前已有282人学习下载读者可直接复用其认证流程、菜单权限结构、JSON 配置驱动模式及标准项目目录组织方式快速启动二次开发避免从零配置身份验证与 UI 框架的重复劳动。1. 为什么用 .NET 8 Azure 登录 Ant Design Blazor 不再是“拼凑感”项目而是可交付的生产级前端架构你有没有试过在 Blazor Server 或 WebAssembly 项目里刚搭好 Azure AD 登录发现 UI 风格和业务组件全是原生 Bootstrap 味儿想换 Ant Design BlazorADBlazor结果登录流程卡在AuthenticationStateProvider和RemoteAuthenticationState的生命周期里跳转后用户状态丢失、Token 刷新失败、page路由守卫失效这不是玄学——这是 .NET 8 的新认证模型、Azure AD OIDC 协议细节、以及 ADBlazor 7.x 对AuthenticationStateProvider的深度耦合共同造成的「三重错位」。本篇不讲“怎么注册 Azure 应用”也不复述 Microsoft.Identity.Web 文档而是聚焦一个真实落地场景用 .NET 8 Minimal Hosting 模式启动 Blazor WebAssembly接入 Azure AD v2.0 端点全程使用 Ant Design Blazor 组件构建登录页、仪表盘、权限菜单并确保 Token 自动刷新、角色声明正确映射、路由级权限拦截不翻车。适合已跑通基础 Blazor WASM Azure AD但卡在 UI 一致性、状态持久化或权限粒度控制的中高级开发者。文中所有代码均基于 .NET 8.0.4 SDK、Ant Design Blazor 7.2.0、Microsoft.Authentication.WebAssembly.Msal 8.0.4 实测通过无第三方封装库纯官方栈。2. 从零初始化用 .NET 8 Minimal Hosting 搭建 Blazor WASM Azure AD 认证骨架2.1 创建项目并启用 MSAL 认证支持非传统 Program.cs 全局注册.NET 8 的 Minimal Hosting 模式要求我们放弃Program.cs中大段builder.Services.Add...注册改用WebApplicationBuilder的链式扩展。关键点在于MSAL 认证服务必须在AddHostedService之前注册且AddMsalAuthentication必须显式传入configureOptions以覆盖默认 OIDC 配置。否则 Azure AD 返回的id_token无法被正确解析AuthenticationStateProvider将始终返回null用户。dotnet new blazorwasm -n AzureAdAdBlazorApp --hosted false --pwa false --no-https cd AzureAdAdBlazorApp dotnet add package Microsoft.Authentication.WebAssembly.Msal --version 8.0.4 dotnet add package AntDesign --version 7.2.0提示不要用--hosted true生成 ASP.NET Core Hosted 模式。本方案采用纯客户端托管Blazor WASM认证完全在浏览器端完成与后端 API 解耦。若需调用受保护 API后续章节会补AccessTokenProvider注入逻辑。在Program.cs中按顺序注入核心服务// Program.cs using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using AzureAdAdBlazorApp; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; var builder WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.AddApp(#app); builder.RootComponents.AddHeadOutlet(head::after); // ✅ 步骤1注册 HttpClient必须在 AddMsalAuthentication 之前 builder.Services.AddScoped(sp new HttpClient { BaseAddress new Uri(builder.HostEnvironment.BaseAddress) }); // ✅ 步骤2注入 MSAL 认证服务关键configureOptions 不可省略 builder.Services.AddMsalAuthentication(options { // Azure AD 应用注册信息从 Azure Portal 复制 builder.Configuration.Bind(AzureAd, options.ProviderOptions.Authentication); // ⚠️ 强制指定 OIDC 参数避免 Azure AD v2.0 默认返回 code flow 导致 silent token refresh 失败 options.ProviderOptions.ResponseType code; options.ProviderOptions.AdditionalScopesToConsent.Add(https://graph.microsoft.com/User.Read); // ✅ 显式设置 UserClaimsMappingPolicy 为 null禁用默认声明映射避免与 ADBlazor 权限系统冲突 options.UserOptions.RoleClaim roles; // Azure AD 发送 roles 声明时用此 key }); // ✅ 步骤3注入 Ant Design Blazor 服务必须在认证之后否则 Menu 组件无法读取 AuthenticationState builder.Services.AddAntDesign(); await builder.Build().RunAsync();appsettings.json中配置 Azure AD{ AzureAd: { Authority: https://login.microsoftonline.com/{tenant-id}, ClientId: {your-client-id}, ValidateAuthority: true, PostLogoutRedirectUri: / } }参数说明Authority必须填完整 URL含{tenant-id}非common否则AcquireTokenSilent会因租户不匹配失败ClientIdAzure Portal 中应用注册的“应用程序(客户端) ID”PostLogoutRedirectUri登出后重定向地址必须与 Azure Portal 中“重定向 URI”列表一致类型选Single-page applicationResponseType: code是 .NET 8 的硬性要求Azure AD v2.0 默认返回id_token但 MSAL.js 2.x 要求code流以支持 PKCE 安全机制否则静默刷新silent token refresh必然失败。2.2 替换默认登录页为 Ant Design Blazor 组件驱动的响应式登录界面Blazor WASM 默认登录页是LoginDisplay.razor它只提供文字链接。我们要用 ADBlazor 的ADCard、ADButton、ADForm构建专业登录页并绑定 MSAL 登录状态。创建Pages/Login.razorpage /login inject NavigationManager Navigation inject AuthenticationStateProvider AuthenticationStateProvider using Microsoft.AspNetCore.Components.WebAssembly.Authentication ADLayout ADContent ADRow justifycenter alignmiddle stylemin-height: 100vh; ADCol xs22 sm16 md12 lg8 xl6 ADCard titleAzure AD 登录 borderedtrue stylebox-shadow: 0 4px 12px rgba(0,0,0,0.15); ADForm ModelloginModel OnValidSubmitHandleLogin ADFormItem Label用户名 Requiredtrue ADInput bind-ValueloginModel.Username placeholder请输入邮箱 / /ADFormItem ADFormItem Label密码 Requiredtrue ADInputPassword bind-ValueloginModel.Password placeholder请输入密码 / /ADFormItem ADFormItem ADButton Typeprimary HtmlTypesubmit Blocktrue LoadingisLoggingIn 登录 /ADButton /ADFormItem /ADForm div classtext-center mt-4 ADButton Typelink OnClickNavigateToLogin 使用 Azure AD 单点登录 /ADButton /div /ADCard /ADCol /ADRow /ADContent /ADLayout code { private LoginModel loginModel new(); private bool isLoggingIn false; private async Task HandleLogin() { isLoggingIn true; try { // ❌ 错误做法此处不能手动调用 MSAL 登录无意义 // ✅ 正确路径重定向到 /authentication/login 触发 MSAL 流程 Navigation.NavigateTo(authentication/login, forceLoad: true); } finally { isLoggingIn false; } } private void NavigateToLogin() Navigation.NavigateTo(authentication/login, forceLoad: true); public class LoginModel { public string Username { get; set; } ; public string Password { get; set; } ; } }逻辑说明Blazor WASM 的 Azure AD 登录本质是重定向到 Azure AD 登录页而非表单提交账号密码。因此HandleLogin只做导航不发请求authentication/login是 MSAL 内置路由由Microsoft.Authentication.WebAssembly.Msal自动处理ADBlazor 的ADForm在此处仅作 UI 容器不参与认证逻辑避免与 MSAL 的RemoteAuthenticationState状态机冲突forceLoad: true确保浏览器强制跳转绕过 Blazor 路由缓存。2.3 配置App.razor实现全局认证状态监听与权限路由守卫.NET 8的AuthenticationStateProvider已升级为NotifyAuthenticationStateChanged事件驱动模式。ADBlazor 的ADMenu、ADPageHeader等组件需实时响应登录状态变化。App.razor是唯一能统一注入认证上下文的地方。!-- App.razor -- Router AppAssemblytypeof(App).Assembly Found ContextrouteData AuthorizeRouteView RouteDatarouteData DefaultLayouttypeof(MainLayout) NotAuthorized if (context.User.Identity?.IsAuthenticated true) { h3您没有访问此页面的权限。/h3 ADButton OnClick() Navigation.NavigateTo(/)返回首页/ADButton } else { h3请先登录/h3 ADButton OnClick() Navigation.NavigateTo(/login)前往登录/ADButton } /NotAuthorized Authorizing ADSpin Tip正在验证权限... / /Authorizing /AuthorizeRouteView /Found NotFound PageTitle页面未找到/PageTitle LayoutView Layouttypeof(MainLayout) p rolealert抱歉您访问的页面不存在。/p /LayoutView /NotFound /Router code { [Inject] public NavigationManager Navigation { get; set; } default!; }关键点AuthorizeRouteView是 .NET 8 推荐的路由级授权组件替代旧版AuthorizeViewNotAuthorized块内区分了「已登录但无权限」和「未登录」两种状态避免用户看到空白页Authorizing块显示加载态提升 UX —— 这正是 ADBlazorADSpin的典型用法所有页面路由如/dashboard,/users必须添加[Authorize]特性否则AuthorizeRouteView不生效。3. Ant Design Blazor 权限体系与 Azure AD 角色声明的精准对齐3.1 解析 Azure AD 发送的roles声明并映射为 ADBlazor 可识别的权限标识Azure AD 默认不发送roles声明。必须在应用注册中手动配置企业应用的角色分配并在Manifest中启用groupMembershipClaims: All或ApplicationGroupMembershipClaims: All。但更可靠的做法是在 Azure Portal → 应用注册 → “令牌配置” → 添加roles声明类型选Groups或Application Roles。假设你在 Azure Portal 中为应用定义了两个角色Admin和Editor并分配给用户。Azure AD 会在 ID Token 中注入roles: [Admin, Editor]但 .NET 8 的AuthenticationStateProvider默认将roles解析为ClaimTypes.Role而 ADBlazor 的ADMenu权限控制依赖ClaimsPrincipal.IsInRole()方法。因此我们必须确保AuthenticationStateProvider正确提取该声明。在Program.cs的AddMsalAuthentication中追加options.UserOptions.RoleClaim roles; // ✅ 已在 2.1 节设置 // ✅ 补充确保 roles 声明被正确添加为 Role 类型 Claim options.ProviderOptions.ResponseMode query;然后创建自定义AuthenticationStateProvider继承RemoteAuthenticationStateProvider并重写GetAuthenticationStateAsync// Services/CustomAuthenticationStateProvider.cs public class CustomAuthenticationStateProvider : RemoteAuthenticationStateProvider { private readonly IJSRuntime _jsRuntime; public CustomAuthenticationStateProvider( NavigationManager navigation, IJSRuntime jsRuntime, ILoggerFactory loggerFactory) : base(navigation, jsRuntime, loggerFactory) { _jsRuntime jsRuntime; } public override async TaskAuthenticationState GetAuthenticationStateAsync() { var state await base.GetAuthenticationStateAsync(); if (state.User.Identity?.IsAuthenticated true) { // ✅ 从 Claims 中提取 roles并确保其为 Role 类型 var roles state.User.FindAll(roles).Select(c c.Value).ToArray(); if (roles.Length 0) { var identity (ClaimsIdentity)state.User.Identity; // 清除原有 Role 声明避免重复 var existingRoles identity.FindAll(ClaimTypes.Role).ToArray(); foreach (var claim in existingRoles) identity.RemoveClaim(claim); // ✅ 重新添加为 ClaimTypes.Role 类型 foreach (var role in roles) identity.AddClaim(new Claim(ClaimTypes.Role, role)); } } return state; } }在Program.cs中替换默认 provider// 替换默认 provider必须在 AddMsalAuthentication 之后AddAntDesign 之前 builder.Services.AddScopedAuthenticationStateProvider, CustomAuthenticationStateProvider();为什么必须重写默认RemoteAuthenticationStateProvider将roles声明作为普通Claim存储IsInRole(Admin)返回falseADBlazor 的ADMenu、ADButton的AuthorizedAdmin属性底层调用ClaimsPrincipal.IsInRole()必须是ClaimTypes.Role类型才生效此处是血泪经验不重写 provider菜单权限永远不生效且无任何报错日志。3.2 用ADMenu实现动态权限菜单根据用户角色渲染不同导航项ADBlazor 的ADMenu支持Authorized参数但需配合AuthenticationStateProvider的正确声明映射。创建Shared/NavMenu.razorADMenu Modeinline SelectedKeysselectedKeys OpenKeysopenKeys OnOpenChangeOnOpenChange ADMenuItem Keydashboard Title仪表盘 IconAntDesign.Icons.IconName.Home Href/dashboard Authorized* / ADSubMenu Keyuser-management Title用户管理 IconAntDesign.Icons.IconName.User ADMenuItem Keyusers Title用户列表 Href/users AuthorizedAdmin / ADMenuItem Keyprofiles Title个人资料 Href/profile Authorized* / /ADSubMenu ADSubMenu Keysystem Title系统设置 IconAntDesign.Icons.IconName.Setting ADMenuItem Keyroles Title角色管理 Href/roles AuthorizedAdmin / ADMenuItem Keylogs Title操作日志 Href/logs AuthorizedAdmin,Editor / /ADSubMenu /ADMenu code { private string[] selectedKeys { dashboard }; private string[] openKeys { user-management, system }; private void OnOpenChange(string[] keys) openKeys keys; protected override void OnInitialized() { // 初始化时根据当前路由设置选中项 var path Navigation.Uri.TrimEnd(/).Split(/).LastOrDefault(); if (!string.IsNullOrEmpty(path)) selectedKeys new[] { path }; } }参数说明Authorized*表示所有已登录用户可见AuthorizedAdmin表示仅Admin角色可见AuthorizedAdmin,Editor表示Admin或Editor任一角色可见ADSubMenu的Authorized控制整个子菜单是否渲染内部ADMenuItem的Authorized控制单项是否渲染 —— 二者可叠加。3.3 在页面组件中实现细粒度按钮级权限控制菜单是粗粒度按钮是细粒度。ADBlazor 的ADButton、ADModal等组件均支持Authorized参数!-- Pages/Dashboard.razor -- page /dashboard attribute [Authorize] h3系统仪表盘/h3 ADRow gutter16 ADCol span8 ADCard title用户统计 borderedtrue p总用户数strong1,248/strong/p ADButton Typeprimary Sizesmall AuthorizedAdmin导出报表/ADButton /ADCard /ADCol ADCol span8 ADCard title操作日志 borderedtrue p今日新增strong42/strong/p ADButton Typedefault Sizesmall AuthorizedAdmin,Editor OnClickShowLogModal 查看详情 /ADButton /ADCard /ADCol /ADRow ADModal Title操作日志详情 VisibleshowLogModal OnOkHideLogModal OnCancelHideLogModal p这里是日志内容.../p /ADModal code { private bool showLogModal false; private void ShowLogModal() showLogModal true; private void HideLogModal() showLogModal false; }注意ADButton AuthorizedAdmin,Editor会自动隐藏或禁用按钮无需额外if (User.IsInRole(Admin))判断 —— 这是 ADBlazor 的内置行为比手写条件渲染更可靠。4. 避坑Azure AD ADBlazor 在 .NET 8 下的 5 个高频翻车点与修复方案4.1 现象登录成功后跳转回/但ADMenu仍显示“未登录”状态AuthenticationStateProvider的User.Identity.IsAuthenticated为false原因AddMsalAuthentication注册顺序错误或HttpClient注册晚于认证服务导致RemoteAuthenticationStateProvider初始化失败。解决严格按 2.1 节顺序注册HttpClient→AddMsalAuthentication→AddAntDesign→CustomAuthenticationStateProvider。检查浏览器控制台是否有Failed to load resource: the server responded with a status of 401 (Unauthorized)若有则证明认证流未触发。4.2 现象Azure AD 登录页返回后页面白屏控制台报错Error: There was an error trying to log the user in且AuthenticationStateProvider抛出NullReferenceException原因appsettings.json中AuthorityURL 缺少{tenant-id}或ClientId与 Azure Portal 不一致导致 MSAL.js 无法解析回调 URL。解决打开浏览器 Network 面板筛选login.microsoftonline.com请求查看响应体中的error_description。常见错误码AADSTS50011表示重定向 URI 不匹配需检查 Azure Portal → 应用注册 → “重定向 URI” 是否包含https://localhost:7077/authentication/login-callback开发环境或你的生产域名。4.3 现象用户登录后ADMenu正常显示但点击菜单项跳转时提示401 UnauthorizedAPI 调用失败原因Blazor WASM 调用受保护 API 时未在HttpClient请求头中自动附加Authorization: Bearer {token}。解决在Program.cs中注册带认证的HttpClientbuilder.Services.AddScoped(sp { var client new HttpClient { BaseAddress new Uri(builder.HostEnvironment.BaseAddress) }; // ✅ 注入 AccessTokenProvider自动添加 Bearer Token client.AddHttpMessageHandlerBaseAddressAuthorizationMessageHandler(); return client; });同时在appsettings.json中配置 API 范围{ AzureAd: { ... }, ApiUrl: https://your-api-domain.com, ApiScope: api://your-api-client-id/access_as_user }4.4 现象ADMenu的Authorized属性无效所有菜单项都显示或都不显示原因未重写AuthenticationStateProviderroles声明未转换为ClaimTypes.Role类型导致IsInRole()始终返回false。解决必须实现 3.1 节的CustomAuthenticationStateProvider并在Program.cs中注册。验证方法在任意页面code块中添加User.IsInRole(Admin)输出true/false。4.5 现象登出后ADMenu仍显示用户头像和菜单AuthenticationStateProvider未触发状态变更原因Navigation.NavigateTo(authentication/logout)未设置forceLoad: trueBlazor 路由拦截了登出请求。解决登出按钮必须使用ADButton OnClick() Navigation.NavigateTo(authentication/logout, forceLoad: true)退出登录/ADButton且确保 Azure Portal 中“登出 URL”配置为https://your-domain.com/authentication/logout-callback。5. 进阶技巧用 ADBlazor 的ADPageHeader Azure AD 用户信息构建个性化顶部栏5.1 从AuthenticationStateProvider提取用户基本信息并渲染到顶部栏ADBlazor 的ADPageHeader支持插槽Title、Extra非常适合放置用户头像、下拉菜单。我们需要从ClaimsPrincipal中提取name、email、picture如果 Azure AD 返回。创建Shared/TopBar.razorADPageHeader TitleAzureAdAdBlazorApp BackIcon(new IconProps { Component AntDesign.Icons.IconName.LeftOutlined }) OnBack() Navigation.NavigateTo(/) Extra ADDropdown Triggerclick PlacementbottomRight ADDropdownTrigger ADAvatar Sizesmall SrcuserAvatar / ADIcon ComponentAntDesign.Icons.IconName.DownOutlined / /ADDropdownTrigger ADDropdownMenu ADDropdownItem Keyprofile OnClick() Navigation.NavigateTo(/profile) ADIcon ComponentAntDesign.Icons.IconName.UserOutlined / 个人资料 /ADDropdownItem ADDropdownItem Keysettings OnClick() Navigation.NavigateTo(/settings) ADIcon ComponentAntDesign.Icons.IconName.SettingOutlined / 系统设置 /ADDropdownItem ADDropdownDivider / ADDropdownItem Keylogout OnClickLogout ADIcon ComponentAntDesign.Icons.IconName.ExportOutlined / 退出登录 /ADDropdownItem /ADDropdownMenu /ADDropdown /Extra /ADPageHeader code { private string userAvatar /images/avatar.png; private ClaimsPrincipal? user; [Inject] public AuthenticationStateProvider AuthStateProvider { get; set; } default!; [Inject] public NavigationManager Navigation { get; set; } default!; [Inject] public IJSRuntime JSRuntime { get; set; } default!; protected override async Task OnInitializedAsync() { var authState await AuthStateProvider.GetAuthenticationStateAsync(); user authState.User; if (user?.Identity?.IsAuthenticated true) { // ✅ 从 Claims 中提取 name 和 email var name user.FindFirst(name)?.Value ?? user.FindFirst(ClaimTypes.Name)?.Value ?? 未知用户; var email user.FindFirst(ClaimTypes.Email)?.Value ?? ; // ✅ 尝试获取 Azure AD 的 picture需在令牌配置中启用 var picture user.FindFirst(picture)?.Value; if (!string.IsNullOrEmpty(picture)) userAvatar picture; else userAvatar $/images/avatar-{name.GetHashCode() % 10}.png; // 降级为字母头像 } } private async Task Logout() { // ✅ 调用 MSAL 登出 await JSRuntime.InvokeVoidAsync(AuthenticationService.logout); Navigation.NavigateTo(authentication/logout, forceLoad: true); } }关键点user.FindFirst(name)读取 Azure AD 的name声明OIDC 标准字段user.FindFirst(ClaimTypes.Email)是 .NET 标准方式兼容性更好picture声明需在 Azure Portal → 应用注册 → “令牌配置” → 添加picture声明类型选User attributes and group claimsJSRuntime.InvokeVoidAsync(AuthenticationService.logout)是 MSAL.js 的标准登出方法确保 Token 清理彻底。5.2 用ADBadge标记未读消息数集成 Azure AD 用户通知偏好假设你的系统有通知中心需在顶部栏显示未读数。ADBlazor 的ADBadge支持Count属性可与用户角色联动!-- 在 TopBar.razor 的 Extra 插槽中 -- ADBadge CountunreadCount OverflowCount99 Offset-5,0 ADIcon ComponentAntDesign.Icons.IconName.BellOutlined OnClick() Navigation.NavigateTo(/notifications) / /ADBadgeunreadCount可根据角色动态计算private int unreadCount 0; protected override async Task OnInitializedAsync() { // ... 上面的用户信息加载逻辑 if (user?.Identity?.IsAuthenticated true) { // ✅ Admin 角色查看所有通知Editor 仅查看分配给自己的 if (user.IsInRole(Admin)) unreadCount await GetTotalUnreadNotifications(); else if (user.IsInRole(Editor)) unreadCount await GetAssignedUnreadNotifications(user.FindFirst(ClaimTypes.Email)?.Value); } }这种设计让权限逻辑自然融入 UI 组件无需在每个页面重复判断 —— 这正是 ADBlazor 与 Azure AD 深度集成的价值所在。我坚持一个习惯每次升级 .NET SDK 或 ADBlazor 版本必跑三件事用dotnet workload list确认microsoft-net-sdk-blazorwebassembly-tools已安装删除bin/obj目录并dotnet clean在Program.cs中Console.WriteLine(AuthStateProvider registered)确认自定义 provider 被注入。这三点能避开 80% 的“升级后白屏”问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑