Blazor Server调用Microsoft Graph多次发送邮件异常问题
问题根因
这是Blazor Server集成Microsoft Identity + Microsoft Graph时的典型身份上下文丢失问题,和业务代码逻辑无关:
- Blazor Server使用长连接Circuit模式维持会话,首次加载页面时存在完整的HttpContext,第一次调用Graph接口可以正常从令牌缓存读取当前用户账号,完成静默取token发邮件。
- 第一次请求结束后,Circuit上下文中持有的用户身份标识没有被正确持久化,第二次调用时
AcquireTokenSilent方法找不到对应账号的缓存条目,直接抛出MsalUiRequiredException。 - 原代码直接在组件事件回调里调用
ConsentHandler.HandleException,而Blazor组件事件触发时不存在完整的HttpContext,重定向逻辑无法正常执行,直接导致页面跳空白。 - 重新登录后会重新生成完整的用户上下文和令牌缓存,所以功能暂时恢复,第二次调用又重复上述问题。
修复方案
按以下步骤调整即可支持连续多次发邮件:
- 修正服务注册生命周期
Blazor Server默认不注册IHttpContextAccessor,且不能用单例生命周期注册GraphServiceClient,否则会跨请求混用用户上下文。在Program.cs中调整注册逻辑:
// 注册HttpContext访问器 builder.Services.AddHttpContextAccessor(); // 用Scoped生命周期注册GraphServiceClient,每次请求范围绑定对应用户 builder.Services.AddScoped(sp => { var tokenAcquisition = sp.GetRequiredService<ITokenAcquisition>(); var authProvider = new BaseBearerTokenAuthenticationProvider( new TokenAcquisitionAuthenticationProvider(tokenAcquisition, new[] { "Mail.Send" }) ); return new GraphServiceClient(authProvider); });
- 调整组件内发邮件逻辑
不要在无HttpContext的回调里直接触发认证挑战,显式指定Graph请求的权限范围,遇到令牌失效时通过强制导航触发重新认证:
@inject GraphServiceClient GraphServiceClient @inject IHttpContextAccessor HttpContextAccessor @inject NavigationManager NavigationManager <button @onclick="SendMail">发送测试邮件</button> @code { private async Task SendMail() { var message = new Message { Subject = "Meet for lunch?", Body = new ItemBody { ContentType = BodyType.Text, Content = "The new cafeteria is open." }, ToRecipients = new List<Recipient>() { new Recipient { EmailAddress = new EmailAddress { Address = "fannyd@contoso.onmicrosoft.com" } } }, CcRecipients = new List<Recipient>() { new Recipient { EmailAddress = new EmailAddress { Address = "danas@contoso.onmicrosoft.com" } } } }; var saveToSentItems = false; try { await GraphServiceClient.Me .SendMail(message, saveToSentItems) .Request() .WithScopes("Mail.Send") .PostAsync(); } catch (MsalUiRequiredException) { // 存在有效HttpContext时走内置的同意/挑战逻辑 if (HttpContextAccessor.HttpContext != null) { var consentHandler = HttpContextAccessor.HttpContext.RequestServices .GetRequiredService<MicrosoftIdentityConsentAndConditionalAccessHandler>(); consentHandler.HandleException(new MsalUiRequiredException("invalid_grant", "Token expired")); } // 长连接无HttpContext时,强制导航到登录页重认证 else { var returnUrl = Uri.EscapeDataString(NavigationManager.Uri); NavigationManager.NavigateTo($"MicrosoftIdentity/Account/SignIn?redirectUri={returnUrl}", forceLoad: true); } } } }
- 配置校验
- 确认Azure AD应用的
Mail.Send委托权限已经授予管理员同意,避免每次调用都触发增量同意流程。 - 多实例部署场景下,不要使用内存存储令牌缓存,替换为分布式缓存(如Redis)持久化令牌,避免跨实例请求找不到缓存条目。
- 不要在Circuit中全局缓存持有用户身份对象,每次调用Graph服务时从当前作用域获取最新上下文。
- 确认Azure AD应用的
内容的提问来源于stack exchange,提问作者Nb777
相关产品推荐
相关产品推荐

