一到前后端联调就报 “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 完成转账 —— 这就麻烦大了!
同源策略就是为了阻止这种 “跨站请求伪造”:
- 限制不同源的 JS 不能随便读取对方的 Cookie、LocalStorage;
- 限制不同源的 AJAX/fetch 请求不能随便获取响应数据;
- 但允许
img
、script
、link
等标签加载跨域资源(比如 CDN 的图片、JS)。
跨域时到底发生了什么?—— 最容易误解的点
当你用fetch
或axios
调用跨域接口时,控制台报错 “CORS blocked”,你可能会以为 “请求没发出去”—— 大错特错!
真相:
请求成功到达了服务器,服务器也成功返回了响应,但浏览器在拿到响应后,发现 “对方域名不在同源白名单里”,就把响应给拦截抛弃了。
举个例子:
前端(http://localhost:5173
)调用后端(http://localhost:8080/api/test
):
- 前端发送请求 → 后端收到并处理 → 后端返回数据;
- 浏览器检查响应头:“有没有
Access-Control-Allow-Origin: http://localhost:5173
?” - 如果没有 → 浏览器拦截响应,控制台报错 “CORS policy blocked”;
- 如果有 → 浏览器放行,前端正常拿到数据。
所以跨域问题的本质是:浏览器对响应的 “秋后算账” ,而非请求发不出去。
解决跨域的两种核心方案:JSONP 和 CORS
既然浏览器拦的是 “跨域的 AJAX 响应”,那解决思路要么 “绕开 AJAX”,要么 “让浏览器认可这个跨域响应”。
方案一:JSONP —— 钻 script 标签的 “空子”
浏览器虽然拦 AJAX,但对script
标签加载跨域资源睁一只眼闭一只眼(比如加载 CDN 的 JS)。JSONP 就是利用这个 “漏洞” 实现跨域。
原理:让后端返回 “函数调用”
- 前端提前定义一个全局函数(比如
handleData
),用来处理数据; - 动态创建
script
标签,src
指向跨域接口,并在 URL 里带上函数名(比如?callback=handleData
); - 后端收到请求后,返回 “
handleData(实际数据)
” 这样的字符串; 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);
JSONP 的优缺点:
- 优点:兼容性好(支持老浏览器),实现简单;
- 缺点:只能发 GET 请求(
script
标签只能 GET),需要后端配合,有安全风险(后端可能注入恶意代码)。
方案二:CORS —— 后端 “盖章放行”(推荐)
CORS(Cross-Origin Resource Sharing)是 W3C 标准,本质是让后端在响应头里 “盖章”,告诉浏览器 “这个跨域请求我认可,你别拦”。
两种 CORS 请求类型:
1. 简单请求 —— 直接放行
满足以下条件的请求属于 “简单请求”,浏览器不拦截:
- 方法是
GET
、POST
、HEAD
; - 请求头只有
Accept
、Accept-Language
、Content-Type
等简单字段; Content-Type
只能是text/plain
、multipart/form-data
、application/x-www-form-urlencoded
。
后端只需设置一个响应头:
// 允许http://localhost:5173的跨域请求
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
// 或者允许所有域(生产环境不推荐)
res.setHeader('Access-Control-Allow-Origin', '*');
2. 复杂请求 —— 先 “预检” 再放行
如果请求不满足 “简单请求” 条件(比如用PUT
、DELETE
方法,或Content-Type: application/json
),浏览器会先发送一个 “预检请求”(OPTIONS
方法),确认后端允许后再发真实请求。
预检请求的流程:
- 前端发送
OPTIONS
请求,问后端:“我要用PUT
方法,带Content-Type: application/json
,你允许吗?”; - 后端返回响应,告诉浏览器:“允许
PUT
,允许Content-Type
,有效期 300 秒”; - 浏览器收到后,若确认允许,再发送真实的
PUT
请求; - 后端处理真实请求,返回数据(同样需要带
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)”。
下次再遇到跨域报错,先看请求方法和请求头,判断是简单还是复杂请求,然后让后端按规则配置响应头 —— 搞定!再也不用对着控制台的红报错发呆了~ 🚀