推式接入(身份握手 / Push Handoff)
推式接入(身份握手 / Push Handoff)
场景:用户在 工作空间 → 扩展 页点击开发者的扩展应用,平台把用户身份以 ?wsa=<JWT> 推给目标扩展应用落地页。开发者在落地页消费并在后端校验后、换成自己的会话。
本模式下开发者不需要调用平台的签名端点——那是工作空间前端在用户点击时主动调用的。开发者只负责「收下并校验」。
端到端时序
loading...
sequenceDiagram
autonumber
actor User as 用户访问扩展应用
participant GB as GPTBots 平台
participant FE as 扩展应用-落地页
participant BE as 扩展应用-后端
User->>GB: 点击应用图标(POST sign-token)
GB->>GB: 校验点击人是该 workspace 成员
GB->>GB: 用密钥签 wsa<br/>(5 分钟 JWT,aud=扩展应用 host)
GB->>FE: 打开 app_home_url?wsa=<JWT>
Note over FE: consumeHandoff()<br/>读 ?wsa=,POST 至扩展应用后端
FE->>BE: POST /session/exchange { wsa }
BE->>BE: verifyWsa() → identity
BE->>BE: 建立自有 session
BE-->>FE: 返回 session(Set-Cookie)
Note over FE: history.replaceState 抹掉 ?wsa=
平台生成跳转 URL 的规则(开发者无需实现):
- 从注册信息里按
app_home_url精确匹配目标扩展应用(不在册的 URL 一律拒签,防身份外泄)。 - 校验点击人确实是该工作空间(
workspace_id)成员。 - 若
app_home_url已带 query,则用&拼接wsa;wsa值已 URL 编码,你读取后无需手动解码。
前端:消费落地页的 wsa
使用 SDK(推荐)
import { consumeHandoff } from '@gptbots/workspace-extension-sdk';
try {
const identity = await consumeHandoff({
exchangeUrl: '/session/exchange', // 你自己的后端校验接口
// search: location.search, // 默认读 location.search
// paramName: 'wsa', // 默认参数名 wsa
// strip: true, // 默认成功后抹除 URL 中的 wsa
});
bootYourApp(identity);
} catch (e) {
// 没有 wsa(用户直接访问)、或后端校验失败
showLoginOrError(e);
}
import { consumeHandoff } from '@gptbots/workspace-extension-sdk';
try {
const identity = await consumeHandoff({
exchangeUrl: '/session/exchange', // 你自己的后端校验接口
// search: location.search, // 默认读 location.search
// paramName: 'wsa', // 默认参数名 wsa
// strip: true, // 默认成功后抹除 URL 中的 wsa
});
bootYourApp(identity);
} catch (e) {
// 没有 wsa(用户直接访问)、或后端校验失败
showLoginOrError(e);
}
此代码块在浮窗中显示
consumeHandoff 做四件事:① 读 ?wsa= ② POST { wsa } 到 exchangeUrl ③ 成功后 history.replaceState 抹掉 ?wsa= ④ 返回后端回传的 identity。
抹除时机:令牌只在成功交换后才从 URL 抹除,以便一次瞬时失败可以刷新重试。这是一枚 5 分钟一次性令牌;若开发者希望失败也立即抹除,可
catch后手动调stripHandoffToken()。
不建会话,只读身份(receive-only)
import { readHandoffToken, stripHandoffToken } from '@gptbots/workspace-extension-sdk';
const token = readHandoffToken(); // 纯函数,返回原始 JWT 字符串或 null
if (token) {
// 仍建议把 token 发给你后端 verifyWsa 后再信任其内容(不要在前端解析 JWT 当可信身份)
await fetch('/telemetry/visit', { method: 'POST', body: JSON.stringify({ wsa: token }) });
}
stripHandoffToken(); // 无论如何抹掉 URL 里的 wsa
import { readHandoffToken, stripHandoffToken } from '@gptbots/workspace-extension-sdk';
const token = readHandoffToken(); // 纯函数,返回原始 JWT 字符串或 null
if (token) {
// 仍建议把 token 发给你后端 verifyWsa 后再信任其内容(不要在前端解析 JWT 当可信身份)
await fetch('/telemetry/visit', { method: 'POST', body: JSON.stringify({ wsa: token }) });
}
stripHandoffToken(); // 无论如何抹掉 URL 里的 wsa
此代码块在浮窗中显示
⚠️ 不要在前端解开 JWT 就当可信身份。JWT 的签名只有用密钥才能校验,而密钥只在后端。前端解析仅能用于「非安全」的展示占位,任何授权判断都必须以后端
verifyWsa的结果为准。
后端:校验 wsa
3.1 用 SDK(推荐)
import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';
app.post('/session/exchange', (req, res) => {
try {
const identity = verifyWsa(req.body.wsa, {
secret: process.env.EXTENSION_APP_SECRET, // Tier2 per-app 密钥 或 Tier1 共享密钥
audience: 'app.example.com', // 扩展应用 host,必须等于 aud
// issuer: 'gptbots-workspace', // 默认
// leewaySeconds: 30, // 时钟漂移容忍,默认 30s
// algorithms: ['HS256'], // 默认
});
// 多租户隔离:把请求归到 identity.workspaceId 名下
const sid = createSession(identity);
res.cookie('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax' });
res.json(identity);
} catch (e) {
const code = e instanceof WsaVerificationError ? e.code : 'Error';
res.status(401).json({ code });
}
});
import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';
app.post('/session/exchange', (req, res) => {
try {
const identity = verifyWsa(req.body.wsa, {
secret: process.env.EXTENSION_APP_SECRET, // Tier2 per-app 密钥 或 Tier1 共享密钥
audience: 'app.example.com', // 扩展应用 host,必须等于 aud
// issuer: 'gptbots-workspace', // 默认
// leewaySeconds: 30, // 时钟漂移容忍,默认 30s
// algorithms: ['HS256'], // 默认
});
// 多租户隔离:把请求归到 identity.workspaceId 名下
const sid = createSession(identity);
res.cookie('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax' });
res.json(identity);
} catch (e) {
const code = e instanceof WsaVerificationError ? e.code : 'Error';
res.status(401).json({ code });
}
});
此代码块在浮窗中显示
不使用 SDK
wsa 是标准 HS256 JWT,任意语言的 JWT 库都能校验。Java / Node / Python 示例见 05-令牌校验与安全 §3。无论用不用 SDK,签名 / iss / aud / exp 四项都必须校验。
注册扩展应用(拿到密钥)
工作空间的 OWNER/ADMIN:工作空间 → 空间管理 → 扩展应用 → 添加,填写:
| 字段 | 说明 |
|---|---|
| 应用名称 | 展示名,建议 ≤ 12 个汉字避免截断 |
| 应用图标 | 方形图标,建议 ≥ 128×128 |
| 应用入口 URL | 你的 app_home_url,作为唯一键,签名时按完整字符串严格匹配 |
| 鉴权模式 | 选 workspace_account(需要传递身份) |
提交后一次性明文展示 App Secret,立即复制保存(关闭后仅能轮换)。
落地页 checklist
- 收到
?wsa=后先 POST 至扩展应用后端服务进行校验,再信任内容 - 校验通过立刻
history.replaceState抹除wsa(consumeHandoff默认已做) - 换成自有会话,后续 XHR/fetch/img 不再透传
wsa - 处理「用户直接访问、无
wsa」的分支(引导登录或匿名) - 校验失败按
WsaVerificationError.code给出可读提示
