- 网络安全
- 认证鉴权
- 后端
【免费下载链接】jose
JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes
jose 提供的customFetch是一个以unique symbol形式导出的特殊选项键,将其传入 createRemoteJWKSet 的第二个参数后,可以完全接管“远程 JSON Web Key Set(JWKS)的 HTTP 获取”这一环节,从而启用高级 fetch 配置、HTTP 代理、网络错误重试、请求日志甚至测试桩(mock)等能力。读完本文,你将掌握customFetch的精确签名、四种开箱即用的实战用法(ky 重试与日志、undici 代理、undici 重试、undici 测试模拟),以及它在 jose 源码内部真实的调用链与错误处理语义。
customFetch 是什么:一个键、一种类型、一套契约
在 jose 中,customFetch并不是一个函数,而是一个unique symbol。它由 src/jwks/remote.ts 导出:
export const customFetch: unique symbol = Symbol()之所以使用symbol而不是字符串属性名,是为了避免与RemoteJWKSetOptions上的普通选项(如timeoutDuration、headers)发生键冲突。它作为RemoteJWKSetOptions的可选计算属性存在(见 RemoteJWKSetOptions 接口):
interface RemoteJWKSetOptions { // ... [customFetch]?: FetchImplementation }与customFetch配套的FetchImplementation类型(见 FetchImplementation 类型别名)精确定义了你需要实现的函数形态:
type FetchImplementation = ( /** 要发起请求的 URL */ url: string, /** 本应直接传给全局 fetch 的 options 参数 */ options: { headers: Headers // HTTP 请求头 method: 'GET' // 请求方法,固定为 GET redirect: 'manual' // 重定向策略,固定为 manual signal: AbortSignal // 用于超时/取消 }, ) => Promise<Response>也就是说,你提供的函数接收(url, options)两个参数,返回一个Promise<Response>——这与原生fetch的调用约定完全一致,因此任何符合该形态的 fetch 兼容实现(如 ky、undici.fetch、Node 全局 fetch)都可以无缝替换。它由 jose 主入口统一导出(见 src/index.ts),你可以直接import { customFetch, createRemoteJWKSet } from 'jose'使用;createRemoteJWKSet本身也同时支持从子路径'jose/jwks/remote'导入。
为什么需要 customFetch?
createRemoteJWKSet默认使用全局fetch发起 JWKS 请求。但在真实生产环境中,全局fetch往往不足以满足需求:
- 企业网络要求流量经过指定的 HTTP/HTTPS 代理;
- 弱网或间歇性故障环境下需要自动重试网络错误;
- 需要记录每次出站请求与响应以便排查问题或做审计;
- 单元测试中不希望真正发起网络请求,而希望用 mock 拦截响应。
customFetch正是为这些场景预留的“后门”:把 fetch 的决策权完全交给你,而 jose 只负责组装好url与options并消费返回的Response。
实战一:用 ky 实现自动重试与请求/响应日志
ky 是 sindresorhus 开发的轻量级 fetch 封装,内置指数退避重试(默认重试 2 次)与丰富的事件钩子。将customFetch指向 ky 后,你既能获得重试能力,又能借助hooks记录出站请求、重试事件和响应:
import ky from 'ky' import { createRemoteJWKSet, customFetch } from 'jose' // 前置条件(按你的实际场景赋值) let url!: URL let logRequest!: (request: Request) => void let logResponse!: (request: Request, response: Response) => void let logRetry!: (request: Request, error: Error, retryCount: number) => void const JWKS = createRemoteJWKSet(url, { [customFetch]: (...args) => ky(args[0], { ...args[1], // 透传 jose 组装的 headers/method/redirect/signal hooks: { beforeRequest: [ (request) => { logRequest(request) }, ], beforeRetry: [ ({ request, error, retryCount }) => { logRetry(request, error, retryCount) }, ], afterResponse: [ (request, _, response) => { logResponse(request, response) }, ], }, }), })关键点在于...args[1]:jose 会把headers、method: 'GET'、redirect: 'manual'、signal完整传入,你只需原样展开给 ky,即可保住 jose 内置的超时中止与请求头行为。beforeRequest、beforeRetry、afterResponse三个钩子分别覆盖“发请求前”“重试前”“响应后”三个时机,足以支撑完整的可观测性需求。
实战二:用 undici 的 EnvHttpProxyAgent 自动探测并使用 HTTP 代理
在 Node.js 服务端环境中,许多部署要求所有外呼流量经由代理。undici 的EnvHttpProxyAgent会读取HTTP_PROXY/HTTPS_PROXY/NO_PROXY等环境变量并自动为请求选择代理,配合customFetch即可让 JWKS 请求也走代理:
import * as undici from 'undici' import { createRemoteJWKSet, customFetch } from 'jose' let url!: URL // 自动从环境变量读取代理配置(参见 undici 的 EnvHttpProxyAgent 文档) let envHttpProxyAgent = new undici.EnvHttpProxyAgent() // @ts-ignore const JWKS = createRemoteJWKSet(url, { [customFetch]: (...args) => { // @ts-ignore return undici.fetch(args[0], { ...args[1], dispatcher: envHttpProxyAgent }) }, })dispatcher是 undici fetch 的扩展选项,用于指定底层 Agent 实例;...args[1]依然透传 jose 的标准选项。这段代码让 JWKS 获取自动适配企业代理环境,无需手写任何代理解析逻辑。
实战三:用 undici 的 RetryAgent 自动重试网络错误
默认全局 fetch 在遇到ECONNRESET、ECONNREFUSED这类瞬时网络错误时会直接失败,导致 JWT 校验中断。undici 的RetryAgent可以针对特定错误码自动重试:
import * as undici from 'undici' import { createRemoteJWKSet, customFetch } from 'jose' let url!: URL let retryAgent = new undici.RetryAgent(new undici.Agent(), { statusCodes: [], // 只重试网络错误,不重试 HTTP 状态码 errorCodes: [ 'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN', 'ENETUNREACH', 'EHOSTDOWN', 'UND_ERR_SOCKET', ], }) // @ts-ignore const JWKS = createRemoteJWKSet(url, { [customFetch]: (...args) => { // @ts-ignore return undici.fetch(args[0], { ...args[1], dispatcher: retryAgent }) }, })这里将statusCodes置为空数组、仅针对errorCodes重试,是一个经过刻意收敛的配置:HTTP 层错误(如 404、5xx)仍交由 jose 处理并直接失败,只有底层网络错误才触发重试,避免对远端不稳定服务做无意义的过度重试。值得注意的是,jose 源码中默认是不做任何网络错误重试的,自定义RetryAgent正好弥补了这一环(详见下文源码分析中的测试佐证)。
实战四:用 undici 的 MockAgent 在测试中模拟 JWKS 响应
在单元测试中,你既不希望依赖真实网络,也不想为 mock 重写整个解析链路。undici 的MockAgent配合customFetch可以拦截请求并返回预设的 JWKS 数据,且能严格禁止真实网络连接:
import * as undici from 'undici' import { createRemoteJWKSet, customFetch } from 'jose' let url!: URL let mockAgent = new undici.MockAgent() mockAgent.disableNetConnect() // 禁止任何真实网络请求 // @ts-ignore const JWKS = createRemoteJWKSet(url, { [customFetch]: (...args) => { // @ts-ignore return undici.fetch(args[0], { ...args[1], dispatcher: mockAgent }) }, }) // 随后按需对 mockAgent 注册拦截规则: // mockAgent.get('https://as.example.com').intercept({ path: '/jwks' }).reply(200, jwks)jose 自己的测试套件正是这么做的:在 test/jwks/remote.test.ts 中,测试代码创建MockAgent、disableNetConnect(),然后把包装后的undiciFetch通过[customFetch]传入createRemoteJWKSet(见 test/jwks/remote.test.ts),从而在完全离线的环境下验证jwtVerify的完整流程。
类型注意事项:为什么要写 @ts-expect-error
官方文档对此有一个明确的已知坑(Known caveat):当你把customFetch的入参透传给 ky、undici.fetch 这类 fetch 兼容模块时,极大概率会遇到类型不匹配——因为这些模块的签名几乎不可能与标准fetch完全一致(例如 undici 的 options 比标准多出dispatcher,ky 的 options 又包含hooks等扩展字段)。文档建议在透传处使用@ts-expect-error明确豁免:
Expect Type-related issues when passing the inputs through to fetch-like modules, they hardly ever get their typings inline with actual fetch, you should
@ts-expect-errorthem.
这也是上面所有示例中反复出现// @ts-ignore或// @ts-expect-error注释的原因。在真实项目中,更推荐把这些兼容函数抽成一个带宽松类型签名的工具函数,把豁免注释集中在一处,而不是散落在业务代码里。
源码级原理:customFetch 在内部如何被消费
理解了用法之后,再看 jose 内部如何使用customFetch,能帮你更准确地预判行为。在 src/jwks/remote.ts 中,fetchJwks是唯一调用你自定义实现的入口:
async function fetchJwks(url, headers, signal, fetchImpl = fetch) { const response = await fetchImpl(url, { method: 'GET', signal, redirect: 'manual', headers, }).catch((err) => { if (err.name === 'TimeoutError') { throw new JWKSTimeout() } throw err }) if (response.status !== 200) { throw new JOSEError('Expected 200 OK from the JSON Web Key Set HTTP response') } try { return await response.json() } catch { throw new JOSEError('Failed to parse the JSON Web Key Set HTTP response as JSON') } }由这段代码可以提炼出几个与customFetch直接相关的契约:
- 参数形态固定:jose 始终以
method: 'GET'、redirect: 'manual'、signal(来自AbortSignal.timeout(timeoutDuration))和组装好的headers调用你的实现,返回的Response会被response.json()消费; - HTTP 状态码语义:只要响应状态不是
200,无论你的自定义实现做了什么(重试、代理、mock),最终都会抛出JOSEError(Expected 200 OK from the JSON Web Key Set HTTP response)。因此自定义实现不应在“非 200”上自行放行; - 超时映射:如果请求因
AbortSignal.timeout中止而抛出名为TimeoutError的错误,会被映射为JWKSTimeout(错误码ERR_JWKS_TIMEOUT,见 src/util/errors.ts); - JSON 解析失败:即使你的实现正常返回响应,若
response.json()失败,也会抛出JOSEError(Failed to parse the JSON Web Key Set HTTP response as JSON)。
另外,src/jwks/remote.ts 显示默认会注入两个请求头:User-Agent: jose/v6.2.10(浏览器环境为了规避不必要的 CORS 预检会省略)以及accept: application/json, application/jwk-set+json。自定义实现透传...args[1]时这些头会被保留;如果你完全丢弃传入的 options,则需要自行补充。
在调用策略层面,createRemoteJWKSet采用“惰性 + 冷却窗口”模型(src/jwks/remote.ts):只有缓存过期或没有键匹配时才触发一次请求,且在cooldownDuration(默认 30 秒)内成功获取后不再重复请求,以防止滥用;reload()方法则可绕过冷却窗口强制刷新。多个并发触发会被合并到同一个pendingFetch(src/jwks/remote.ts),其中对 Cloudflare Workers 这类无状态运行时有专门的pendingFetch重置处理(src/jwks/remote.ts)——这意味着即使你注入自定义 fetch,这部分并发合并与冷却语义依然生效。
与其它选项的协同
customFetch不是孤立存在的选项,它通常与 RemoteJWKSetOptions 中的其余配置配合使用:
| 选项 | 默认值 | 说明 |
|---|---|---|
timeoutDuration | 5000 ms | 请求超时,超时后中止并抛JWKSTimeout,必须是非负整数 |
cooldownDuration | 30000 ms | 成功获取后在此窗口内不再触发新请求,防止滥用 |
cacheMaxAge | 600000 ms(10 分钟) | 缓存 JWKS 的最大有效期,超过后重新获取 |
headers | 见上 | 附加到请求的额外 HTTP 头 |
[jwksCache] | — | 云函数等无法常驻内存缓存场景下的可写缓存对象(见 jwksCache 变量) |
[customFetch] | 全局fetch | 自定义 fetch 实现(本文主题) |
例如你可以在使用customFetch的同时收紧timeoutDuration、放宽cooldownDuration,并用headers携带认证信息——这些都由 jose 组装后原样传给你的实现。
测试佐证:customFetch 在 jose 测试套件中的实际覆盖
jose 的官方测试 test/jwks/remote.test.ts 全面覆盖了customFetch相关行为,可作为你实现自测的参考基线:
- 正常校验流:通过
[customFetch]注入包装后的 undici fetch,配合MockAgent拦截https://as.example.com/jwks,验证jwtVerify能成功校验签发的 JWT(test/jwks/remote.test.ts); - 网络错误传播:测试直接命中真实网络场景,验证
ENOTFOUND、ECONNREFUSED会被原样向上抛出并携带err.cause.code(test/jwks/remote.test.ts)——这正是“用 RetryAgent 重试这类错误码”能生效的前提; - 超时映射:将
timeoutDuration: 500与[customFetch]组合,验证超时最终抛出errors.JWKSTimeout(test/jwks/remote.test.ts); - 响应控制:通过可控的 fetch 实现(先挂起再逐一 resolve)验证并发
reload()的去重与新旧序列取舍(test/jwks/remote.test.ts)。
这些测试从侧面印证了一个结论:customFetch只是替换“发请求”这一步,其上游的缓存、冷却、超时、并发合并与错误映射语义全部由 jose 自身保证,不会因为换了 fetch 实现而丢失。
小结
customFetch是 jose 远程 JWKS 模块留出的一个高价值扩展点:以unique symbol作为选项键、以FetchImplementation类型定义契约,让你的 JWKS 获取从“全局 fetch 一步到位”升级为可代理、可重试、可观测、可 mock 的生产级网络栈。将其与 createRemoteJWKSet 的冷却窗口、缓存有效期、超时等内置策略组合使用,即可在保持 jose 安全语义不变的前提下,把远程 JWKS 的获取行为完全掌控在自己手中。更完整的模块视图可参考 jwks/remote 文档 及其中的 RemoteJWKSet 接口。
- 网络安全
- 认证鉴权
- 后端
【免费下载链接】jose
JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes
相关推荐
jose 远程 JWKS 的 FetchImplementation 类型:自定义 fetch 函数的签名契约、内部调用链与实战配置
jose 远程 JWKS 的 FetchImplementation 类型:自定义 fetch 函数的签名契约、内部调用链与实战配置 远程 JSON Web K
网络安全认证鉴权后端AutoDev项目中自定义测试代理的上下文变量实现
AutoDev项目中自定义测试代理的上下文变量实现 引言:为什么需要上下文变量? 在AI驱动的编程辅助工具中,上下文理解是核心挑战。传统的代码生成工具往往缺乏对
人工智能大模型AI Agent代码智能体开发工具工具调用后端前端抖音自动上传工具:一键批量发布视频的终极指南
抖音自动上传工具:一键批量发布视频的终极指南 抖音自动上传工具是一款专为内容创作者打造的自动化视频发布神器,能够帮你实现从视频采集到发布的完整自动化流程。这款开
工作流自动化网页爬虫视频处理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考