跨域问题详解

什么是跨域?

跨域问题主要源自浏览器的同源策略(Same-Origin Policy)。同源策略要求协议、域名、端口三者完全相同,任何一个不同都会被视为跨域请求。例如,当你在 http://example.com 的网页中请求 http://api.example.com 的数据时,就会触发跨域请求限制。

同源的定义

  • 协议相同(http/https)
  • 域名相同
  • 端口相同

跨域限制内容

  • Ajax请求无法获取响应数据
  • Cookie、LocalStorage和IndexDB无法读取
  • DOM和JS对象无法获取

常见的跨域解决方案

1. JSONP(JSON with Padding)

原理:利用 <script> 标签不受同源策略限制的特性,通过动态创建script标签,请求跨域服务器,并在请求URL中指定回调函数,服务器返回这个回调函数的调用,回调函数中包含我们需要的数据。

实现方式

function jsonp(url, callback) {
  const script = document.createElement('script');
  // 在URL中添加回调函数名
  script.src = `${url}?callback=${callback.name}`;
  document.body.appendChild(script);
}
 
function handleData(data) {
  console.log(data);
}
 
jsonp('http://example.com/api/data', handleData);

服务器端返回:

handleData({"name": "张三", "age": 30});

优点

  • 兼容性好,在古老的浏览器中也能使用
  • 不需要XMLHttpRequest或fetch支持
  • 可以避开部分浏览器的跨域限制

缺点

  • 只支持GET请求
  • 存在安全隐患,容易受到XSS攻击
  • 需要服务器配合返回特定格式的数据
  • 无法处理请求错误

适用场景:简单的跨域数据获取,特别是在需要兼容老旧浏览器的情况下。

2. CORS(Cross-Origin Resource Sharing)

原理:CORS是一种基于HTTP头的机制,通过设置响应头允许服务器声明哪些源站有权限访问哪些资源。浏览器会自动进行CORS通信,关键在于服务器配置。

实现方式: 服务器端设置响应头:

Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true

简单请求与预检请求

  • 简单请求:满足特定条件的请求(如GET、HEAD、POST方法且Content-Type限于application/x-www-form-urlencoded、multipart/form-data或text/plain)直接发送
  • 预检请求:不满足简单请求条件的请求会先发送OPTIONS请求进行预检,获得服务器许可后再发送实际请求

优点

  • 支持所有类型的HTTP请求
  • 是W3C标准,浏览器原生支持
  • 可以配置精细的访问控制规则
  • 支持携带身份凭证(如Cookie)

缺点

  • 不支持IE10以下的浏览器
  • 配置相对复杂
  • 需要服务器端配合设置

适用场景:现代浏览器的跨域请求,特别是需要进行POST、PUT等非GET请求的场景。

3. document.domain

原理:通过设置相同的document.domain,实现不同子域之间的跨域通信。

实现方式: 在 a.example.comb.example.com 两个页面中都设置:

document.domain = 'example.com';

优点

  • 实现简单
  • 可以通信的方式多样,可以操作DOM、共享数据等

缺点

  • 只适用于主域相同,子域不同的情况
  • 存在安全风险
  • 在现代浏览器中逐渐被弃用

适用场景:主域相同的子域之间的通信,如企业内部多个子系统之间的数据共享。

4. postMessage

原理:HTML5引入的API,允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文档、多窗口、跨域消息传递。

实现方式: 发送方:

// otherWindow: 接收消息的窗口引用
// message: 发送的数据
// targetOrigin: 指定接收消息的窗口的源
otherWindow.postMessage('Hello world!', 'http://example.com');

接收方:

window.addEventListener('message', function(event) {
  // 验证发送方的源
  if (event.origin !== 'http://trusted-source.com') return;
  console.log('收到消息:', event.data);
});

优点

  • HTML5标准API,浏览器原生支持
  • 可以跨域通信
  • 可以在各种场景下使用(iframe、新开窗口等)
  • 可以传递结构化数据

缺点

  • 需要明确知道接收消息的窗口引用
  • 需要进行安全验证,防止恶意网站的消息

适用场景

  • 页面与其打开的新窗口之间的通信
  • 页面与嵌套的iframe之间的通信
  • 多窗口之间的通信

5. Nginx代理

