You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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服务时从当前作用域获取最新上下文。

内容的提问来源于stack exchange,提问作者Nb777

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 19:09:22