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;遇到任何問題時請查閱 常見問題與排錯