集成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调用失败;强制登出后重新登录则恢复正常。 - 生产环境中,用户长时间未操作但浏览器保持打开时也会出现该问题,需强制登出重登才能解决。
疑问:
- 是否配置有误?
- 需添加什么设置来提前触发重登,或无需强制重登即可维持令牌有效性?
- 是否有方法检测令牌是否过期,避免每次调用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
相关产品推荐
相关产品推荐

