浏览器同源策略

理解同源策略的限制范围和跨域解决方案

问题

什么是浏览器同源策略 (Same-Origin Policy)?它限制了哪些行为?如何解决跨域问题?

解答

什么是同源

两个 URL 的协议域名端口完全相同,才算同源。

// 假设当前页面是 https://example.com:443/page

// 同源
'https://example.com/api'        // ✅ 协议、域名、端口都相同

// 不同源
'http://example.com/api'         // ❌ 协议不同 (http vs https)
'https://api.example.com/data'   // ❌ 域名不同 (子域名也算不同)
'https://example.com:8080/api'   // ❌ 端口不同

同源策略限制了什么

限制类型具体表现
DOM 访问无法读取跨域 iframe 的 DOM
Cookie/Storage无法读取跨域页面的 Cookie、localStorage
AJAX 请求无法发送跨域 XMLHttpRequest/Fetch 请求
// 尝试访问跨域 iframe 的 DOM - 会报错
const iframe = document.querySelector('iframe')
iframe.contentWindow.document // ❌ Blocked by SOP

// 跨域 AJAX 请求 - 会被拦截
fetch('https://other-domain.com/api')
  .then(res => res.json())
  .catch(err => console.log('跨域请求被阻止'))

跨域解决方案

1. CORS(推荐)

服务端设置响应头允许跨域:

// Node.js Express 示例
app.use((req, res, next) => {
  // 允许的源
  res.setHeader('Access-Control-Allow-Origin', 'https://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')
  
  // 处理预检请求
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204)
  }
  next()
})
// 前端请求携带凭证
fetch('https://api.example.com/data', {
  credentials: 'include' // 携带 Cookie
})

2. JSONP(仅支持 GET)

利用 <script> 标签不受同源策略限制的特性:

// 封装 JSONP 函数
function jsonp(url, callbackName = 'callback') {
  return new Promise((resolve, reject) => {
    // 创建全局回调函数
    const fnName = `jsonp_${Date.now()}`
    window[fnName] = (data) => {
      resolve(data)
      document.body.removeChild(script)
      delete window[fnName]
    }
    
    // 创建 script 标签
    const script = document.createElement('script')
    script.src = `${url}?${callbackName}=${fnName}`
    script.onerror = reject
    document.body.appendChild(script)
  })
}

// 使用
jsonp('https://api.example.com/data')
  .then(data => console.log(data))

3. 代理服务器

开发环境使用 webpack/vite 代理:

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api/, '')
      }
    }
  }
}

4. postMessage(跨窗口通信)

// 父页面发送消息
const iframe = document.querySelector('iframe')
iframe.contentWindow.postMessage({ type: 'greeting', data: 'hello' }, 'https://child.com')

// 子页面接收消息
window.addEventListener('message', (event) => {
  // 验证来源
  if (event.origin !== 'https://parent.com') return
  console.log(event.data) // { type: 'greeting', data: 'hello' }
  
  // 回复消息
  event.source.postMessage({ type: 'reply', data: 'hi' }, event.origin)
})

关键点

  • 同源定义:协议 + 域名 + 端口,三者完全一致
  • 限制范围:DOM 访问、Cookie/Storage 读取、AJAX 请求
  • CORS:服务端设置 Access-Control-Allow-* 响应头,是标准解决方案
  • JSONP:利用 script 标签绕过限制,只支持 GET 请求
  • 预检请求:非简单请求会先发 OPTIONS 请求询问服务器是否允许