使用MSAL生成令牌遇问题:无权限范围且反复跳转登录页
ADAL迁移至MSAL后令牌无权限范围且持续跳转登录问题修复
背景
原代码基于ADAL(Microsoft.IdentityModel.Clients.ActiveDirectory)实现了多场景的令牌获取逻辑,包含用户断言、服务到服务、静默获取三种分支:
private async Task<string> GetAuthorizationHeaderCore(string resource) { var objectId = (this.httpContextAccessor.HttpContext.User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value; var userTenantId = (this.httpContextAccessor.HttpContext.User.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid"))?.Value; var distributedTokenCache = new DistributedTokenCache( this.scopedCacheFactory, this.dataProtectionProvider, this.telemetryClient, userTenantId + ":" + objectId ); var clientCredential = new ClientCredential(this.authOptions.ClientId, this.authOptions.ClientSecret); var authenticationContext = new AuthenticationContext(this.authOptions.AADInstance + this.authOptions.TenantId, distributedTokenCache); AuthenticationResult authenticationResult; var accessToken = await GetTokenWithFallBackAsync(this.httpContextAccessor.HttpContext, "access_token"); if (accessToken != null) { if (this.httpContextAccessor?.HttpContext?.Items?["isAuthenticatedService"] as bool? == true) { authenticationResult = await authenticationContext.AcquireTokenAsync(resource, this.backgroundServiceClientCredential).ConfigureAwait(false); } else { var userPrincipalName = this.httpContextAccessor.HttpContext.User.Identity.Name; var userAssertion = new UserAssertion(accessToken, "urn:ietf:params:oauth:grant-type:jwt-bearer", userPrincipalName); authenticationResult = await authenticationContext.AcquireTokenAsync(resource, clientCredential, userAssertion).ConfigureAwait(false); } } else { // Just hope we already have a token (openid case) var userIdentifier = new UserIdentifier(objectId, UserIdentifierType.UniqueId); authenticationResult = await authenticationContext.AcquireTokenSilentAsync(resource, clientCredential, userIdentifier); } return authenticationResult.CreateAuthorizationHeader(); }
迁移至MSAL(Microsoft.Identity.Client)后,现有代码仅实现了客户端凭证流,未覆盖原有的多场景逻辑,导致生成的令牌无权限范围、系统持续跳转登录:
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(this.Configuration["Authentication:AzureAd:ClientId"]) .WithRedirectUri(x.Properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey]) .WithClientSecret(this.Configuration["Authentication:AzureAd:ClientSecret"]) .WithAuthority(this.Configuration["Authentication:AzureAd:AADInstance"] + this.Configuration["Authentication:AzureAd:TenantId"]) .Build(); var scopes = new string[] { $"{graphApiResource}/.default" }; AuthenticationResult authenticationResult; authenticationResult = await app.AcquireTokenForClient(scopes).ExecuteAsync();
问题诊断
- 场景覆盖不全:原ADAL代码处理了用户委托、服务到服务、静默获取三种场景,现有MSAL代码仅使用
AcquireTokenForClient(客户端凭证流),无法处理用户上下文相关的令牌请求,导致权限范围缺失。 - 令牌缓存缺失:原ADAL集成了分布式令牌缓存,现有MSAL代码未配置缓存,导致无法复用已有令牌,触发重复登录流程。
- 用户身份关联不足:静默获取场景未关联用户唯一标识,无法从缓存中匹配用户令牌。
修复方案
1. 集成分布式令牌缓存
MSAL通过IDistributedTokenCacheProvider实现分布式缓存,需先注册该服务(示例基于ASP.NET Core):
// 在Program.cs或Startup.cs中注册缓存服务 builder.Services.AddDistributedMemoryCache(); // 可替换为Redis等分布式缓存 builder.Services.AddMsalDistributedTokenCache();
2. 实现多场景匹配的MSAL令牌获取逻辑
对应原ADAL的三个分支场景,在MSAL中实现对应的令牌获取方法:
private async Task<string> GetAuthorizationHeaderCore(string resource) { var httpContext = this.httpContextAccessor.HttpContext; var objectId = httpContext.User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value; var userTenantId = httpContext.User.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value; var clientId = this.Configuration["Authentication:AzureAd:ClientId"]; var clientSecret = this.Configuration["Authentication:AzureAd:ClientSecret"]; var authority = $"{this.Configuration["Authentication:AzureAd:AADInstance"]}{this.Configuration["Authentication:AzureAd:TenantId"]}"; // 构建ConfidentialClientApplication并关联缓存 var app = ConfidentialClientApplicationBuilder.Create(clientId) .WithClientSecret(clientSecret) .WithAuthority(authority) .Build(); // 绑定分布式缓存到MSAL应用 var tokenCacheProvider = httpContext.RequestServices.GetRequiredService<IDistributedTokenCacheProvider>(); await tokenCacheProvider.InitializeAsync(app.UserTokenCache); AuthenticationResult authenticationResult; var scopes = new string[] { $"{resource}/.default" }; // 按目标资源生成scope var accessToken = await GetTokenWithFallBackAsync(httpContext, "access_token"); if (accessToken != null) { if (httpContext.Items["isAuthenticatedService"] as bool? == true) { // 服务到服务场景:客户端凭证流 authenticationResult = await app.AcquireTokenForClient(scopes) .ExecuteAsync(); } else { // 用户委托场景:On-Behalf-Of流 var userPrincipalName = httpContext.User.Identity.Name; var userAssertion = new UserAssertion(accessToken, "urn:ietf:params:oauth:grant-type:jwt-bearer", userPrincipalName); authenticationResult = await app.AcquireTokenOnBehalfOf(scopes, userAssertion) .ExecuteAsync(); } } else { // 静默获取场景:从缓存中读取用户令牌 var userAccount = await app.GetAccountAsync($"{objectId}.{userTenantId}"); if (userAccount == null) { throw new InvalidOperationException("No user account found in token cache."); } authenticationResult = await app.AcquireTokenSilent(scopes, userAccount) .ExecuteAsync(); } return authenticationResult.CreateAuthorizationHeader(); }
关键说明
- Scope格式:MSAL中需使用
{resource}/.default格式表示针对目标资源的全权限(与ADAL的resource参数对应)。 - On-Behalf-Of流:对应原ADAL的User Assertion场景,需确保应用已在Azure AD中配置“委派权限”,并获得用户授权。
- 令牌缓存:通过
IDistributedTokenCacheProvider复用令牌,避免重复登录和令牌请求。
内容的提问来源于stack exchange,提问作者Neo
相关产品推荐
相关产品推荐

