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

集成Azure AD认证授权的MSAL问题及令牌过期检测需求

问题

已按照微软示例应用配置Azure AD认证,并通过授权语句锁定应用,仅允许已认证用户访问。遇到以下问题:

  • 测试环境中重启IIS Express后,若浏览器未关闭,无需重新登录即可导航应用,但调用MS Graph相关功能时会报错:ErrorCode: user_null Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call。此时应用认为用户已授权,但实际因令牌问题导致Graph API调用失败;强制登出后重新登录则恢复正常。
  • 生产环境中,用户长时间未操作但浏览器保持打开时也会出现该问题,需强制登出重登才能解决。

疑问:

  1. 是否配置有误?
  2. 需添加什么设置来提前触发重登,或无需强制重登即可维持令牌有效性?
  3. 是否有方法检测令牌是否过期,避免每次调用Graph API前都执行GetAccessTokenForUserAsync?

现有配置代码

// Add services to the container.
builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
    options.Conventions.AllowAnonymousToFolder("/Login");
    options.Conventions.AuthorizeFolder("/");
    options.Conventions.AuthorizeFolder("/files");
});

//authentication pipline
builder.Services.AddHttpContextAccessor();
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApp(options =>
                {
                    builder.Configuration.Bind("AzureAd", options);
                    options.Events = new OpenIdConnectEvents
                    {
                        //Tap into this event to add a UserID Claim to a new HttpContext identity
                        OnTokenValidated = context =>
                        {
                            //This query returns the UserID from the DB by sending the email address in the claim from Azure AD
                            string query = "select dbo.A2F_0013_ReturnUserIDForEmail(@Email) as UserID";
                            string connectionString = builder.Configuration.GetValue<string>("ConnectionStrings:DBContext");
                            string signInEmailAddress = context.Principal.FindFirstValue("preferred_username");

                            using (var connection = new SqlConnection(connectionString))
                            {
                                var queryResult = connection.QueryFirst(query, new { Email = signInEmailAddress });

                                var claims = new List<Claim>
                                {
                                    new Claim("UserID", queryResult.UserID.ToString())
                                };

                                var appIdentity = new ClaimsIdentity(claims);

                                context.Principal.AddIdentity(appIdentity);
                            }

                            return Task.CompletedTask;
                        },
                    };

                })
                    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                        .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
                        .AddInMemoryTokenCaches();

//Add Transient Services
builder.Services.AddTransient<IOneDrive, OneDrive>();

builder.Services.AddControllers(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
});

builder.Services.AddRazorPages()
                .AddMicrosoftIdentityUI();

所有控制器中使用:

[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]

临时解决方法:在Graph API调用前执行

var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(initialScopes);

分析与解决方案

配置问题点

你的配置整体符合微软官方示例,但存在两个核心问题:

  • 内存令牌缓存局限性:使用AddInMemoryTokenCaches()时,IIS Express重启、生产环境应用池回收都会清空内存缓存,导致AcquireTokenSilent找不到有效令牌,触发MsalUiRequiredException。
  • 会话与令牌生命周期不同步:应用Cookie会话的过期时间未与Azure AD令牌对齐,出现会话仍有效但令牌已过期/丢失的矛盾情况。

针对性优化方案

1. 替换内存令牌缓存(解决重启后令牌丢失)

生产环境必须使用分布式缓存替代内存缓存,避免应用重启或池回收时丢失令牌。示例以Redis为例:

// 先安装NuGet包:Microsoft.Identity.Web.TokenCache.Redis
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
    .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
    .AddDistributedTokenCaches();

// 配置Redis连接
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
});

2. 修复Claims添加逻辑,避免干扰令牌流程

将OnTokenValidated中的自定义Claims添加逻辑移至ClaimsTransformation,避免破坏MSAL的令牌验证与缓存流程:

// 注册自定义Claims转换服务
builder.Services.AddScoped<IClaimsTransformation, CustomClaimsTransformation>();

// 自定义转换类
public class CustomClaimsTransformation : IClaimsTransformation
{
    private readonly IConfiguration _configuration;

    public CustomClaimsTransformation(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        if (principal.Identity.IsAuthenticated)
        {
            string signInEmailAddress = principal.FindFirstValue("preferred_username");
            string connectionString = _configuration.GetValue<string>("ConnectionStrings:DBContext");
            string query = "select dbo.A2F_0013_ReturnUserIDForEmail(@Email) as UserID";

            using (var connection = new SqlConnection(connectionString))
            {
                var queryResult = connection.QueryFirst(query, new { Email = signInEmailAddress });
                var claims = new List<Claim>
                {
                    new Claim("UserID", queryResult.UserID.ToString())
                };
                var appIdentity = new ClaimsIdentity(claims);
                principal.AddIdentity(appIdentity);
            }
        }
        return Task.FromResult(principal);
    }
}

3. 同步会话与令牌过期时间

配置Cookie认证的过期时间与Azure AD默认的60分钟AccessToken过期时间对齐,开启滑动过期:

builder.Services.ConfigureApplicationCookie(options =>
{
    options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
    options.SlidingExpiration = true; // 用户操作时自动刷新会话过期时间
});

4. 正确处理令牌刷新(无需手动检测过期)

GetAccessTokenForUserAsync内部会自动检查令牌是否过期:若未过期则直接返回缓存令牌,若已过期则自动尝试刷新,失败才会抛出MsalUiRequiredException。因此在Graph API调用前调用该方法是合理的,并非每次都会触发新的令牌请求。

同时建议添加全局异常捕获,自动触发重新认证:

try
{
    // 调用Graph API逻辑
}
catch (MsalUiRequiredException ex)
{
    // 自动触发重新登录流程
    await _tokenAcquisition.ReplyForbiddenWithWwwAuthenticateHeaderAsync(initialScopes, ex);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 11:40:43