被跨域折磨到想掀桌子?浏览器的 “安全结界” 原来这么回事!

46 阅读7分钟

一到前后端联调就报 “Access to fetch at 'xxx' from origin 'xxx' has been blocked by CORS policy”?控制台红得刺眼,接口明明通的,数据却死活拿不到 —— 这就是让无数前端开发者抓狂的 “跨域问题”。

今天咱们就把跨域这层纸捅破:从浏览器为什么要搞 “同源策略”,到 JSONP 和 CORS 两种方案的底层逻辑,再到实战代码演示,保证让你看完再也不怕被跨域 “卡脖子”。

浏览器为啥要搞 “同源策略”

跨域的核心是同源策略—— 浏览器给前端套的一个 “安全结界”。先明确什么是 “同源”:

同源三要素:协议(http/https)、域名(domain)、端口(port)必须完全一致。
举几个例子:

  • http://localhost:5173 和 http://localhost:8080 → 端口不同,不同源(跨域);
  • http://www.baidu.com 和 https://www.baidu.com → 协议不同,不同源(跨域);
  • http://localhost:5173 和 http://localhost:5173/api/user → 三要素相同,同源(不跨域)。

为啥要有这破规矩?—— 为了防 “坏人”

想象一下:你刚在银行网站登录(https://bank.com),浏览器保存了你的登录 Cookie。如果没有同源策略,你再打开一个恶意网站(https://bad.com),它就能通过 JS 调用银行接口(https://bank.com/transfer),利用你保存的 Cookie 完成转账 —— 这就麻烦大了!

image.png 同源策略就是为了阻止这种 “跨站请求伪造”:

  • 限制不同源的 JS 不能随便读取对方的 Cookie、LocalStorage;
  • 限制不同源的 AJAX/fetch 请求不能随便获取响应数据;
  • 但允许imgscriptlink等标签加载跨域资源(比如 CDN 的图片、JS)。

跨域时到底发生了什么?—— 最容易误解的点

当你用fetchaxios调用跨域接口时,控制台报错 “CORS blocked”,你可能会以为 “请求没发出去”—— 大错特错!

真相
请求成功到达了服务器,服务器也成功返回了响应,但浏览器在拿到响应后,发现 “对方域名不在同源白名单里”,就把响应给拦截抛弃了。

举个例子:
前端(http://localhost:5173)调用后端(http://localhost:8080/api/test):

  1. 前端发送请求 → 后端收到并处理 → 后端返回数据;
  2. 浏览器检查响应头:“有没有Access-Control-Allow-Origin: http://localhost:5173?”
  3. 如果没有 → 浏览器拦截响应,控制台报错 “CORS policy blocked”;
  4. 如果有 → 浏览器放行,前端正常拿到数据。

所以跨域问题的本质是:浏览器对响应的 “秋后算账” ,而非请求发不出去。

解决跨域的两种核心方案:JSONP 和 CORS

既然浏览器拦的是 “跨域的 AJAX 响应”,那解决思路要么 “绕开 AJAX”,要么 “让浏览器认可这个跨域响应”。

方案一:JSONP —— 钻 script 标签的 “空子”

浏览器虽然拦 AJAX,但对script标签加载跨域资源睁一只眼闭一只眼(比如加载 CDN 的 JS)。JSONP 就是利用这个 “漏洞” 实现跨域。

原理:让后端返回 “函数调用”

  1. 前端提前定义一个全局函数(比如handleData),用来处理数据;
  2. 动态创建script标签,src指向跨域接口,并在 URL 里带上函数名(比如?callback=handleData);
  3. 后端收到请求后,返回 “handleData(实际数据)” 这样的字符串;
  4. script标签加载后,会执行这段 JS,相当于调用handleData并传入数据。

实战代码:手写一个 JSONP

前端代码http://localhost:5173):

<script>
// 1. 定义全局处理函数
function show(data) {
  console.log('拿到跨域数据:', data);
}

// 2. 动态创建script标签
function getJSONP(url, params) {
  const script = document.createElement('script');
  // 拼接参数,带上callback函数名
  const query = new URLSearchParams({ ...params, callback: 'show' }).toString();
  script.src = `${url}?${query}`;
  document.body.appendChild(script);
}

// 3. 调用跨域接口
getJSONP('http://localhost:3000/say', { wd: 'I love you' });
</script>

后端代码http://localhost:3000,Node.js):

const http = require('http');
const server = http.createServer((req, res) => {
  if (req.url.startsWith('/say')) {
    // 解析URL中的callback参数
    const url = new URL(req.url, 'http://localhost:3000');
    const callback = url.searchParams.get('callback'); // 拿到"show"
    const data = { code: 0, msg: '我不爱你' }; // 要返回的数据
    
    // 关键:返回"show(数据)"格式的JS代码
    res.writeHead(200, { 'Content-Type': 'text/javascript' });
    res.end(`${callback}(${JSON.stringify(data)})`); // 输出:show({"code":0,"msg":"我不爱你"})
  }
});
server.listen(3000);

image.png

JSONP 的优缺点:

  • 优点:兼容性好(支持老浏览器),实现简单;
  • 缺点:只能发 GET 请求(script标签只能 GET),需要后端配合,有安全风险(后端可能注入恶意代码)。

方案二:CORS —— 后端 “盖章放行”(推荐)

CORS(Cross-Origin Resource Sharing)是 W3C 标准,本质是让后端在响应头里 “盖章”,告诉浏览器 “这个跨域请求我认可,你别拦”。

两种 CORS 请求类型:

1. 简单请求 —— 直接放行

满足以下条件的请求属于 “简单请求”,浏览器不拦截:

  • 方法是GETPOSTHEAD
  • 请求头只有AcceptAccept-LanguageContent-Type等简单字段;
  • Content-Type只能是text/plainmultipart/form-dataapplication/x-www-form-urlencoded

后端只需设置一个响应头

// 允许http://localhost:5173的跨域请求
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
// 或者允许所有域(生产环境不推荐)
res.setHeader('Access-Control-Allow-Origin', '*');
2. 复杂请求 —— 先 “预检” 再放行

如果请求不满足 “简单请求” 条件(比如用PUTDELETE方法,或Content-Type: application/json),浏览器会先发送一个 “预检请求”(OPTIONS方法),确认后端允许后再发真实请求。

预检请求的流程

  1. 前端发送OPTIONS请求,问后端:“我要用PUT方法,带Content-Type: application/json,你允许吗?”;
  2. 后端返回响应,告诉浏览器:“允许PUT,允许Content-Type,有效期 300 秒”;
  3. 浏览器收到后,若确认允许,再发送真实的PUT请求;
  4. 后端处理真实请求,返回数据(同样需要带Access-Control-Allow-Origin)。

实战代码(复杂请求示例):

前端代码(发PATCH请求,Content-Type: application/json):

fetch('http://localhost:8000/api/test', {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: '前端' })
}).then(res => res.json()).then(data => console.log(data));

