logo
开发者文档
搜索
05 · 令牌校验与安全

05 · 令牌校验与安全

无论推式还是拉式,最终开发者都会拿到一枚 wsa(HS256 JWT)。这一篇是所有接入方的必读项——校验不严 = 身份可伪造。

强制校验清单

拿到 wsa 后,在扩展应用的后端服务中按顺序校验(verifyWsa 已内置全部):

  1. 签名:用平台分发/注册获得的密钥以 HS256 校验。失败 → 拒绝。
  2. exp:当前时间 ≤ exp(含少量 leeway)。exp 必须存在——没有 exp 的令牌应直接拒绝(否则等于永不过期)。
  3. iss:必须等于 gptbots-workspace
  4. aud:必须等于目标扩展应用的 host这是防止「令牌被偷去打另一个应用」的关键防线,不可省略。
  5. iat / nbf(如存在):不得在未来(超出 leeway),防止「签发时间设在遥远未来」的令牌长期可用。
  6. workspace_id 分租:若扩展做了多工作空间隔离,把请求归到该 workspace_id 下,禁止跨租访问。
  7. 落地后立刻抹除 URL 里的 wsa / codehistory.replaceState,避免用户复制 URL 时把令牌一起分享出去。
  8. 建立自有会话:换成扩展应用自己的 session/cookie,后续请求不要再依赖 wsa——它 5 分钟就过期。

官方 SDK 的 verifyWsa 强制要求 exp 存在、校验 iat/nbf 未来漂移,并做常量时间签名比较与算法白名单(默认仅 HS256,杜绝 alg 混淆攻击)。用它就自动满足 1–5。


推荐用 SDK 校验

import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify'; try { const id = verifyWsa(wsa, { secret: process.env.EXTENSION_APP_SECRET, audience: 'app.example.com', }); // id: { accountId, role, workspaceId, username?, email?, avatar?, appName?, issuedAt?, expiresAt? } } catch (e) { if (e instanceof WsaVerificationError) { // e.code: InvalidToken | InvalidSignature | Expired | NotYetValid // | WrongIssuer | WrongAudience | MissingClaim | UnsupportedAlgorithm } }
                      
                      import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';

try {
  const id = verifyWsa(wsa, {
    secret: process.env.EXTENSION_APP_SECRET,
    audience: 'app.example.com',
  });
  // id: { accountId, role, workspaceId, username?, email?, avatar?, appName?, issuedAt?, expiresAt? }
} catch (e) {
  if (e instanceof WsaVerificationError) {
    // e.code: InvalidToken | InvalidSignature | Expired | NotYetValid
    //       | WrongIssuer | WrongAudience | MissingClaim | UnsupportedAlgorithm
  }
}

                    
此代码块在浮窗中显示

完整选项与错误码见 SDK-API-参考

不用 SDK 的校验(多语言)

wsa 是标准 HS256 JWT,任意 JWT 库都能校验。务必显式开启 iss / aud 校验并锁定 algorithms=['HS256']

Java(auth0 java-jwt)

JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SHARED_SECRET)) .withIssuer("gptbots-workspace") .withAudience("app.example.com") .acceptLeeway(30) // 容忍 30s 时钟漂移 .build(); DecodedJWT jwt = verifier.verify(wsaParam); String userId = jwt.getSubject(); String workspaceId = jwt.getClaim("workspace_id").asString(); String role = jwt.getClaim("role").asString(); String username = jwt.getClaim("username").asString(); // 可能为 null String email = jwt.getClaim("email").asString(); // 可能为 null
                      
                      JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SHARED_SECRET))
    .withIssuer("gptbots-workspace")
    .withAudience("app.example.com")
    .acceptLeeway(30) // 容忍 30s 时钟漂移
    .build();

