Engineering

遊戲大廳的 iframe SDK 設計:跨來源安全實戰

2026 年 5 月 5 日約 11 分鐘閱讀作者:Hao0321 Studio

遊戲大廳裡 14 款遊戲都是用 iframe 嵌入 play.html。每款遊戲在自己的 iframe 沙箱裡跑,跟外殼大廳之間透過一套自製 SDK 通訊:上傳分數、讀取使用者、觸發廣告、回報遊玩進度。這套通訊機制看似簡單,但牽涉跨來源安全、postMessage 協定設計、版本管理三個複雜議題。

為什麼用 iframe 而不是直接 import

三個優勢:

  1. 故障隔離:一款遊戲爆掉不會帶倒整個大廳
  2. 狀態隔離:每款遊戲的 localStorage 命名空間獨立
  3. 外部嵌入:遊戲檔案可以獨立發布到 itch.io、Gumroad,不依賴大廳

SDK 的雙向設計

iframe 跟父頁面(大廳)之間有兩個通訊方向:

方向用途
遊戲 → 大廳上傳分數、回報玩遊戲、請求廣告播放、登出
大廳 → 遊戲傳遞使用者資訊、暫停 / 繼續、調整音量

同源 vs 跨源

本站架構:

同源(同 protocol、同 domain、同 port)所以可以用 window.parent 直接互讀。但為了未來想嵌入別站,還是設計了 postMessage 協定。

SDK 的 API 設計

大廳暴露給遊戲的全域物件:

// 遊戲端使用方式
window.parent.haoGame.reportScore(8240);
window.parent.haoGame.getUser(); // → { id, name, coins, ... }
window.parent.haoGame.requestAd('revive', { reward: 50 });
window.parent.haoGame.setReady();

// 大廳端定義 (在 play.html)
window.haoGame = {
  reportScore(score) { /* 上傳到 api.hao0321.com */ },
  getUser() { return JSON.parse(localStorage.hao_user || 'null'); },
  requestAd(type, params) { /* 觸發 AdSense */ },
  setReady() { /* 隱藏 loading 動畫 */ },
};

跨源備援:postMessage 協定

當遊戲被嵌到其他網域時,window.parent 會被瀏覽器擋掉。SDK 自動降級到 postMessage:

// 遊戲端 SDK
function reportScore(score) {
  if (sameOrigin()) {
    window.parent.haoGame.reportScore(score);
  } else {
    window.parent.postMessage({
      type: 'reportScore',
      payload: { score },
      __hao: true,
    }, '*');
  }
}

function sameOrigin() {
  try {
    return window.parent.location.origin === window.location.origin;
  } catch {
    return false;
  }
}

大廳端 listener:

window.addEventListener('message', (e) => {
  // 安全檢查
  if (!e.data || !e.data.__hao) return;
  if (!isAllowedOrigin(e.origin)) return;

  switch (e.data.type) {
    case 'reportScore':
      reportScore(e.data.payload.score);
      break;
    case 'requestAd':
      requestAd(e.data.payload.type);
      break;
  }
});

安全檢查:origin allowlist

postMessage 最常見的漏洞是接受任何來源訊息。修法:

const ALLOWED_ORIGINS = [
  'https://game.hao0321.com',
  'https://hao0321.com',
  'https://itch.io',
  'https://gum.co',
];

function isAllowedOrigin(origin) {
  return ALLOWED_ORIGINS.some(o =>
    origin === o || origin.endsWith('.' + o.replace(/^https?:\/\//, ''))
  );
}

版本管理:API 演化

SDK 一旦發布到外部,就要永遠相容。我用 __hao_version 欄位標記:

window.parent.postMessage({
  __hao: true,
  __hao_version: 2,
  type: 'reportScore',
  payload: { score: 8240, gameId: 'dodge-master' },
}, '*');

大廳端根據版本決定怎麼處理。新版可以加欄位但不能改舊欄位語意。

登入狀態同步

玩家在大廳登入後,狀態存在 localStorage。但 iframe 因為同源所以可以共享 — 不需要額外傳遞。

跨源情境(嵌到別站)就需要明確傳:

// 遊戲端
function getUser() {
  return new Promise((resolve) => {
    if (sameOrigin()) {
      resolve(JSON.parse(localStorage.hao_user || 'null'));
      return;
    }
    const handler = (e) => {
      if (e.data.__hao_reply === 'getUser') {
        window.removeEventListener('message', handler);
        resolve(e.data.payload);
      }
    };
    window.addEventListener('message', handler);
    window.parent.postMessage({ __hao: true, type: 'getUser' }, '*');
    setTimeout(() => resolve(null), 1000); // timeout
  });
}

X-Frame-Options 踩坑

Cloudflare Pages 預設會把 X-Frame-Options: DENY 加到所有檔案,連同源 iframe 都被擋。修法:在 _headers 改成 SAMEORIGIN(或對 game/* 路徑單獨設):

/*
  X-Frame-Options: SAMEORIGIN

/game/*
  X-Frame-Options: DENY  # 遊戲檔案禁止其他站嵌入

除錯技巧

iframe 通訊壞掉時很難 debug,因為 console log 在不同 frame。我加了一個 wrapper:

function log(...args) {
  console.log('[GAME]', ...args);
  if (sameOrigin()) {
    window.parent.console.log('[FROM GAME]', ...args);
  }
}

這樣大廳的 console 也看得到遊戲的 log,方便整合除錯。

結語

iframe + postMessage 是被低估的 web 平台特性。用對了可以做出組件化、可獨立發布、安全隔離的應用架構。本站 14 款遊戲共用一套 SDK,新遊戲開發只需要呼叫 5 個 API 就能整合到大廳。

SDK 完整原始碼(約 200 行)我計畫整理成 GitHub repo 開放。有興趣可以寄信 lo246179268@gmail.com