后端代码(Node.js):

const http = require('http');
const server = http.createServer((req, res) => {
  // 1. 处理预检请求(OPTIONS方法)
  if (req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
    // 允许的方法(必须包含真实请求的方法)
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH,OPTIONS');
    // 允许的请求头(必须包含真实请求的Content-Type)
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
    // 预检结果有效期(300秒内不用再发预检)
    res.setHeader('Access-Control-Max-Age', '300');
    res.writeHead(200);
    res.end();
    return;
  }

  // 2. 处理真实请求(PATCH方法)
  if (req.url === '/api/test' && req.method === 'PATCH') {
    res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify({ msg: '跨域成功!' }));
  }
});
server.listen(8000);

CORS 的优缺点:

  • 优点:支持所有 HTTP 方法,安全(后端精确控制允许的域),无需前端额外处理;
  • 缺点:兼容性依赖浏览器(IE10 + 支持),复杂请求需要处理预检,后端配置稍复杂。

总结:什么时候用哪种方案?

场景推荐方案理由
现代浏览器 + 后端可控CORS标准方案,支持所有请求类型,安全可靠
需要兼容老浏览器(如 IE8-)JSONP利用 script 标签兼容性,老浏览器也支持
只需要 GET 请求JSONP 或 CORS简单场景两者都行,优先 CORS
后端不配合改代码考虑代理(如 webpack-dev-server 代理)前端本地转发请求,规避跨域

记住一句话:跨域问题的本质是 “浏览器的安全限制”,解决思路要么 “让浏览器认可(CORS)”,要么 “绕开浏览器的限制(JSONP)”。

下次再遇到跨域报错,先看请求方法和请求头,判断是简单还是复杂请求,然后让后端按规则配置响应头 —— 搞定!再也不用对着控制台的红报错发呆了~ 🚀