DecodedJWT jwt = verifier.verify(wsaParam);
String userId      = jwt.getSubject();
String workspaceId = jwt.getClaim("workspace_id").asString();
String role        = jwt.getClaim("role").asString();
String username    = jwt.getClaim("username").asString();  // 可能为 null
String email       = jwt.getClaim("email").asString();     // 可能为 null

                    
此代码块在浮窗中显示

Node.js(jsonwebtoken)

const jwt = require('jsonwebtoken'); const payload = jwt.verify(wsaParam, SHARED_SECRET, { algorithms: ['HS256'], issuer: 'gptbots-workspace', audience: 'app.example.com', clockTolerance: 30, }); const { sub: userId, workspace_id, role, username, email, avatar } = payload;
                      
                      const jwt = require('jsonwebtoken');

const payload = jwt.verify(wsaParam, SHARED_SECRET, {
  algorithms: ['HS256'],
  issuer: 'gptbots-workspace',
  audience: 'app.example.com',
  clockTolerance: 30,
});
const { sub: userId, workspace_id, role, username, email, avatar } = payload;

                    
此代码块在浮窗中显示

Python(PyJWT)

import jwt payload = jwt.decode( wsa_param, SHARED_SECRET, algorithms=["HS256"], issuer="gptbots-workspace", audience="app.example.com", leeway=30, ) user_id = payload["sub"] workspace_id = payload["workspace_id"] role = payload["role"]
                      
                      import jwt

payload = jwt.decode(
    wsa_param,
    SHARED_SECRET,
    algorithms=["HS256"],
    issuer="gptbots-workspace",
    audience="app.example.com",
    leeway=30,
)
user_id      = payload["sub"]
workspace_id = payload["workspace_id"]
role         = payload["role"]

                    
此代码块在浮窗中显示

密钥编码提醒:HS256 直接把密钥字符串按 UTF-8 字节作为 HMAC key。Tier 2 的 per-app 密钥形如 wext_+64 hex,请原样字符串作为密钥传入(不要再做 hex 解码),与平台签名侧保持一致。

安全要求(逐条落实)

  1. 密钥即整个安全模型。当前 HS256 是对称密钥:一旦泄露,任何人都能伪造任意工作空间用户身份打进接入扩展应用。只放后端(环境变量/KMS),绝不写进前端代码、git 仓库、日志、客户端配置。
  2. JWT 在 URL 中的暴露面(仅推式):query string 会被浏览器历史、Web 服务器 access log、CDN 缓存日志、Referer 记录。即使被记录,5 分钟内被拿到仍可利用。落地后立即 history.replaceState 抹除consumeHandoff 默认已做)。拉式(M-Auth)天生不把 wsa 放进 URL,更安全。
  3. 不要把 wsa 透传到子资源。换成自有会话后,后续 XHR/fetch/img 一律不再带原始 wsa,否则它会出现在每个子资源的 Referer 里。
  4. 时钟同步。HS256 对 exp 严格判定,服务器时钟需 NTP 同步;示例里 30s leeway 容忍小幅漂移,别扩大到分钟级。
  5. role 仅作默认权限映射。它只反映用户在该工作空间的角色,不代表在你应用内的权限;你的应用维护自己的权限模型。
  6. 多租户隔离。始终用 workspace_id 作为数据隔离键,防止 A 工作空间的用户读到 B 的数据。
  7. 密钥轮换/吊销应急:在空间管理里对应用「轮换密钥」,会生成新密钥并一次性展示,随后用新密钥重新配置你的后端。

5. 前端可信边界(重要)

  • 前端只做展示占位,不做授权判断。前端能 base64 解开 JWT 的 payload,但没有密钥无法验签——任何人都能伪造一个「看起来对」的 payload。
  • 所有「这个人是谁、能不能做某事」的判断,必须以扩展应用后端 verifyWsa 的结果为准。
  • 因此标准策略是:前端把 wsa/code 交给扩展应用后端 → 后端校验 → 后端建会话 → 前端只认这个会话。

下一步:SDK-API-参考 查完整 API;遇到任何问题时请查阅 常见问题与排错