MSAL Node 中的令牌缓存

当 MSAL Node 获取令牌时,它会将其缓存在内存中以供将来使用。 MSAL Node 会为你管理令牌的生命周期,并自动刷新令牌。 像 acquireTokenSilent() 这样的 API 可从给定账户的缓存中检索访问令牌:

MSAL 不会出于安全原因公开刷新令牌。 请参阅常见问题解答,了解如何获取刷新令牌。

安全地使用客户端机密

不应对客户端机密进行硬编码。 dotenv npm 包可用于将机密存储在 .env 文件(位于项目的根目录中)中,该文件应包含在 .gitignore 中,以防止意外上传机密。

const msal = require('@azure/msal-node');
require('dotenv').config(); // process.env now has the values defined in a .env file

// Create msal application object
const cca = new msal.ConfidentialClientApplication({
    auth: {
        clientId: "Enter_the_Application_Id_Here", // e.g. "00001111-aaaa-2222-bbbb-3333cccc4444" (guid)
        authority: "https://login.microsoftonline.com/Enter_the_Tenant_Info_Here", // e.g. "common" or your tenantId (guid)
        clientSecret: process.env.clientSecret // obtained during app registration
    }
});

/**
* acquireToken* APIs return an account object containing the "homeAccountId"
* you should keep a record of this in your app and use it later on when calling acquireTokenSilent
*/
const someUserHomeAccountId = "Enter_User_Home_Account_Id";

const msalTokenCache = cca.getTokenCache();
const account = await msalTokenCache.getAccountByHomeId(someUserHomeAccountId);

const silentTokenRequest = {
    account: account,
    scopes: ["User.Read"],
};

cca.acquireTokenSilent(silentTokenRequest).then((response) => {
    // do something with response
}).catch((error) => {
    // catch and handle errors
});

在生产环境中,你很可能希望序列化并保留令牌缓存。 根据应用程序的类型,可以:

  • 桌面应用、控制台应用(公共客户端应用(PCA)):
    • 使用 MSAL 节点扩展,该扩展在 Windows、Linux 和 Mac OS 上提供持久性和静态加密解决方案
  • Web 应用、Web API、守护程序应用(机密客户端应用(CCA)):
    • MSAL 的内存令牌缓存无法扩展到生产环境。 使用 分布式令牌缓存 模式在所选的存储环境中保留缓存(Redis、MongoDB、SQL 数据库等)-keep 请记住,可以结合使用这些缓存, 例如 Redis 般的内存缓存作为第一层持久性,将 SQL 数据库作为第二层、更稳定的持久性层)

内存中缓存

MSAL 维护内存缓存。 内存中缓存代表应用程序缓存状态。 内存中缓存的生存期与 MSAL 应用程序对象相同。 如果使用 MSAL 的进程重启,则进程生命周期完成后,将清除缓存。 如果内存中缓存为空,并且没有用于从中还原缓存的永久性缓存,则用户必须重新进行身份验证。 当这种情况发生时,如果用户仍与 Microsoft Entra ID 保持活动会话,他们可能无需任何提示即可重新进行身份验证,但这仍然会降低用户体验。 服务到服务场景(即客户端凭据流、代理流)也会受到影响,因为从 Microsoft Entra ID 获取令牌涉及 HTTP 请求,比从缓存获取令牌慢得多。

请注意,内存中缓存对于服务器端应用程序来说不可缩放,在缓存中保存几个 100 个令牌后性能会降低。 对于 Web 应用和 Web API 方案,这大约为 100 个用户提供服务。 对于使用客户端凭据授权调用其他应用的守护程序应用,这意味着几百个租户。 有关详细信息,请参阅下面的 性能

⚠️ 我们建议所有生产应用程序对缓存进行持久化,并使用加密,以兼顾安全性和所需的缓存持久性。 如果选择不保留缓存, 则 TokenCache 接口仍可用于访问缓存的实体。

持久缓存

访问内存中缓存时,MSAL Node 将触发事件,应用可以选择是否保留缓存(请参阅: TokenCacheContext)(例如文件、SQL 数据库等)。 这构成两个操作:

  1. 在访问缓存之前,将缓存从持久性存储中加载到 MSAL 内存中
  2. 如果自上次访问以来内存中缓存已更改,请将缓存保存回持久性

为了保留缓存,MSAL 接受 配置中的自定义缓存插件。 此插件应实现 ICachePlugin 接口:

interface ICachePlugin {
    beforeCacheAccess: (tokenCacheContext: TokenCacheContext) => Promise<void>;
    afterCacheAccess: (tokenCacheContext: TokenCacheContext) => Promise<void>;
}

接口的基本实现 ICachePlugin 可能如下所示(如果正在构建服务器端应用,另请参阅 性能和安全性 ):

class MyCachePlugin implements ICachePlugin {
    private client: ICacheClient;

    constructor(client: ICacheClient) {
        this.client = client; // client object to access the persistent cache
    }

    public async beforeCacheAccess(cacheContext: TokenCacheContext): Promise<void> {
        const cacheData = await this.client.get(); // get the cache from persistence
        cacheContext.tokenCache.deserialize(cacheData); // deserialize it to in-memory cache
    }

    public async afterCacheAccess(cacheContext: TokenCacheContext): Promise<void> {
        if (cacheContext.cacheHasChanged) {
            await this.client.set(cacheContext.tokenCache.serialize()); // deserialize in-memory cache to persistence
        }
    }
}
  • 如果要开发公共客户端应用, MSAL 节点扩展 会为你处理此问题。
  • 如果要开发机密客户端应用,应通过单独的服务保留缓存,因为单个 每个服务器 缓存实例不适用于具有许多服务器和应用实例的云环境。

强烈建议在磁盘上保存令牌缓存时对其进行加密。 对于公共客户端应用,MSAL Node Extensions 可开箱即用地提供此功能。 但是,对于机密客户端,你负责设计适当的加密解决方案。

性能和安全性

在公共客户端应用中,MSAL 节点扩展可确保性能和安全性。

在处理用户的机密客户端应用(用于登录用户并调用 Web API 的 Web 应用和调用下游 Web API 的 Web API)上,给定应用程序可以同时激活许多用户。 建议按用户序列化一个缓存 Blob(请参阅 CacheRecord)。 这有助于跨分布式系统缩放缓存。 使用键对缓存(分区键)进行分区,例如:

  • 对于 Web 应用: <userObjectId>.<tenantId> (即 homeAccountId
  • 对于使用客户端凭据授权的多租户守护程序应用:<clientId>.<tenantId>
  • 对于使用 OBO 调用其他 Web API 的 Web API:传入访问令牌的哈希(即 oboAssertion) — 该令牌随后将被交换为 OBO 令牌。

⚠️ 请务必查看 性能 ,详细了解如何监视使用情况并避免性能不佳。

Web 应用

由于 Web 应用面向用户,并且通常依赖于会话来跟踪每个用户,因此缓存的适当分区键通常存储在会话数据中,并且需要检索缓存查找才能进行。 为了帮助解决此问题,MSAL Node 提供了 DistributedCachePlugin 类,该类实现 ICachePluginDistributedCachePlugin 的一个实例需要:

  • 客户端 接口ICacheClient),用于在持久性服务器上实现 getset 操作(Redis、MySQL 等)。
  • 一个分区管理器IPartitionManager),用于读取和写入到给定会话 ID 的缓存。

有关示例实现,请参阅 使用 DistributedCachePlugin 的 Web 应用

另见

有关如何在 MSAL Node 应用中处理缓存的详细信息,请参阅以下示例: