动态身份验证

以 Markdown 格式查看

您的 API 可能需要动态身份验证,其中需要为每个请求生成或刷新凭据,例如签名短期 JWT 或轮换令牌。TypeScript SDK 通过自定义 fetcher 中间件支持这种模式。

自定义 fetcher 中间件

在 TypeScript SDK 中实现动态身份验证的推荐方式是使用自定义 fetcher。这充当所有请求的中间件,允许您在单个位置注入身份验证逻辑,而无需重写单个方法。

工作原理

当您在生成器配置中启用 allowCustomFetcher 时,生成的 SDK 在客户端选项中接受 fetcher 参数。此 fetcher 包装所有 HTTP 请求,为您提供身份验证逻辑的单个注入点。

示例:短期 JWT 签名

以下是如何通过令牌记忆化实现 JWT 签名:

1

在生成器配置中启用自定义 fetcher

在您的 generators.yml 中添加 allowCustomFetcher: true

generators.yml
1# 在您的 TypeScript SDK 生成器中
2- name: fernapi/fern-typescript-sdk
3 config:
4 allowCustomFetcher: true
2

通过 packageJson 添加运行时依赖

使用 packageJson 配置选项添加 jsonwebtoken 依赖,以便它合并到生成的 package.json 中:

generators.yml
1# 在您的 TypeScript SDK 生成器中
2- name: fernapi/fern-typescript-sdk
3 config:
4 packageJson:
5 dependencies:
6 jsonwebtoken: "^9.0.0"
7 devDependencies:
8 "@types/jsonwebtoken": "^9.0.0"

查看 TypeScript 配置页面 了解所有可用的 packageJson 选项。

3

创建带有 JWT 签名的自定义 fetcher

创建一个包装默认 fetcher 并注入 JWT 身份验证的 fetcher 函数:

src/wrapper/jwtFetcher.ts
1import * as jwt from "jsonwebtoken";
2import { fetcher as defaultFetcher, type FetchFunction } from "../core/fetcher";
3
4export function createJwtFetcher(privateKey: string): FetchFunction {
5 // 缓存令牌以避免在每个请求上重新生成
6 let cachedToken: { value: string; expiresAt: number } | undefined;
7
8 return async (args) => {
9 const now = Math.floor(Date.now() / 1000);
10
11 // 如果令牌已过期或即将过期(2 秒内),则重新生成令牌
12 if (!cachedToken || cachedToken.expiresAt - 2 <= now) {
13 const payload = {
14 iat: now,
15 exp: now + 15, // 令牌有效期 15 秒
16 };
17 const token = jwt.sign(payload, privateKey, { algorithm: "RS256" });
18 cachedToken = { value: token, expiresAt: payload.exp };
19 }
20
21 // 将 JWT 注入请求头
22 const headers = {
23 ...(args.headers ?? {}),
24 Authorization: `Bearer ${cachedToken.value}`,
25 };
26
27 // 使用修改后的头部调用默认 fetcher
28 return defaultFetcher({ ...args, headers });
29 };
30}
4

创建包装器客户端

扩展生成的客户端以使用自定义 fetcher:

src/wrapper/PlantStoreClient.ts
1import { PlantStoreClient as FernClient } from "../Client";
2import { createJwtFetcher } from "./jwtFetcher";
3
4// 从生成的客户端构造函数推断确切的选项类型
5type FernClientOptions = ConstructorParameters<typeof FernClient>[0];
6// 接受除 'fetcher' 之外的所有选项,并添加我们的 'privateKey'
7type Options = Omit<FernClientOptions, "fetcher"> & { privateKey: string };
8
9export class PlantStoreClient extends FernClient {
10 constructor(options: Options) {
11 // 提取 privateKey 并将所有其他选项传递给父类
12 const { privateKey, ...clientOptions } = options;
13 super({
14 ...clientOptions,
15 fetcher: createJwtFetcher(privateKey),
16 });
17 }
18}

这种模式使用 ConstructorParameters 从生成的客户端推断确切的选项类型,确保与所有客户端选项(headers、timeoutInSeconds、maxRetries 等)的兼容性,而无需硬编码它们。这使包装器在生成器添加新选项时保持向前兼容。

5

导出包装器客户端

更新您的 index.ts 以导出包装器而不是生成的客户端:

src/index.ts
1export { PlantStoreClient } from "./wrapper/PlantStoreClient";
2export * from "./api"; // 导出类型
6

添加到 .fernignore

保护您的自定义代码不被覆盖:

.fernignore
1+ src/wrapper
2+ src/index.ts
使用 .fernignore 来保护您的自定义包装器文件,而不是管理 package.json。通过 generators.yml 中的 packageJson 配置选项添加依赖项。
7

使用客户端

用户可以使用客户端,在所有请求上自动进行 JWT 签名:

1import { PlantStoreClient } from "plant-store-sdk";
2
3const client = new PlantStoreClient({
4 privateKey: process.env.PRIVATE_KEY,
5 environment: "https://api.plantstore.com",
6});
7
8// JWT 在每个请求上自动签名和注入
9const plant = await client.plants.get("monstera-123");
10const newPlant = await client.plants.create({
11 name: "Fiddle Leaf Fig",
12 species: "Ficus lyrata"
13});

其他身份验证模式

这种相同的模式适用于其他动态身份验证场景:

  • OAuth 令牌刷新:在每个请求之前自动刷新过期的访问令牌
  • HMAC(基于哈希的消息身份验证代码)签名:基于请求内容使用 HMAC 签名对请求进行签名
  • 轮换 API 密钥:基于速率限制在多个 API 密钥之间切换
  • 基于时间的令牌:生成包含时间戳或随机数的令牌

重要考虑事项

自定义 fetcher 要求

  • 生成器配置:必须在您的 generators.yml 中启用 allowCustomFetcher 选项,以便 fetcher 参数在 BaseClientOptions 中可用
  • 导入路径:从 ../core/fetcher(或生成的 SDK 中的适当路径)导入默认 fetcher,以使用您的自定义逻辑包装它
  • 保留所有参数:在包装默认 fetcher 时,确保传递所有参数以保持与 SDK 内部行为的兼容性

安全考虑事项

  • 仅服务器端:永远不要在浏览器环境中暴露私钥。使用私钥进行 JWT 签名应该只在服务器端代码(Node.js、Deno、Bun)中进行
  • 安全密钥存储:永远不要硬编码私钥;使用环境变量或安全密钥管理系统
  • 避免双重身份验证:如果您的 API 在 Fern 定义中已经使用了 bearer token 身份验证,请小心不要覆盖现有的 Authorization 头部。考虑使用不同的头部名称或有条件地设置头部

性能和并发

  • 令牌记忆化:缓存令牌以避免在每个请求上重新生成它们。上面的示例缓存令牌并在过期前 2 秒刷新它们
  • 线程安全:所示的记忆化模式在 JavaScript 的单线程事件循环中对并发请求是安全的,但在其他环境中要注意竞态条件
  • 宽限期:在令牌过期前稍早刷新令牌(例如,提前 2 秒)以避免令牌在请求处理期间过期的边缘情况

头部合并

  • 保留现有头部:在注入身份验证头部时,始终展开现有头部以避免覆盖 SDK 或用户设置的头部
  • 头部优先级:头部按顺序合并:SDK 默认值 → 客户端选项 → 请求选项 → 自定义 fetcher。您的自定义 fetcher 最后运行,可以覆盖先前的头部

时间同步

  • 时钟漂移:注意客户端和服务器之间潜在的时钟漂移。考虑为令牌过期检查添加容差
  • 时间戳精度:对 iatexp 声明使用 Unix 时间戳(自纪元以来的秒数)以符合 JWT 标准

最佳实践

  • 适当缓存令牌:在安全性(较短的令牌生命周期)和性能(较少频繁的重新生成)之间取得平衡
  • 优雅处理错误:为身份验证失败和令牌刷新错误实现重试逻辑
  • 彻底测试:确保您的包装器处理所有边缘情况,包括并发请求、令牌过期和网络故障
  • 监控令牌使用:记录令牌生成和刷新事件以帮助调试生产中的身份验证问题

另请参阅