解決 Microsoft.Identity.Web 的令牌快取問題。

本文協助你診斷並解決 Microsoft.Identity.Web 中的令牌快取問題。 令牌快取問題可能導致認證失敗、效能下降或出現意外的登入提示。 若要了解 Microsoft.Identity.Web 中的代幣快取運作方式,請參閱代幣快取概覽。

先決條件

在排查問題前,請確認以下事項:

  • 你使用的是受支援的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 設定不符。

診斷步驟

請依照以下步驟辨識連線故障:

  1. 確認連線。 從應用程式主機,使用 Test-NetConnection(PowerShell)或 redis-cli,測試連接到 Redis 或 SQL Server。
  2. 檢查一下連接字串。 確認 連接字串 是否與快取伺服器的主機名稱、埠號及憑證相符。
  3. 檢視防火牆規則。 在 Azure 中,確認應用程式服務或虛擬網路能存取快取資源。
  4. 檢查服務健全狀況。 在 Azure 入口網站中,檢視你的 Azure Cache for Redis 或 SQL Database 實例的健康狀況與指標。

解決方案

步驟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 App 服務 中執行且快取位於虛擬網路中,請將 App Service 的出站 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 資料保護,讓加密金鑰在所有伺服器實例間共享。

選項A:Azure Blob 儲存體 + Azure Key Vault(建議用於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 Key Vault 保護靜止狀態的金鑰。 所有存取相同 blob 和金鑰的應用程式實例都能互相加密和解密彼此的快取條目。

選項 B:具備憑證保護的共享檔案系統

builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\keys"))
    .ProtectKeysWithCertificate(certificate);

小提示

在輪換資料保護憑證時,請同時包含 UnprotectKeysWithAnyCertificate 目前及先前的憑證。 這允許解密在輪換期間被舊憑證保護的金鑰。

記憶體隨著記憶體內快取而成長

癥狀

應用程式記憶體的消耗會隨時間穩定成長。 如果應用程式在容器或 App Service 計畫中執行,且記憶體限制固定,最終會重啟或拋 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)或同意。 應用程式無法在快取中找到現有的識別碼。

原因

此問題發生在令牌快取查詢未能將快取項目與目前使用者帳號匹配時。 常見的原因包括:

  • 快取金鑰與代幣儲存時使用的金鑰不同。 這種情況可能在 HomeAccountId 或租戶情境發生變化時出現。
  • 應用程式會在負載平衡器後方運行多個實例,並透過記憶體快取,請求會路由到沒有使用者憑證的實例。
  • 請求的宣示或範圍改變了,快取的令牌無法滿足新要求。
  • 會話親和功能未啟用,使用者會路由到沒有快取標記的其他實例。

診斷步驟

請依照以下步驟找出為何無法在緩存中找到令牌的原因:

  1. 檢查快取類型。 如果您在多重實例部署中使用 AddInMemoryTokenCaches() ,一個實例快取的權杖在另一個實例上是無法取得的。 切換到分散式快取。
  2. 請確認帳戶識別碼。 啟用除錯層級日誌並搜尋 HomeAccountId。 確認識別碼在不同請求間一致。
  3. 檢查內視鏡。 確認 GetAccessTokenForUserAsync 所要求的範圍是否與最初同意的範圍相符。 作用域不符會導致 MSAL 請求新的標記。
  4. 檢視條件存取政策。 Microsoft Entra ID 條件存取政策要求特定資源進行逐步驗證,這會產生一些與快取無關的額外提示。

解決方案

步驟 1:切換到分散式快取

如果你的應用程式執行多個實例,使用分散式快取在實例間共享令牌:

services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = Configuration.GetConnectionString("Redis");
});

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(Configuration)
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDistributedTokenCaches();

步驟二:確認瞄準鏡的一致性

確保你在取得憑證時所請求的範圍與認證時設定的範圍相符:

// 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 從 Basic 升級為標準或高級),以獲得更高的吞吐量和更低的延遲。
  • 啟用異地複寫。 如果您的應用程式向多個區域的使用者提供服務,請使用 Azure Cache for Redis 的地理複寫功能,以便快取更加接近每個區域的運算資源。
  • 檢視網路設定。 使用 Private Link 或 VNet 整合來減少應用程式與快取之間的網路跳躍。

步驟 3:減少序列化的代幣大小

若令牌快取項目較大,請檢視應用程式是否請求比必要資源更多的令牌。 每種獨特的資源與範圍組合都會增加快取項目的大小。 盡可能整合 API 呼叫,以減少每位使用者快取的不同存取權杖數量。

Redis 快取清除

癥狀

使用者會間歇性地被提示重新認證,且無基於憑證到期的模式。 Redis 監控顯示evicted_keys正在增加,並且used_memory接近maxmemory的上限。

原因

當 Redis 達到 maxmemory 限制時,會根據配置中的maxmemory-policy進行金鑰淘汰。 預設政策volatile-lru()會移除最近使用最少且過期的金鑰。 若 Redis 實例與其他應用程式資料共享,標記快取項目會爭奪空間,可能會被提前驅逐。

解決方案

步驟一:查看驅逐政策

請查看目前的驅逐政策:

redis-cli CONFIG GET maxmemory-policy

對於令牌快取,預設的 volatile-lru 是合適的,因為令牌快取條目具有過期時間。 然而,若其他不含到期日的資料佔用記憶體,則會先逐出 token 項目。

步驟二:使用專用的 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)不會自動移除過期條目。 過期的條目會持續存在,直到明確清除,導致資料表無限成長、查詢效能下降及儲存空間消耗。

解決方案

步驟一:安排定期清理工作

建立 SQL Server Agent 工作或排程任務,以定期移除過期條目:

-- 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 Database 中,因為沒有 SQL Server Agent,可以使用 Azure 自動化、帶有計時器觸發器的 Azure Functions,或 Elastic Jobs 來排程清理。

步驟二:新增索引以提升清理效率

如果快取表的到期欄還沒有索引,請新增一個以加快刪除操作:

CREATE NONCLUSTERED INDEX IX_TokenCache_ExpiresAtTime
ON [dbo].[TokenCache] (ExpiresAtTime);

步驟三:監控桌面尺寸

新增監控功能以追蹤資料列數與資料表大小隨時間變化:

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];

步驟四:考慮轉換到 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 實作。