Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> JavaScript

HTML5 Cross-browser iframe postMessage: Child đến Parent


Same-Origin vs Cross-Origin

  • Same-origin: Cùng protocol, domain, port → Truy cập trực tiếp parent.document, parent.someFunction()
  • Cross-origin: Khác domain/port/protocol → **Chỉ dùng postMessage** (Security policy)

postMessage API

// Gửi message
targetWindow.postMessage(message, targetOrigin);

// Nhận message
window.addEventListener('message', (event) => {
  // KHÔNG BAO GIỜ tin cậy event.data mà không verify origin
  if (event.origin !== 'https://trusted-domain.com') return;
  
  // Xử lý message
  console.log('Received:', event.data);
});

Parent Page (Receiver)

<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <title>Parent Page</title>
  <style>
    body { font-family: sans-serif; padding: 2rem; }
    iframe { width: 100%; height: 300px; border: 1px solid #ddd; border-radius: 8px; }
    .log { background: #f8f9fa; padding: 1rem; margin-top: 1rem; border-radius: 4px; font-family: monospace; max-height: 200px; overflow-y: auto; }
    .log-entry { padding: 0.25rem 0; border-bottom: 1px solid #eee; }
    .log-entry:last-child { border-bottom: none; }
    .sent { color: #27ae60; }
    .received { color: #3498db; }
    .error { color: #e74c3c; }
  </style>
</head>
<body>
  <h1>Parent Page (Receiver)</h1>
  <p>Domain: <strong id="parentDomain"></strong></p>
  
  <iframe id="childFrame" src="child.html"></iframe>
  
  <div class="log" id="log"></div>

  <script>
    document.getElementById('parentDomain').textContent = window.location.origin;
    const log = document.getElementById('log');
    
    function addLog(message, type = 'received') {
      const div = document.createElement('div');
      div.className = `log-entry ${type}`;
      div.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
      log.appendChild(div);
      log.scrollTop = log.scrollHeight;
    }
    
    // Danh sách origin được tin cậy
    const ALLOWED_ORIGINS = [
      'https://localhost:3000',
      'https://127.0.0.1:5500',
      'https://your-child-domain.com'
    ];
    
    window.addEventListener('message', (event) => {
      // 1. VALIDATE ORIGIN - QUAN TRỌNG NHẤT
      if (!ALLOWED_ORIGINS.includes(event.origin)) {
        addLog(`BLOCKED: Origin không được tin cậy: ${event.origin}`, 'error');
        return;
      }
      
      // 2. Xử lý message
      const data = event.data;
      addLog(`Nhận từ ${event.origin}: ${JSON.stringify(data)}`);
      
      // 3. Reply nếu cần (two-way communication)
      if (data.type === 'ping') {
        event.source.postMessage({ type: 'pong', timestamp: Date.now() }, event.origin);
      }
    });
    
    // Gửi message đến iframe (nếu cần)
    function sendToChild(data) {
      const iframe = document.getElementById('childFrame');
      if (iframe.contentWindow) {
        iframe.contentWindow.postMessage(data, '*'); // Hoặc origin cụ thể
        addLog(`Gửi đến child: ${JSON.stringify(data)}`, 'sent');
      }
    }
  </script>
</body>
</html>

Child Page (Sender) - iframe content

<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <title>Child Page (Iframe)</title>
  <style>
    body { font-family: sans-serif; padding: 1.5rem; background: #fdfdfd; }
    button { margin: 0.5rem; padding: 0.5rem 1rem; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; }
    .log { background: #fff; border: 1px solid #ddd; padding: 1rem; margin-top: 1rem; border-radius: 4px; font-family: monospace; max-height: 200px; overflow-y: auto; }
  </style>
</head>
<body>
  <h2>Child Page (Sender)</h2>
  <p>Domain: <strong id="childDomain"></strong></p>
  
  <button onclick="sendMessage('hello')>Gửi 'hello'</button>
  <button onclick="sendMessage({type: 'userAction', action: 'click', data: {id: 123}})">Gửi object phức tạp</button>
  <button onclick="sendPing()">Gửi Ping (chờ Pong)</button>
  
  <div class="log" id="log"></div>

  <script>
    document.getElementById('childDomain').textContent = window.location.origin;
    const log = document.getElementById('log');
    const parentOrigin = 'https://localhost:3000'; // Parent origin - CẦN CẤU HÌNH
    
    function addLog(message, type = 'sent') {
      const div = document.createElement('div');
      div.className = `log-entry ${type}`;
      div.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
      log.appendChild(div);
      log.scrollTop = log.scrollHeight;
    }
    
    function sendMessage(data) {
      // Gửi đến parent
      window.parent.postMessage(data, parentOrigin);
      addLog(`Đã gửi: ${JSON.stringify(data)}`);
    }
    
    function sendPing() {
      const message = { type: 'ping', timestamp: Date.now(), id: Math.random() };
      window.parent.postMessage(message, parentOrigin);
      addLog(`Ping sent: ${message.id}`);
    }
    
    // Nhận reply từ parent
    window.addEventListener('message', (event) => {
      // Verify origin
      if (event.origin !== parentOrigin) {
        addLog(`BLOCKED reply from: ${event.origin}`, 'error');
        return;
      }
      
      const data = event.data;
      addLog(`Nhận reply: ${JSON.stringify(data)}`, 'received');
    });
  </script>
</body>
</html>

Pattern nâng cao: Message Channel (Two-way, bảo mật hơn)

// Parent
const channel = new MessageChannel();
iframe.contentWindow.postMessage({ type: 'init' }, childOrigin, [channel.port2]);

channel.port1.onmessage = (event) => {
  console.log('Received from child:', event.data);
};

// Child
window.addEventListener('message', (event) => {
  if (event.data.type === 'init' && event.ports[0]) {
    const port = event.ports[0];
    port.onmessage = (e) => {
      // Handle message from parent
      port.postMessage({ response: 'ok' });
    };
    // Gửi message đến parent qua port
    port.postMessage({ ready: true });
  }
});

Security Checklist

  • ✅ **Luôn validate event.origin** trước khi xử lý
  • ✅ Gửi targetOrigin cụ thể (không dùng * trừ khi broadcast)
  • ✅ Không gửi dữ liệu nhạy cảm (token, password) qua postMessage
  • ✅ Sử dụng MessageChannel cho two-way communication bảo mật hơn
  • ✅ Xác thực message structure (type, payload) trước khi xử lý
  • ✅ Rate limiting nếu cần (tránh spam message)

Debug Tips

// Log tất cả message để debug
window.addEventListener('message', (e) => {
  console.group(`📨 Message from ${e.origin}`);
  console.log('Data:', e.data);
  console.log('Source:', e.source);
  console.log('Ports:', e.ports);
  console.groupEnd();
});