浏览器原理 · 45/51
1. addEventListener 第三个参数 2. addEventListener 与 attachEvent 区别 3. 浏览器兼容性测试与内核 4. 浏览器兼容性问题 5. 浏览器内核与引擎 6. 浏览器图层创建条件 7. 浏览器多进程架构 8. 浏览器渲染机制 9. 浏览器存储方案 10. 浏览器版本检测方法 11. children 与 childNodes 区别 12. 常见浏览器兼容性问题 13. Chrome 页面进程数量 14. 坐标系统对比 15. 多标签页通讯方案 16. 删除 Cookie 17. 自定义事件 18. DOM 事件处理方式演进 19. 元素尺寸属性对比 20. DOM 节点操作 21. DOM 事件机制 22. addEventListener 与 attachEvent 的区别 23. 获取页面所有复选框 24. HTMLCollection 与 NodeList 区别 25. Hybrid 应用开发 26. 强缓存命中机制 27. 浏览器缓存机制 28. 页面编码与资源编码不一致处理 29. jQuery 事件绑定方法对比 30. Input 点击触发的事件顺序 31. JavaScript 浏览器兼容性问题 32. jQuery 多事件绑定实现 33. JSBridge 原理 34. 链接点击后 Hover 失效解决方案 35. 减少重绘和回流的性能优化 36. 移动端 300ms 点击延迟问题 37. 移动端视口配置 38. 移动端点击穿透问题解决 39. 移动端兼容性问题 40. JSBridge 原理与实现 41. 移动端 1px 像素问题解决方案 42. 浏览器渲染流程 43. 页面加载完成事件对比 44. Offset、Scroll、Client 属性对比 45. 同源策略与跨域解决方案 46. Script 标签位置对页面加载的影响 47. Service Worker 与 PWA 48. 存储方案对比:Cookie、Storage、IndexedDB 49. 强缓存默认时间 50. URL 到页面显示的完整过程 51. V8 引擎 JavaScript 执行过程

同源策略与跨域解决方案

浏览器同源策略的限制及 6 种跨域方案实现

问题

什么是浏览器的同源策略?有哪些跨域解决方案?

解答

同源策略

同源策略是浏览器的安全机制,要求协议、域名、端口三者完全相同才算同源。

http://example.com/a.html
http://example.com/b.html        ✅ 同源
https://example.com/a.html       ❌ 协议不同
http://api.example.com/a.html    ❌ 域名不同
http://example.com:8080/a.html   ❌ 端口不同

受限制的操作:

  • AJAX 请求
  • DOM 访问
  • Cookie、LocalStorage、IndexedDB 读取

1. CORS(跨域资源共享)

最标准的跨域方案,需要服务端配合设置响应头。

// 前端:正常发请求
fetch('http://api.example.com/data', {
  method: 'POST',
  credentials: 'include', // 携带 cookie
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'test' })
})
// 服务端:Node.js Express
app.use((req, res, next) => {
  // 允许的源,不能用 * 如果要携带 cookie
  res.setHeader('Access-Control-Allow-Origin', 'http://example.com')
  // 允许的方法
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
  // 允许的自定义头
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  // 允许携带 cookie
  res.setHeader('Access-Control-Allow-Credentials', 'true')
  // 预检请求缓存时间
  res.setHeader('Access-Control-Max-Age', '86400')
  
  // 处理预检请求
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204)
  }
  next()
})

简单请求 vs 预检请求:

// 简单请求:直接发送
// 条件:GET/HEAD/POST + 简单头 + Content-Type 为 text/plain、multipart/form-data、application/x-www-form-urlencoded

// 预检请求:先发 OPTIONS
// 触发条件:PUT/DELETE、自定义头、Content-Type 为 application/json 等

2. JSONP

利用 <script> 标签不受同源限制的特性,只支持 GET 请求。

// 封装 JSONP
function jsonp(url, callbackName = 'callback') {
  return new Promise((resolve, reject) => {
    // 生成唯一回调函数名
    const fnName = `jsonp_${Date.now()}_${Math.random().toString(36).slice(2)}`
    
    // 挂载全局回调
    window[fnName] = (data) => {
      resolve(data)
      // 清理
      document.head.removeChild(script)
      delete window[fnName]
    }
    
    // 创建 script 标签
    const script = document.createElement('script')
    script.src = `${url}?${callbackName}=${fnName}`
    script.onerror = () => {
      reject(new Error('JSONP request failed'))
      document.head.removeChild(script)
      delete window[fnName]
    }
    
    document.head.appendChild(script)
  })
}

