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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 06:43:26