logo
开发者文档
搜索
推式接入(身份握手 / 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,则用 & 拼接 wsawsa 值已 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 抹除 wsaconsumeHandoff 默认已做)
  • 换成自有会话,后续 XHR/fetch/img 不再透传 wsa
  • 处理「用户直接访问、无 wsa」的分支(引导登录或匿名)
  • 校验失败按 WsaVerificationError.code 给出可读提示

安全细节与多语言校验:令牌校验与安全。想改成「应用内登录按钮」:拉式登录