本文可帮助你诊断和解决 Microsoft.Identity.Web 中的令牌缓存问题。 令牌缓存问题可能会导致身份验证失败、性能下降或意外登录提示。 有关 Microsoft.Identity.Web 中令牌缓存工作原理的概述,请参阅 Token 缓存概述。
先决条件
在进行故障排除之前,请确认以下内容:
- 你使用的是 受支持版本的 Microsoft.Identity.Web。
- 应用程序在
Program.cs或Startup.cs中配置了令牌缓存。 - 你有权访问应用程序日志,如果适用,则有权访问分布式缓存基础结构。
启用令牌缓存日志和诊断
启用详细日志记录作为第一个诊断步骤。 Microsoft。Identity.Web 使用 ASP.NET Core日志记录基础结构,并通过 Microsoft 身份验证库 (MSAL) 发出事件。
启用 MSAL 日志记录
将标识库的日志级别设置为 Debug 于 appsettings.json 中:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Identity.Web": "Debug",
"Microsoft.IdentityModel": "Debug"
}
}
}
订阅 MSAL 缓存事件
订阅 MSAL 令牌缓存通知事件以跟踪缓存命中、未命中和序列化活动:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.OnL2CacheFailure = (ex) =>
{
logger.LogWarning(ex, "L2 cache failure encountered.");
// Return true to allow the operation to continue despite the cache failure.
// Return false to propagate the exception.
return true;
};
});
监视缓存指标
对于生产监视,请跟踪以下关键指标:
- 缓存命中率 - 命中率低表示未从缓存中检索令牌。
- L2 缓存延迟 - 高延迟表明分布式缓存连接或性能问题。
- 缓存序列化错误 - 读取或写入过程中的错误表示损坏或版本不匹配。
- 内存消耗 - 持续增长可能表示缺少逐出策略。
分布式缓存 (L2) 连接失败
症状
应用程序日志显示连接超时错误或间歇性身份验证失败。 用户体验登录延迟;你会看到异常情况,例如:
Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache:
StackExchange.Redis.RedisConnectionException:
No connection is active/available to service this operation.
或者对于SQL Server分布式缓存:
Microsoft.Data.SqlClient.SqlException:
A network-related or instance-specific error occurred while
establishing a connection to SQL Server.
原因
分布式缓存后备存储(Redis 或 SQL Server)无法访问。 常见原因包括:
- 连接字符串不正确或访问凭据过期。
- 阻止来自应用主机的连接的网络防火墙规则。
- 缓存服务已关闭或正在维护。
- 客户端和缓存服务器之间的 SSL/TLS 配置不匹配。
诊断步骤
按照以下步骤确定连接失败:
- 验证连接。 在应用程序主机中,使用
Test-NetConnection或redis-cli测试与 Redis 或 SQL Server 的连接。 - 检查连接字符串。 请确认连接字符串与缓存服务器的主机名、端口和凭据相匹配。
- 查看防火墙规则。 在Azure中,验证应用服务或虚拟网络是否可以访问缓存资源。
- 检查服务运行状况。 在Azure门户中,查看Azure Cache for Redis或 SQL 数据库实例的运行状况和指标。
解决方案
步骤 1:更正连接字符串
请验证您的appsettings.json中的连接字符串:
{
"ConnectionStrings": {
"Redis": "your-redis-instance.redis.cache.windows.net:6380,password=your-access-key,ssl=True,abortConnect=False"
}
}
重要
在 Redis 连接字符串 中设置 abortConnect=False。 此设置允许应用程序在暂时性连接失败后自动重新连接,而不是立即抛出异常。
步骤 2:配置重试和复原能力
配置 OnL2CacheFailure 回调,以便在分布式缓存暂时不可用时,应用程序正常降级:
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.OnL2CacheFailure = (ex) =>
{
// Log the failure for monitoring and alerting.
logger.LogWarning(ex, "Distributed token cache is unavailable. " +
"Falling back to in-memory cache.");
return true; // Continue without the L2 cache.
};
// Set a timeout to avoid blocking the request pipeline.
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
});
步骤 3:打开防火墙规则
如果应用程序在Azure 应用服务中运行,并且缓存位于虚拟网络中,请将应用服务出站 IP 地址添加到缓存防火墙允许列表。
缓存反序列化错误
症状
在升级 Microsoft.Identity.Web 或 MSAL.NET 后,应用程序在从分布式缓存读取时会引发反序列化异常。 用户必须再次登录,你会看到异常,例如:
System.Text.Json.JsonException:
The JSON value could not be converted to the expected type.
或者:
Microsoft.Identity.Client.MsalClientException:
Error code: json_parse_failed
原因
令牌缓存序列化格式在库版本之间更改。 以前版本缓存的令牌不能由新版本反序列化。 此问题最常发生在 MSAL.NET 或 Microsoft.Identity.Web 的主版本升级期间。
解决方案
选项 A:清除缓存
最简单的解决方法是清除分布式缓存中的所有条目。 用户重新进行身份验证一次,后续令牌以新格式写入。
刷新 Redis 缓存:
redis-cli FLUSHDB
或者清除SQL Server分布式缓存表:
DELETE FROM [dbo].[TokenCache];
注释
清除缓存会导致所有活动用户重新进行身份验证。 如果应用程序为大型用户群提供服务,请在维护时段内规划此操作。
选项 B:正常处理反序列化错误
将缓存适配器配置为将反序列化失败视为缓存未命中的情况,而非将其视为致命错误。
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.OnL2CacheFailure = (ex) =>
{
if (ex is JsonException or MsalClientException)
{
logger.LogWarning(ex, "Cache deserialization failed. " +
"Treating as cache miss.");
return true;
}
return false; // Propagate unexpected errors.
};
});
使用此方法时,受影响的缓存条目会在用户重新进行身份验证时自动替换,无需手动刷新缓存。
跨服务器的加密密钥不匹配
症状
尽管分布式缓存正常工作,但多实例部署中也会出现反序列化错误。 一个服务器实例缓存的令牌不能由另一个服务器实例读取。 日志中会显示 json_parse_failed 或 IDW10802 错误。
原因
启用缓存加密时(options.Encrypt = true),Microsoft。Identity.Web 使用 ASP.NET Core 数据保护来加密缓存条目。 默认情况下,每个服务器实例都会生成自己的数据保护密钥,因此一个实例无法解密另一个实例编写的条目。
解决方案
配置 ASP.NET Core数据保护以在所有服务器实例之间共享加密密钥。
Option A:Azure Blob 存储 + Azure 密钥保管库(建议用于Azure部署)
using Microsoft.AspNetCore.DataProtection;
using Azure.Identity;
builder.Services.AddDataProtection()
.PersistKeysToAzureBlobStorage(
new Uri("https://yourstorageaccount.blob.core.windows.net/dataprotection/keys.xml"),
new DefaultAzureCredential())
.ProtectKeysWithAzureKeyVault(
new Uri("https://yourkeyvault.vault.azure.net/keys/dataprotection-key"),
new DefaultAzureCredential());
builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.Encrypt = true;
});
此配置将数据保护密钥环存储在Azure Blob 存储中,并使用Azure 密钥保管库保护静态密钥。 访问同一 Blob 和密钥的所有应用程序实例都可以加密和解密彼此的缓存条目。
选项 B:具有证书保护的共享文件系统
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\keys"))
.ProtectKeysWithCertificate(certificate);
小窍门
在轮换数据保护证书时,使用UnprotectKeysWithAnyCertificate 来包括当前证书和以前的证书。 这允许解密在轮换时段内使用旧证书保护的密钥。
内存中缓存的内存增长
症状
随着时间推移,应用程序内存消耗稳步增长。 如果应用程序在具有固定内存限制的容器或应用服务计划中运行,它最终会重启或引发 OutOfMemoryException。 监控表明托管堆在没有垃圾回收机制的情况下增长。
原因
使用 AddInMemoryTokenCaches() 没有大小限制会导致缓存增长不受限制。 这种情况在为许多用户提供服务的应用程序中尤其存在问题,因为每个用户的令牌条目无限期地消耗内存。
默认情况下, MemoryCache 不会强制实施最大大小,也不会逐出条目,除非设置了过期策略。
解决方案
选项 A:设置大小限制和滑动过期
使用过期策略配置内存中缓存:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
services.Configure<MsalMemoryTokenCacheOptions>(options =>
{
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
options.SlidingExpiration = TimeSpan.FromHours(2);
});
使用这些设置时,条目将在 12 小时后过期,且不受访问影响,而闲置 2 小时的条目将更早被移除。
选项 B:切换到分布式缓存
对于具有多个并发用户的应用程序,内存中缓存不会缩放。 切换到分布式缓存,例如 Redis:
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
});
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
分布式缓存从应用程序进程卸载内存,在重启时保留令牌,并支持多实例部署。
选项 C:使用 L1/L2 混合体系结构
Microsoft。Identity.Web 支持将快速内存中 L1 缓存与持久性分布式 L2 缓存相结合的混合方法。 配置 L1/L2 混合缓存:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.L1CacheOptions = new MsalMemoryTokenCacheOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
SlidingExpiration = TimeSpan.FromMinutes(2)
};
});
在使用 L1/L2 缓存时,频繁访问的令牌将通过内存 (L1) 提供,其延迟为低于一毫秒。 L2 缓存提供持久性和跨实例一致性。 L1 缓存使用短过期来限制内存增长。
重复的 MFA 或同意提示
症状
即使用户最近完成了这些步骤,用户也会反复提示进行多重身份验证(MFA)或同意。 应用程序在缓存中找不到现有令牌。
原因
当令牌缓存查找无法匹配当前用户帐户的缓存条目时,会出现此问题。 常见原因包括:
- 缓存密钥不同于存储令牌时使用的密钥。 如果
HomeAccountId或租户上下文发生更改,则可能会出现这种情况。 - 应用程序在具有内存中缓存的负载均衡器后面运行多个实例,请求路由到没有用户令牌的实例。
- 请求的声明或范围已更改,因此缓存的令牌不满足新要求。
- 未启用会话相关性,因此用户会路由到缺少其缓存令牌的不同实例。
诊断步骤
按照以下步骤确定缓存中找不到令牌的原因:
- 检查缓存类型。 如果在多实例部署中使用
AddInMemoryTokenCaches(),则缓存在一个实例上的令牌在另一个实例上不可用。 切换到分布式缓存。 - 验证帐户标识符。 启用调试级别的日志记录并搜索
HomeAccountId。 确认标识符在请求之间是一致的。 - 检查范围。 确认请求
GetAccessTokenForUserAsync的范围与最初同意的范围匹配。 范围不匹配会导致 MSAL 请求新令牌。 - 查看条件访问策略。 Microsoft Entra ID条件访问策略,要求对特定资源进行分步身份验证会导致与缓存无关的其他提示。
解决方案
步骤 1:切换到分布式缓存
如果应用程序运行多个实例,请使用分布式缓存跨实例共享令牌:
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
});
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
步骤 2:验证一致的作用域
确保获取令牌时请求的范围与身份验证期间配置的作用域匹配:
// In authentication setup — initial scopes.
.EnableTokenAcquisitionToCallDownstreamApi(new[] { "User.Read", "Mail.Read" })
// When acquiring a token — use the same scopes.
var token = await tokenAcquisition.GetAccessTokenForUserAsync(
new[] { "User.Read", "Mail.Read" });
步骤 3:启用会话相关性(临时解决方法)
如果无法立即切换到分布式缓存,请在负载均衡器上启用会话相关性(粘滞会话)。 会话相关性将用户的请求路由到同一实例。 此方法是一种暂时的解决方法,具有可伸缩性限制。
缓存性能问题
症状
令牌检索速度缓慢,下游 API 调用延迟增加。 监视显示令牌获取请求的平均响应时间较高。 延迟不是来自标识提供者, 令牌是从缓存中提供的。
原因
缓存性能问题通常是由于以下原因:
- 高 L2 缓存延迟。 分布式缓存负载过大,地理上与应用程序相距很远,或者使用大小不足的服务层。
- 大型令牌缓存条目。 为每个用户缓存多个资源的令牌的应用程序可以生成读取和写入速度缓慢的大型序列化缓存条目。
- 无 L1 缓存。 每个令牌获取都会通过网络转到分布式缓存,即使对于经常使用的令牌也是如此。
解决方案
步骤 1:启用 L1 内存中缓存
L1 缓存将经常访问的令牌存储在进程内存中,从而避免到 L2 的网络往返:
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.L1CacheOptions = new MsalMemoryTokenCacheOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
SlidingExpiration = TimeSpan.FromMinutes(2)
};
});
使用此配置,来自 L1 的令牌具有亚毫秒级延迟。 不在 L1 中的令牌回退到 L2 分布式缓存。
步骤 2:优化分布式缓存层
如果 L2 缓存延迟较高,请考虑以下操作:
- 纵向扩展 Redis 实例。 移动到更高的层(例如,从基本层移动到标准层或 Azure Cache for Redis 中的高级层),以获得更多的吞吐量和更低的延迟。
- 启用异地复制。 如果应用程序为多个区域中的用户提供服务,请使用Azure Cache for Redis异地复制,使缓存接近每个区域的计算。
- 查看网络配置。 使用专用链接或 VNet 集成来减少应用程序与缓存之间的网络跃点。
步骤 3:减少序列化令牌大小
如果令牌缓存条目很大,请查看应用程序是否请求令牌以获取的资源超过必要的资源。 每个唯一的资源和范围组合都会增加缓存条目的大小。 尽可能合并 API 调用,以减少每个用户缓存的不同访问令牌数。
Redis 缓存逐出
症状
系统间歇性地提示用户重新进行身份验证,且没有基于令牌过期的模式。 Redis 监控显示 evicted_keys 增加,并且 used_memory 正在接近 maxmemory 阈值。
原因
当 Redis 达到其 maxmemory 限制时,它会根据配置的 maxmemory-policy 淘汰键。 默认策略 (volatile-lru) 将逐出最近使用最少的密钥,这些密钥已过期。 如果 Redis 实例与其他应用程序数据共享,令牌缓存条目会与其他数据争夺空间,并可能被过早删除。
解决方案
步骤 1:检查逐出策略
检查当前的逐出策略:
redis-cli CONFIG GET maxmemory-policy
对于令牌缓存,volatile-lru(默认值)是适合的,因为令牌缓存条目具有到期设置。 但是,如果未过期的其他数据占用内存,则首先逐出令牌条目。
步骤 2:使用专用 Redis 实例
使用专用 Redis 实例将令牌缓存与其他应用程序数据隔离:
{
"ConnectionStrings": {
"RedisTokenCache": "token-cache-redis.redis.cache.windows.net:6380,password=...,ssl=True,abortConnect=False",
"RedisAppData": "app-data-redis.redis.cache.windows.net:6380,password=...,ssl=True,abortConnect=False"
}
}
// Register the token cache Redis instance specifically for distributed caching.
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("RedisTokenCache");
});
步骤 3:增加 Redis 内存限制
如果专用实例不可行,请增加 maxmemory 设置。 在Azure Cache for Redis中,纵向扩展到更高的层或增加缓存大小。
步骤 4:设置适当的缓存条目过期时间
设置合理的过期时间,以便在内存耗尽之前删除过时的条目:
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
options.SlidingExpiration = TimeSpan.FromHours(2);
});
SQL 分布式缓存表增长
症状
SQL 分布式缓存表会持续增长,占用磁盘空间。 针对缓存表的数据库查询随时间推移缓慢,你可能会看到有关表大小或存储限制的警告。
原因
SQL Server分布式缓存(Microsoft.Extensions.Caching.SqlServer)不会自动删除过期的条目。 过期的条目将一直保留,直到显式清除,这会导致表无限增长、查询性能下降以及存储消耗增加。
解决方案
步骤 1:设置定期清理作业
创建SQL Server 代理作业或计划任务以定期删除过期条目:
-- Delete expired entries from the SQL distributed cache table.
-- Schedule this query to run every 30 minutes.
DELETE FROM [dbo].[TokenCache]
WHERE ExpiresAtTime < GETUTCDATE();
小窍门
在 Azure SQL 数据库 中,在 SQL Server 代理 不可用的情况下,请使用 Azure 自动化、Azure Functions 计时器触发器或弹性作业来计划清理。
步骤 2:添加索引以高效清理
如果缓存表在过期列上还没有索引,请添加一个索引来加快删除操作:
CREATE NONCLUSTERED INDEX IX_TokenCache_ExpiresAtTime
ON [dbo].[TokenCache] (ExpiresAtTime);
步骤 3:监视表大小
添加监控来跟踪随着时间推移的行数和表大小。
SELECT
COUNT(*) AS TotalEntries,
COUNT(CASE WHEN ExpiresAtTime < GETUTCDATE() THEN 1 END) AS ExpiredEntries,
COUNT(CASE WHEN ExpiresAtTime >= GETUTCDATE() THEN 1 END) AS ActiveEntries
FROM [dbo].[TokenCache];
步骤 4:考虑切换到 Redis
如果管理 SQL 缓存清理是繁重的,请切换到 Redis,它通过内置的 TTL 机制自动处理过期:
// Replace SQL distributed cache with Redis.
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
});
常规疑难解答技巧
如果问题与本文中的特定方案不匹配,请使用这些提示。
验证缓存是否正在使用
添加临时日志记录以确认令牌是从缓存中读取和写入的:
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.Encrypt = false; // Disable encryption temporarily for debugging only.
options.OnL2CacheFailure = (ex) =>
{
logger.LogError(ex, "L2 cache operation failed.");
return true;
};
});
检查多项缓存注册纪录
如果你的启动代码中多次调用 AddInMemoryTokenCaches() 或 AddDistributedTokenCaches(),则最后一次注册会生效。 验证是否只注册了一个缓存类型。
查看令牌生存期
访问令牌的生存期有限(通常为 60-90 分钟)。 如果用户报告在此时间段后重新进行身份验证,则行为是预期的,而不是缓存问题。 刷新令牌以无提示方式获取新的访问令牌,并存储在缓存中。 如果刷新令牌缺失或已过期,用户必须重新进行身份验证。
使用干净缓存进行测试
诊断问题时,清除缓存以排除损坏或过时的条目:
- 内存中缓存: 重启应用程序。
-
Redis: 在缓存数据库上运行
FLUSHDB。 - SQL Server: 从缓存表中删除所有行。
应用程序重启后令牌缓存为空
症状
每次重启或重新部署应用程序后,用户都必须重新进行身份验证。 分布式缓存显示为空或令牌不会持久化。
原因
在生产环境中使用内存中缓存(AddInMemoryTokenCaches())或非永久性分布式内存缓存(AddDistributedMemoryCache())时,通常会发生此问题。 两个选项都不会在应用程序重启时保留令牌。
AddDistributedMemoryCache() 注册一个在内存中存储数据的 IDistributedCache 实现。 尽管“分布式”名称,但它不会在外部保留数据,并且仅用于开发和测试。
解决方案
切换到持久性分布式缓存:
// Register a persistent cache (Redis example).
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "MyApp_";
});
// Use distributed token caches instead of in-memory.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
警告
不要与持久性分布式缓存混淆 AddDistributedMemoryCache() 。 将 AddStackExchangeRedisCache() (Redis)、AddDistributedSqlServerCache() (SQL Server) 或其他持久性 IDistributedCache实现用于生产工作负荷。