Zoom Meeting SDK 机器人认证实战指南:JWT 签名、ZAK 与 OBF 三类令牌在 knowledge-work-plugins 中的完整解析
【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins
在 Zoom 生态中构建会议纪要、自动录制、AI 助手类"会议机器人"时,最常见的开发困惑集中在认证体系上:JWT 签名到底有没有被废弃?ZAK 令牌是不是必须来自会议参会者?2026 年 2 月 23 日后 OBF(On-Behalf-Of)令牌何时成为硬性要求?本文基于 bot-authentication.md(Zoom Meeting SDK 机器人认证参考文档)逐层拆解这三类令牌的用途、生成方式、适用范围与互斥关系,并结合本仓库中 authorization.md、signature-playbook.md 和 linux/SKILL.md 等配套文档,给出可复制的签名代码、REST API 调用示例与完整的入会流程,帮助你在 Web 端或 Linux 无头环境中把机器人认证链路一次性做对。
一、三类令牌总览:谁负责什么
Meeting SDK 的认证并非单一令牌,而是由三类各司其职的凭证组合完成。原文档给出的总览表如下:
| 令牌 | 用途 | 是否必须 | 是否废弃 |
|---|---|---|---|
| JWT 签名(JWT Signature) | 初始化/认证 Meeting SDK | 必须 | 否 |
| ZAK 令牌 | 以某个 Zoom 用户身份完成认证 | 否(视场景而定) | 否 |
| OBF 令牌 | 以用户归因(attribution)方式加入外部会议 | 否(2026 年 2 月起外部会议必需) | 否 |
三者的关系可以概括为:JWT 签名是每次入会的"入场券",ZAK 是"证明这个应用代表某个已认证 Zoom 用户"的短时效凭证,OBF 则进一步把应用绑定到"当前确实在会议中的某个具体用户"身上。理解了这条主线索,后面所有的 API 调用和 join 参数都容易对号入座。
二、头号误区:JWT App Type 废弃 ≠ JWT 签名废弃
这是本领域排名第一的混淆来源,原文档专门用一张对照表澄清:
| 术语 | 它是什么 | 状态 |
|---|---|---|
| JWT App Type | 用于 REST API 认证的 Zoom 应用类型 | 已废弃(迁移到 Server-to-Server OAuth) |
| JWT 签名 | 使用 SDK 凭据生成的令牌,用于认证 Meeting SDK | 仍然必需,未废弃 |
关键结论:JWT App Type 的废弃与 Meeting SDK 完全无关,SDK 认证依旧依赖 JWT 签名。仓库中的 signature-playbook.md 也在排查规则中强调了这一点:如果开发者把 REST API 的 OAuth 令牌或 Marketplace 的 JWT app-type 令牌拿来当 Meeting SDK 签名使用,应当立即停下澄清——它们不是同一个东西,混用是"join failed"的高频根因之一。
三、JWT 签名(每次都必需)
3.1 它是什么、何时使用
JWT 签名用于向 Zoom 证明你的应用有权调用 Meeting SDK,必须在服务端用 SDK 的 Client ID 与 Client Secret 生成(绝不可把 SDK Secret 放到客户端代码里)。每一次 Meeting SDK 入会都需要它,没有例外。
仓库中的 authorization.md 列出了 JWT 载荷中各 claim 的含义,可作为签名生成的字段清单:
| Claim | 说明 |
|---|---|
sdkKey | 你的 SDK Key |
mn | 会议号(仅数字) |
role | 0 = 参会者,1 = 主持人 |
iat | 签发时间戳 |
exp | 过期时间戳 |
tokenExp | 令牌过期时间戳 |
3.2 服务端生成示例(Node.js + jsrsasign)
原文档给出的标准生成函数如下:
// Server-side (Node.js) const KJUR = require('jsrsasign'); function generateSignature(sdkKey, sdkSecret, meetingNumber, role) { const iat = Math.round(Date.now() / 1000) - 30; // 30 秒前 const exp = iat + 60 * 60 * 2; // iat 起 2 小时后过期 const payload = { sdkKey: sdkKey, // 你的 SDK Client ID mn: meetingNumber, // 要加入的会议号 role: role, // 0 = participant, 1 = host iat: iat, exp: exp, tokenExp: exp }; const header = { alg: 'HS256', typ: 'JWT' }; return KJUR.jws.JWS.sign('HS256', JSON.stringify(header), JSON.stringify(payload), sdkSecret // 你的 SDK Client Secret ); }仓库的 SKILL.md 中还给出了把它包装成生产级后端接口的写法(Express 风格),补充了两个实操细节:会议号要先String(meetingNumber).replace(/\D/g, '')剔除非数字字符,role 要用parseInt(role, 10)强制转成数字——这正是 signature-playbook.md 归纳的"Invalid signature"典型成因(mn格式错误、role 类型错误)。
// server.js (Node.js example) app.post('/api/signature', (req, res) => { const { meetingNumber, role } = req.body; const iat = Math.floor(Date.now() / 1000) - 30; const exp = iat + 60 * 60 * 2; const header = { alg: 'HS256', typ: 'JWT' }; const payload = { sdkKey: process.env.ZOOM_SDK_KEY, mn: String(meetingNumber).replace(/\D/g, ''), role: parseInt(role, 10), iat, exp, tokenExp: exp }; const signature = KJUR.jws.JWS.sign('HS256', JSON.stringify(header), JSON.stringify(payload), process.env.ZOOM_SDK_SECRET ); res.json({ signature, sdkKey: process.env.ZOOM_SDK_KEY }); });3.3 最佳实践:短时效签名(Short-Lived Tokens)
原文档推荐"在即将入会的前一刻"生成签名,并用一个反直觉但合规的时间窗口设计来满足 Zoom 的校验规则:
// 入会前一刻生成令牌 const iat = Math.floor(Date.now() / 1000) - 7200; // 2 小时前 const exp = Math.floor(Date.now() / 1000) + 10; // 10 秒后过期 // 这样设计的原因: // - exp 很短(安全性) // - exp - iat >= 2 小时(Zoom 硬性要求) // - 令牌在使用前即刻生成这套"iat 回拨 2 小时、exp 只留 10 秒"的手法同时满足两个看似矛盾的约束:签名实际有效期极短(防泄露),且exp - iat跨度不小于 2 小时(Zoom 校验要求)。authorization.md 的"安全准则"表把它进一步归纳为:签名只放服务端、使用短过期时间、生成前校验用户身份;signature-playbook.md 则补充了时钟偏移(server clock skew)是"本地能跑、生产失败"的常见诱因——iat/exp依赖服务器时间,生产环境务必保证时钟同步。
3.4 role 取值
| 角色 | 值 | 说明 |
|---|---|---|
| 参会者(Participant) | 0 | 以与会者身份加入 |
| 主持人(Host) | 1 | 以主持人身份加入(需为会议所有者或持有主持人密钥) |
注意 signature-playbook.md 提到的一种失败模式:role=1生成签名却实际执行 join(或反之)会导致签名校验失败;Web"以主持人启动"流程中 role 不匹配还会触发4003 Invalid Parameter错误,此时往往还需要配合 ZAK 令牌。
四、ZAK 令牌(Zoom Access Key)
4.1 它是什么、何时使用
ZAK 是一个短时效凭证,证明你的机器人/应用已认证为某个具体的 Zoom 用户,通过 Zoom REST API 生成。原文档给出的适用场景表:
| 场景 | 是否需要 ZAK |
|---|---|
| 会议开启了"仅认证用户可加入"(Only Authenticated Users Can Join) | 需要 |
| 以主持人身份启动会议(当前主持人不在场) | 需要(主持人的 ZAK) |
| 把机器人的头像改成与某用户一致 | 需要 |
| 普通的会议加入 | 不需要 |
4.2 获取 ZAK 令牌的两步流程
第 1 步:获取 OAuth Access Token(授权码模式):
curl -X POST "https://zoom.us/oauth/token" \ -H "Authorization: Basic {BASE64(client_id:client_secret)}" \ -d "grant_type=authorization_code&code={auth_code}&redirect_uri={redirect_uri}"第 2 步:用 Access Token 生成 ZAK 令牌:
curl -X GET "https://api.zoom.us/v2/users/me/token?type=zak&ttl=7200" \ -H "Authorization: Bearer {access_token}"响应示例:
{ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." }所需的 OAuth 权限范围(scope)为:
user:read:zak这个 scope 在仓库的 granular-scopes.md 中也能交叉印证:"Get the user's ZAK" 接口对应user:read:zak(管理侧为user:read:zak:admin),说明 ZAK 属于用户粒度(user-delegated)的 OAuth 授权。
4.3 在 SDK join 中使用 ZAK
// Web SDK ZoomMtg.join({ signature: signature, // JWT 签名(始终必需) sdkKey: clientId, meetingNumber: meetingNumber, passWord: password, // 注意:大写 W userName: "Meeting Bot", zak: zakToken, // 用于认证加入的 ZAK 令牌 success: (success) => console.log('Joined'), error: (error) => console.error(error) });注意 Web 端passWord的驼峰写法(大写 W)——signature-playbook.md 特别指出,Web Client View 下如果会议有密码而该字段拼错或缺失,join 会以"看起来像认证问题"的方式失败,排查时容易走偏。
4.4 关键属性与一个高频错误
原文档总结的四个属性:
- 短时效:TTL 可配置(通常 1–2 小时);
- 任意 ZAK 均可:不需要来自会议参会者;
- 无并发限制:一个服务账号可生成不限数量的令牌;
- 过期仅在入会时校验:若机器人已在会中,即使 ZAK 过期也不会被断开。
常见错误:以为 ZAK 必须来自会议参会者。正确认知:任意 Zoom 账号的 ZAK 都能满足"仅认证用户可加入"的要求。实务上建议创建一个专用服务账号(如meeting-bot@company.com)为所有机器人统一签发 ZAK。
五、OBF 令牌(On-Behalf-Of):2026 年 2 月的关键变化
5.1 它是什么、为什么引入
OBF 是 Zoom 新引入的凭证,把机器人绑定到当前确实身在会中的某个具体用户。出于问责与透明(accountability and transparency)考虑,它让会议中的人能清楚知道"这个 SDK 应用代表哪个人"。时间线要求:
| 日期 | 外部会议(External Meeting)要求 |
|---|---|
| 2026 年 2 月 23 日之前 | ZAK 或 OBF(可选) |
| 2026 年 2 月 23 日之后 | ZAK 或 OBF 为必需 |
5.2 OBF 与 ZAK 的本质区别
| 维度 | ZAK 令牌 | OBF 令牌 |
|---|---|---|
| 是否需要用户在场 | 否 | 是—— 用户必须在会中 |
| 用户离场后机器人状态 | 保持连接 | 立即被断开 |
| 会议作用域 | 任意会议 | 仅限特定会议 ID |
| 归因方式 | 泛化的身份认证 | 绑定到具体的在场用户 |
从源码结构看,这条"用户离场即断连"的规则意味着 OBF 场景下机器人的生命周期策略必须与 ZAK 场景区分设计:ZAK 机器人可以早于授权用户入会并一直驻留,OBF 机器人则要么等用户在会中再加入,要么接受"加入即失败"的重试成本。
5.3 获取 OBF 令牌
第 1 步:获取 OAuth Access Token(与 ZAK 相同流程)。第 2 步:按会议维度生成 OBF 令牌:
curl -X GET "https://api.zoom.us/v2/users/me/token?type=onbehalf&meeting_id={meeting_id}" \ -H "Authorization: Bearer {access_token}"响应示例:
{ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." }所需 OAuth scope:
user:read:token同样可在 granular-scopes.md 中交叉验证:"Get a user's token" 接口对应user:read:token(另有user:read:token:admin、user:read:token:master等管理粒度变体)。
5.4 在 SDK join 中使用 OBF
// Web SDK ZoomMtg.join({ signature: signature, // JWT 签名(始终必需) sdkKey: clientId, meetingNumber: meetingNumber, passWord: password, userName: "Meeting Bot", obfToken: obfToken, // OBF 令牌(注意字段是 obfToken,不是 zak) success: (success) => console.log('Joined'), error: (error) => console.error(error) });重要:zak与obfToken互斥,只能二选一。在 Linux C++ 端,仓库 linux/SKILL.md 的自动加入+录制示例展示了 OBF 令牌通过 join 参数app_privilege_token传入的对应关系:
JoinParam join_param; join_param.userType = SDK_UT_WITHOUT_LOGIN; auto& params = join_param.param.withoutloginuserJoin; params.meetingNumber = meeting_number; params.userName = "Recording Bot"; params.psw = meeting_password.c_str(); params.app_privilege_token = obf_token.c_str(); // OBF 令牌 SDKError join_err = meeting_service->Join(join_param);同一文档还提到app_privilege_token(即 OBF)也是获得 raw recording 权限的路径之一(其余三种:机器人是主持人/共同主持人、主持人授予录制权限、使用recording_token),说明 OBF 在 Linux 无头机器人场景中还额外承担"提权"职能。
5.5 处理"授权用户尚未在会中"的加入失败
如果机器人先于授权用户入会,SDK v6.6.10+ 会返回特定错误码MEETING_FAIL_AUTHORIZED_USER_NOT_INMEETING。原文档给出的重试实现:
// SDK v6.6.10+ 返回特定错误码 // MEETING_FAIL_AUTHORIZED_USER_NOT_INMEETING async function joinWithRetry(joinOptions, maxRetries = 5) { for (let i = 0; i < maxRetries; i++) { try { await ZoomMtg.join(joinOptions); return; // 成功 } catch (error) { if (error.code === 'MEETING_FAIL_AUTHORIZED_USER_NOT_INMEETING') { console.log(`User not in meeting yet. Retry ${i + 1}/${maxRetries}`); await sleep(3000); // 等待 3 秒 } else { throw error; // 其他错误不重试 } } } throw new Error('Max retries exceeded - user never joined meeting'); }实现要点:只对该特定错误码重试、其他错误直接抛出,避免对"用户永远不入场"做无界等待。
5.6 OBF 令牌的映射问题
使用 OBF 前你必须能把"用户"映射到"会议",原文档给出两条路径:
- Zoom Meetings API:
GET /users/{userId}/meetings拉取用户日程中的会议; - 日历集成:解析 Google Calendar / Outlook 中的会议邀请,提取 meeting_id。
这一步是 OBF 工作流的实际工程瓶颈:meeting_id 必须与type=onbehalf&meeting_id={meeting_id}请求里的参数严格一致。
六、完整机器人入会流程(Before / After 2026-02)
6.1 2026 年 2 月之前(通用流程)
// 1. 生成 JWT 签名(始终必需) const signature = await generateSignature(sdkKey, sdkSecret, meetingNumber, 0); // 2. 如会议要求认证加入,则获取 ZAK let zakToken = null; if (meetingRequiresAuth) { zakToken = await getZAKToken(accessToken); } // 3. 加入会议 await ZoomMtg.join({ signature: signature, sdkKey: sdkKey, meetingNumber: meetingNumber, passWord: password, userName: "Meeting Bot", zak: zakToken, // 可选 });6.2 2026 年 2 月之后(外部会议)
// 1. 生成 JWT 签名(始终必需) const signature = await generateSignature(sdkKey, sdkSecret, meetingNumber, 0); // 2. 外部会议需获取 OBF 令牌 const obfToken = await getOBFToken(accessToken, meetingNumber); // 3. 等待授权用户入会(若使用 OBF) // ... 实现 5.5 节的重试逻辑 ... // 4. 加入会议 await ZoomMtg.join({ signature: signature, sdkKey: sdkKey, meetingNumber: meetingNumber, passWord: password, userName: "Meeting Bot", obfToken: obfToken, // 用于外部会议 });从仓库的 meeting-bots.md 可以看到,这套认证流程在多技能编排里被抽象为固定的"技能链":先由 REST API 技能获取会议元数据并签发 OBF/ZAK,再由meeting-sdk/linux技能执行"以可见参会者身份加入 →StartRawRecording()→ 订阅音频/视频 delegate → 写入 PCM/YUV 或送入下游流水线",需要 Zoom 托管云录制产物时再挂上 webhooks 链路。认证令牌是整条链路的第一环。
七、Linux 无头机器人的落地配置
对无头 Linux 机器人,原文档推荐基于官方 headless 示例仓库(meetingsdk-headless-linux-sample)搭建,其sample.config.toml的配置结构如下:
# 克隆示例仓库后,配置 sample.config.toml [credentials] client_id = "YOUR_CLIENT_ID" client_secret = "YOUR_CLIENT_SECRET" [meeting] join_url = "https://zoom.us/j/123456789?pwd=xxx" # 或者 meeting_id = 123456789 password = "abc123" # 可选令牌 zak_token = "..." # 用于认证加入 obf_token = "..." # 用于外部会议 # 用 Docker 运行 # docker compose up可以看出该示例把 5.4 节的 Web 端zak/obfToken字段统一为 TOML 配置项zak_token/obf_token,凭据、会议信息与可选令牌分三层组织,docker compose up一键起服务。仓库的 linux/SKILL.md 进一步说明该 C++ SDK 是"为无头服务器环境优化的库":无 GUI、GLib 事件循环驱动回调、依赖 PulseAudio 虚拟声卡;其 C++ 端的 JWT 认证走AuthContext.jwt_token+CreateAuthService→SDKAuth的调用链,与 Web 端ZoomMtg.join的signature字段是同一枚 JWT 签名在不同平台的落点。运行环境要求 Ubuntu 22+ / CentOS 8/9、x86_64、CMake 3.16+,原始数据(PCM 32kHz 音频、YUV420 视频)需要先经StartRawRecording()授权后才可订阅。
八、常见错误速查表
原文档把五大高频错误整理成对照表,建议作为认证方案评审的 checklist:
| 错误认知 | 实际情况 | 修正方式 |
|---|---|---|
| JWT 签名已被废弃 | 被废弃的只是 JWT App Type | SDK 场景继续使用 JWT 签名 |
| ZAK 必须来自会议参会者 | 任意 Zoom 账号的 ZAK 均可用 | 用单一服务账号统一签发 |
| ZAK 和 OBF 可以一起用 | 二者互斥 | 只用其一 |
| 用户不在会中先生成 OBF | OBF 要求用户在场 | 实现 5.5 节的重试逻辑 |
| 用开发凭据跑外部会议 | 开发凭据只能作用于自己账号 | 申请生产凭据(约 4–6 周审核) |
九、时间线与权限范围汇总
认证规则的时间线(原文档):
| 日期 | 变化 |
|---|---|
| 当前 | JWT 签名必需;ZAK 可选 |
| 2025 年 11 月 | SDK v6.6.10 引入 OBF 专用错误码 |
| 2026 年 2 月 23 日 | 外部会议必须提供 OBF 或 ZAK |
OAuth 权限范围汇总:
| 令牌 | 所需 Scope |
|---|---|
| ZAK 令牌 | user:read:zak |
| OBF 令牌 | user:read:token |
十、延伸:在仓库中继续深入
围绕本主题,仓库内还有一份完整的参考文档矩阵,按需查阅即可构成闭环:
- references/bot-authentication.md —— 本文主体,机器人三类令牌详解;
- references/authorization.md —— JWT 签名字段与短时效最佳实践;
- references/signature-playbook.md —— "join failed" 的签名根因排查手册(含 4003 错误与
passWord大小写陷阱); - references/troubleshooting.md —— 通用问题与解法;
- linux/SKILL.md —— Linux 无头机器人(C++)的初始化、认证与 raw recording 权限链路;
- general/use-cases/meeting-bots.md —— 会议机器人整体架构与"REST 签发令牌 + SDK 入会 + 云录制 webhook"的技能编排;
- oauth/references/granular-scopes.md ——
user:read:zak、user:read:token等 scope 的完整清单。
适用前提与限制:本文所有结论均以仓库内 Zoom 插件的 meeting-sdk 参考文档为准,其中 OBF 强制要求的时间点(2026-02-23)、SDK 错误码引入版本(v6.6.10)与生产凭据审核周期(4–6 周)均为文档所述的 Zoom 侧政策,落地前建议以你所用 SDK 版本的实际行为为准;本文代码示例为文档内给出的参考实现,不构成对 Zoom API 契约的完整定义。
【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考