// 使用
jsonp('http://api.example.com/data')
  .then(data => console.log(data))
// 服务端返回格式
// jsonp_xxx({ "name": "test", "age": 18 })

app.get('/data', (req, res) => {
  const callback = req.query.callback
  const data = { name: 'test', age: 18 }
  res.send(`${callback}(${JSON.stringify(data)})`)
})

3. Nginx 反向代理

通过同源的 Nginx 服务器转发请求,前端无感知。

server {
    listen 80;
    server_name example.com;
    
    # 前端静态资源
    location / {
        root /var/www/html;
        index index.html;
    }
    
    # API 代理
    location /api/ {
        # 转发到后端服务
        proxy_pass http://api.backend.com/;
        
        # 传递真实 IP
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
// 前端请求同源地址
fetch('/api/users')  // 实际转发到 http://api.backend.com/users

4. postMessage

用于窗口间通信,如 iframe、window.open 打开的页面。

<!-- 父页面:http://parent.com -->
<iframe id="child" src="http://child.com/page.html"></iframe>

<script>
const iframe = document.getElementById('child')

// 发送消息
iframe.onload = () => {
  iframe.contentWindow.postMessage(
    { type: 'greeting', data: 'Hello from parent' },
    'http://child.com'  // 目标源,* 表示不限制
  )
}

// 接收消息
window.addEventListener('message', (e) => {
  // 验证来源
  if (e.origin !== 'http://child.com') return
  console.log('收到子页面消息:', e.data)
})
</script>
<!-- 子页面:http://child.com -->
<script>
// 接收消息
window.addEventListener('message', (e) => {
  // 验证来源
  if (e.origin !== 'http://parent.com') return
  console.log('收到父页面消息:', e.data)
  
  // 回复消息
  e.source.postMessage(
    { type: 'reply', data: 'Hello from child' },
    e.origin
  )
})
</script>

5. WebSocket

WebSocket 协议不受同源策略限制。

// 前端
const ws = new WebSocket('ws://api.example.com/socket')

ws.onopen = () => {
  console.log('连接建立')
  ws.send(JSON.stringify({ type: 'subscribe', channel: 'news' }))
}

ws.onmessage = (e) => {
  const data = JSON.parse(e.data)
  console.log('收到消息:', data)
}

ws.onerror = (e) => console.error('连接错误:', e)
ws.onclose = () => console.log('连接关闭')
// 服务端:Node.js + ws
const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 8080 })

wss.on('connection', (ws, req) => {
  // 可以检查 origin
  const origin = req.headers.origin
  console.log('客户端连接:', origin)
  
  ws.on('message', (message) => {
    const data = JSON.parse(message)
    // 处理消息并回复
    ws.send(JSON.stringify({ type: 'response', data: 'received' }))
  })
})

6. document.domain

仅适用于主域相同的子域之间,如 a.example.comb.example.com

<!-- http://a.example.com/page.html -->
<iframe id="child" src="http://b.example.com/page.html"></iframe>

<script>
// 设置为主域
document.domain = 'example.com'

// 现在可以访问 iframe 的内容
const iframe = document.getElementById('child')
iframe.onload = () => {
  // 直接访问 iframe 的 window 和 document
  console.log(iframe.contentWindow.document.body)
}
</script>
<!-- http://b.example.com/page.html -->
<script>
// 子页面也要设置
document.domain = 'example.com'
</script>

方案对比

方案适用场景优点缺点
CORS通用 API 请求标准方案,支持所有请求类型需要服务端配合
JSONP兼容老浏览器兼容性好只支持 GET,有安全风险
Nginx 反代生产环境前端无感知需要运维配置
postMessage窗口通信安全可控仅限窗口间
WebSocket实时通信全双工,不受限制需要专门的服务端
document.domain子域通信简单仅限主域相同,已废弃

关键点

  • 同源 = 协议 + 域名 + 端口完全相同
  • CORS 是标准方案,注意简单请求和预检请求的区别
  • JSONP 利用 script 标签特性,只能 GET,有 XSS 风险
  • postMessage 必须验证 origin 防止安全问题
  • document.domain 已被废弃,不推荐使用