05 · 令牌校验与安全
05 · 令牌校验与安全
无论推式还是拉式,最终开发者都会拿到一枚 wsa(HS256 JWT)。这一篇是所有接入方的必读项——校验不严 = 身份可伪造。
强制校验清单
拿到 wsa 后,在扩展应用的后端服务中按顺序校验(verifyWsa 已内置全部):
- 签名:用平台分发/注册获得的密钥以 HS256 校验。失败 → 拒绝。
exp:当前时间 ≤exp(含少量 leeway)。exp必须存在——没有exp的令牌应直接拒绝(否则等于永不过期)。iss:必须等于gptbots-workspace。aud:必须等于目标扩展应用的 host。这是防止「令牌被偷去打另一个应用」的关键防线,不可省略。iat/nbf(如存在):不得在未来(超出 leeway),防止「签发时间设在遥远未来」的令牌长期可用。workspace_id分租:若扩展做了多工作空间隔离,把请求归到该workspace_id下,禁止跨租访问。- 落地后立刻抹除 URL 里的
wsa/code:history.replaceState,避免用户复制 URL 时把令牌一起分享出去。 - 建立自有会话:换成扩展应用自己的 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 解码),与平台签名侧保持一致。
安全要求(逐条落实)
- 密钥即整个安全模型。当前 HS256 是对称密钥:一旦泄露,任何人都能伪造任意工作空间用户身份打进接入扩展应用。只放后端(环境变量/KMS),绝不写进前端代码、git 仓库、日志、客户端配置。
- JWT 在 URL 中的暴露面(仅推式):query string 会被浏览器历史、Web 服务器 access log、CDN 缓存日志、Referer 记录。即使被记录,5 分钟内被拿到仍可利用。落地后立即
history.replaceState抹除(consumeHandoff默认已做)。拉式(M-Auth)天生不把wsa放进 URL,更安全。 - 不要把
wsa透传到子资源。换成自有会话后,后续 XHR/fetch/img 一律不再带原始wsa,否则它会出现在每个子资源的 Referer 里。 - 时钟同步。HS256 对
exp严格判定,服务器时钟需 NTP 同步;示例里 30sleeway容忍小幅漂移,别扩大到分钟级。 role仅作默认权限映射。它只反映用户在该工作空间的角色,不代表在你应用内的权限;你的应用维护自己的权限模型。- 多租户隔离。始终用
workspace_id作为数据隔离键,防止 A 工作空间的用户读到 B 的数据。 - 密钥轮换/吊销应急:在空间管理里对应用「轮换密钥」,会生成新密钥并一次性展示,随后用新密钥重新配置你的后端。
5. 前端可信边界(重要)
- 前端只做展示占位,不做授权判断。前端能 base64 解开 JWT 的 payload,但没有密钥无法验签——任何人都能伪造一个「看起来对」的 payload。
- 所有「这个人是谁、能不能做某事」的判断,必须以扩展应用后端
verifyWsa的结果为准。 - 因此标准策略是:前端把
wsa/code交给扩展应用后端 → 后端校验 → 后端建会话 → 前端只认这个会话。
下一步:SDK-API-参考 查完整 API;遇到任何问题时请查阅 常见问题与排错。