原理:利用服务器不受浏览器同源策略限制的特点,通过Nginx反向代理将跨域请求转发到目标服务器,再将响应返回给客户端。

实现方式: Nginx配置:

server {
  listen 80;
  server_name example.com;
  
  location /api/ {
    proxy_pass http://api.another-domain.com/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }
}

CORS配置方式

server {
  listen 80;
  server_name api.example.com;
  
  location / {
    if ($request_method = 'OPTIONS') {
      add_header 'Access-Control-Allow-Origin' '*';
      add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
      add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
      add_header 'Access-Control-Max-Age' 1728000;
      add_header 'Content-Type' 'text/plain charset=UTF-8';
      add_header 'Content-Length' 0;
      return 204;
    }
    
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
    
    # 实际内容处理
  }
}

优点

  • 对前端透明,不需要修改前端代码
  • 支持所有浏览器
  • 可以转发任何类型的请求

缺点

  • 需要服务器端配置
  • 增加了请求的响应时间
  • 需要维护代理服务器

适用场景:企业内部系统或有控制权的多个系统之间的跨域请求。

6. window.name

原理:window.name属性在不同页面加载后依然存在,并且可以存储较大的数据(约2MB)。通过在iframe中加载跨域页面,然后将其location改为同源页面,此时依然可以访问到iframe中的window.name属性。

实现方式

// 主页面
let iframe = document.createElement('iframe');
iframe.style.display = 'none';
 
// 加载后的处理函数
let loaded = false;
iframe.onload = function() {
  if (!loaded) {
    // 第一次加载完成后,将src切换到同源页面
    loaded = true;
    iframe.contentWindow.location = 'about:blank';
  } else {
    // 第二次加载完成后,可以获取数据
    let data = iframe.contentWindow.name;
    console.log('获取的跨域数据:', data);
    iframe.parentNode.removeChild(iframe);
  }
};
 
// 设置src为跨域页面
iframe.src = 'http://other-domain.com/data.html';
document.body.appendChild(iframe);
 
// 在data.html中设置window.name
// window.name = "跨域数据";

优点

  • 可以传递较大数据
  • 兼容性好

缺点

  • 实现复杂
  • 需要创建iframe
  • 数据格式有限制

适用场景:需要传递大量数据且对性能要求不高的跨域通信。

跨域解决方案对比

方案兼容性实现难度是否需要服务端配合支持的请求类型安全性
JSONP简单仅GET
CORS中(IE10+)中等全部
document.domain简单全部(同主域)
postMessage中(IE8+)中等不涉及HTTP请求
Nginx代理复杂全部
window.name复杂不涉及HTTP请求

最佳实践

  1. 优先使用CORS:作为W3C标准,是最规范的跨域解决方案
  2. 服务器可控时使用Nginx代理:对前端透明,实现简单
  3. 简单数据获取可使用JSONP:兼容性好,实现简单
  4. iframe跨域通信使用postMessage:HTML5标准API,使用简单安全
  5. 同主域不同子域使用document.domain:简单直接

面试常见问题

  1. 什么是同源策略?为什么需要同源策略?

    • 同源策略是浏览器的安全机制,限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互
    • 防止恶意网站读取其他网站的敏感数据,保护用户隐私和数据安全
  2. CORS预检请求是什么?什么情况下会触发?

    • 预检请求是浏览器在发送实际跨域请求前,先用OPTIONS方法发送一个请求,检测实际请求是否可以安全发送
    • 非简单请求会触发预检,如使用PUT/DELETE方法,或Content-Type为application/json等
  3. 如何处理CORS中的Cookie传递问题?

    • 前端设置withCredentials: true
    • 服务器设置Access-Control-Allow-Credentials: true
    • 注意:此时Access-Control-Allow-Origin不能设为’*‘,必须是具体的域名
  4. JSONP和CORS有什么区别?

    • JSONP是一种Hack技术,利用script标签不受同源限制的特性,只支持GET请求
    • CORS是W3C标准,通过HTTP头部控制跨域访问权限,支持所有HTTP方法
  5. 如何在不支持CORS的老旧浏览器中实现跨域?

    • 使用JSONP
    • 使用iframe+window.name
    • 使用服务器代理(如Nginx反向代理)