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

Blazor Server(.NET8) EntraID认证调用下游API遇MSAL缓存异常

问题:Blazor Server(.NET 8)调用下游API时MSAL缓存无账户导致MsalUiRequiredException

我正在开发采用Server交互模式的.NET 8 Blazor Web App,通过Microsoft Entra ID完成用户认证,认证功能正常,可确认用户已登录且包含oid、tid等声明。但调用下游API执行以下代码时出现错误:

var token = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes, user);

错误信息:

MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call

详细MSAL日志:

Found 0 accounts in MSAL cache

No refresh tokens or accounts found

Token cache could not satisfy silent request

即便用户已认证,该问题依然存在。以下是我的配置代码:


Program.cs 配置

// Program.cs

builder.Services.AddBlazorServerAuthentication(builder.Configuration);
builder.Services.AddBlazorServerAuthorization();

// Add services to the container.
_ = builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddMicrosoftIdentityConsentHandler();

_ = builder.Services.AddServerSideBlazor()
    .AddMicrosoftIdentityConsentHandler();

_ = builder.Services.AddHttpContextAccessor();
_ = builder.Services.AddCascadingAuthenticationState();

_ = builder.Services.AddTransient<AccessTokenHandler>();

_ = builder.Services
    .AddHttpClient("WebApiClient", client => client.BaseAddress = builder.Configuration.GetValue<Uri>("DownstreamApi:BaseUrl"))
    .AddHttpMessageHandler<AccessTokenHandler>();

认证扩展方法

public static void AddBlazorServerAuthentication(this IServiceCollection services, IConfiguration configuration)
{
    JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
    string[] scopes = configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(';');

    _ = services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddBearerToken()
        .AddMicrosoftIdentityWebApp(options =>
        {
            configuration.Bind("AzureAd", options);

            options.MaxAge = TimeSpan.FromHours(8);
            options.AutomaticRefreshInterval = TimeSpan.FromMinutes(15);

            options.Events.OnRemoteFailure = async context =>
            {
                try
                {
                    AuthenticationProperties authenticationProperties = new()
                    {
                        RedirectUri = "/"
                    };
                    authenticationProperties.Parameters["logoutHint"] = context?.HttpContext?.User?.Identity?.Name;

                    await context.HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, authenticationProperties);
                    await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

                    Console.WriteLine("Authentication failed. Logging out user silently");

                    context.HandleResponse();
                    await Task.CompletedTask;
                }
                catch (Exception)
                {
                    throw;
                }
            };
        })
        .EnableTokenAcquisitionToCallDownstreamApi(scopes)
        .AddInMemoryTokenCaches();
}

public static void AddBlazorServerAuthorization(this IServiceCollection services)
{
    _ = services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });
}

AccessTokenHandler 实现

//AccessTokenHandler

public class AccessTokenHandler : DelegatingHandler
{
    private readonly ITokenAcquisition _tokenAcquisition;
    private readonly IConfiguration _config;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public AccessTokenHandler(ITokenAcquisition tokenAcquisition, IConfiguration config, IHttpContextAccessor httpContextAccessor)
    {
        _tokenAcquisition = tokenAcquisition;
        _config = config;
        _httpContextAccessor = httpContextAccessor;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        string[] scopes = _config["DownstreamApi:Scopes"]?.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        if (scopes?.Length > 0)
        {
            string token = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes,
                user: _httpContextAccessor.HttpContext?.User,
                authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme);
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

appsettings.json 配置

"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "xxxxxxx",
    "ClientSecret": "xyxyxyxyxy",
    "TenantId": "zzzzzzzzz"
},
"DownstreamApi": {
    "BaseUrl": "https://localhost:44385",
    "Scopes": "api://yyyyyyyyyy/access_as_user"
}

问题原因及解决步骤

原因分析

MSAL无法在缓存中找到账户,核心原因包括:

  1. 认证流程未将用户刷新令牌写入MSAL缓存
  2. 范围分割符配置不一致导致令牌请求无效
  3. 令牌获取时指定的认证方案与上下文不匹配
  4. 内存缓存的生命周期与Blazor Server会话模型不兼容

解决步骤

  1. 开启认证令牌保存
    在AddMicrosoftIdentityWebApp配置中添加SaveTokens = true,确保认证后刷新令牌被保存到票据,供MSAL填充缓存:

    .AddMicrosoftIdentityWebApp(options =>
    {
        configuration.Bind("AzureAd", options);
        options.SaveTokens = true; // 新增此行
        // 其他原有配置...
    })
    
  2. 统一范围分割规则
    修正AddBlazorServerAuthentication中的范围分割符,与AccessTokenHandler保持一致(用空格分割):

    // AddBlazorServerAuthentication中
    string[] scopes = configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ', StringSplitOptions.RemoveEmptyEntries);
    
  3. 使用正确的认证方案获取令牌
    将GetAccessTokenForUserAsync中的认证方案改为Cookie默认方案,匹配认证配置:

    string token = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes,
        user: _httpContextAccessor.HttpContext?.User,
        authenticationScheme: CookieAuthenticationDefaults.AuthenticationScheme);
    
  4. 生产环境替换为分布式令牌缓存
    内存缓存在多实例或服务重启时会丢失令牌,推荐用Redis等分布式缓存替代:

    // 替换原有AddInMemoryTokenCaches()
    .AddDistributedTokenCaches()
    .AddStackExchangeRedisCache(options =>
    {
        options.Configuration = "你的Redis连接字符串";
    });
    
  5. 确保HttpContext上下文有效
    确认IHttpContextAccessor已正确注入,且AccessTokenHandler的调用处于请求上下文内(避免在后台任务等无HttpContext场景调用)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 16:07:33