无法缓存Graph与SharePoint多令牌,Graph令牌获取失败求助
背景
我们的ASP.Net Core应用集成了Microsoft Graph,支持微软账户登录、日历访问,当前正在实现OneDrive文件选择器,用于用户查看/选择OneDrive及SharePoint站点文件。
应用通过令牌获取机制为GraphServiceClient获取访问令牌,Startup.cs配置如下:
var initialScopes = configuration.GetValue<string>("Authentication:DownstreamApi:Scopes")?.Split(' '); authenticationBuilder.AddMicrosoftIdentityWebApp(configuration.GetSection("Authentication:AzureAd"), OpenIdConnectDefaults.AuthenticationScheme) .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) .AddMicrosoftGraph(configuration.GetSection("Authentication:DownstreamApi")) .AddDistributedTokenCaches(); services.AddDistributedSqlServerCache(options => { options.ConnectionString = distributedCacheConnection.ConnectionString; options.SchemaName = "dbo"; options.TableName = "MicrosoftTokenCache"; options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90); });
使用Graph客户端时,通过自定义SignedInUserTokenProvider从令牌获取服务获取令牌:
var signedInAuthProvider = new BaseBearerTokenAuthenticationProvider(new SignedInUserTokenProvider(_tokenAcquisition)); _signedInUserGraphClient = new GraphServiceClient(signedInAuthProvider);
SignedInUserTokenProvider.cs代码:
public class SignedInUserTokenProvider : IAccessTokenProvider { private readonly ITokenAcquisition _tokenAcquisition; public SignedInUserTokenProvider(ITokenAcquisition tokenAcquisition) { _tokenAcquisition = tokenAcquisition; } public async Task<string> GetAuthorizationTokenAsync(Uri uri, Dictionary<string, object> additionalAuthenticationContext = default, CancellationToken cancellationToken = default) { return await _tokenAcquisition.GetAccessTokenForUserAsync ( new string[] { }, authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme ); } public AllowedHostsValidator AllowedHostsValidator { get; } }
这部分功能运行正常,可完整使用Graph各项功能。
我们已实现OneDrive文件选择器,该功能需两个不同令牌(分别用于OneDrive和SharePoint),调用令牌获取服务时使用不同作用域:
OneDrive令牌调用代码:
var pickerSite = new Uri("https://companyname-my.sharepoint.com"); string[] scopes = new string[] { $"https://{pickerSite.DnsSafeHost}/AllSites.FullControl" }; var token = await _tokenAcquisition.GetAccessTokenForUserAsync ( scopes, authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme ); return token;
SharePoint令牌调用仅将作用域地址改为https://companyname.sharepoint.com,功能运行正常,可访问OneDrive和SharePoint。
问题现象
三个令牌(一个Graph令牌、两个SharePoint令牌)均存储于分布式SQL缓存中,获取SharePoint令牌后,Graph令牌获取功能失效,报错:
Multiple access tokens found for matching authority, client_id, user and scopes.
{"The cache contains multiple tokens satisfying the requirements. Try to clear token cache. "}
我们预期每个用户可缓存多个不同作用域的令牌,对应不同资源访问需求,但当前无法实现,需要解决令牌缓存冲突问题。
原因分析
问题核心在于SignedInUserTokenProvider的GetAuthorizationTokenAsync方法中,调用GetAccessTokenForUserAsync时传入了空的作用域数组。MSAL查找缓存令牌时,空作用域会被视为匹配所有令牌的作用域条件,而此时缓存中已存在多个同用户、同客户端ID的令牌(Graph和两个SharePoint的),导致无法确定返回哪一个,从而抛出冲突错误。
此外,MSAL的令牌缓存按用户、客户端ID、权限(作用域)、资源区分条目,空作用域会跳过作用域匹配逻辑,引发歧义。
解决方案
1. 修改SignedInUserTokenProvider,传入明确的Graph作用域
不要传入空数组,而是使用配置中定义的Graph作用域,或者直接指定Graph所需的作用域。修改后的代码如下:
public class SignedInUserTokenProvider : IAccessTokenProvider { private readonly ITokenAcquisition _tokenAcquisition; private readonly IConfiguration _configuration; public SignedInUserTokenProvider(ITokenAcquisition tokenAcquisition, IConfiguration configuration) { _tokenAcquisition = tokenAcquisition; _configuration = configuration; } public async Task<string> GetAuthorizationTokenAsync(Uri uri, Dictionary<string, object> additionalAuthenticationContext = default, CancellationToken cancellationToken = default) { var graphScopes = _configuration.GetValue<string>("Authentication:DownstreamApi:Scopes")?.Split(' ') ?? new string[0]; return await _tokenAcquisition.GetAccessTokenForUserAsync ( graphScopes, authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme ); } public AllowedHostsValidator AllowedHostsValidator { get; } }
2. 确保所有令牌请求使用明确作用域
无论是Graph还是SharePoint/OneDrive的令牌请求,都必须传入非空且明确的作用域数组,让MSAL能准确匹配缓存中的对应令牌,避免歧义。
3. 临时清理冲突缓存
先手动清理分布式SQL缓存中的MicrosoftTokenCache表数据,消除已存在的冲突令牌,再测试修复后的代码。
额外注意事项
- 不同资源的令牌作用域要明确区分:Graph的作用域以
https://graph.microsoft.com/开头,SharePoint/OneDrive的作用域对应各自的站点域名。 - 确保
AddMicrosoftGraph配置的作用域与GetAccessTokenForUserAsync传入的作用域一致,避免不必要的令牌请求。
内容的提问来源于stack exchange,提问作者sjones

