前后端联调时最常遇到的"拦路虎",我用两招轻松化解!
当浏览器变成"门卫大爷"
最近我在前后端分离项目中踩了个大坑:前端跑在http://localhost:5173,后端服务在http://localhost:8080。当我用fetch请求后端接口时,浏览器竟然亮出红牌警告
useEffect(() => {
(async () => {
// 前后端联调
const res = await fetch('http://localhost:8080/api/hello')
const data = await res.json()
console.log(data)
})()
}, [])
经过查阅资料:原来浏览器有个同源策略的安全机制,它要求前端只能访问同协议、同域名、同端口的资源。只要这三者任一不同,就被视为"跨域"操作。就像小区门卫只认本楼住户,外来人员一律拦截!浏览器执行跨域拦截主要是为了保护用户的网络安全和隐私,这种机制被称为同源策略(Same-Origin Policy)。同源策略是一个重要的安全概念,用于限制一个源的文档或脚本如何与另一个源的资源进行交互。它有助于隔离潜在的恶意网站,防止它们读取其他网站敏感数据。
举个例子:
protocol://domain:port 协议 域名 端口号
domain 不一样 不是同一个来源
http://localhost:5173 -> http://www.baidu.com/api/user
协议不同也不同源 严格 ? 为什么 ?
http://localhost:5173 -> https://localhost:5173/api/user
端口不同也不同源 cross origin
http://localhost:5173 -> http://localhost:8080/api/user
origin = http(s) + domain + port
那么我们怎么解决跨域问题呢,首先我们想到在前端img、link、script好像都不受到同源策略的影响,都能进行跨域
img
script
这些好像都是跨域进行跨域访问的,这些资源地址适合我们前端项目也是不同源的,由此,我们想到了第一种解决方案JSONP
初探跨域:JSONP的魔法
正当我抓狂时,发现了一个神奇的现象:虽然fetch请求被拦截,但HTML中的<script>标签却能畅通无阻地加载跨域资源!这不就是突破口吗?
JSONP的降龙十八掌
-
前端请求
在页面声明回调函数,用script标签发起请求:<script> function callback(data) { console.log("收到数据:", data.msg); } </script> <!-- 把回调函数名通过参数传给后端 --> <script src="http://localhost:8080/api/hello?callback=callback"></script> -
后端响应
后端返回包裹着函数调用的JS代码:// server.js res.writeHead(200, {'Content-Type': 'text/javascript'}); const data = { msg: '字节我来了' }; res.end("callback(" + JSON.stringify(data) + ")"); -
执行效果
页面加载该脚本时自动执行:callback({msg: "字节我来了"}),完美实现跨域数据传输!
JSONP 利用了 script 可以跨域访问,前端使用
script src=url跨域的资源请求地址需要后端配合,返回的JSON 外面包含着函数,页面上有个函数在等待执行,但是有没有感觉有点麻烦,每次都要创建一个函数,我们这时候就想到了封装一个JSONP函数
封装JSONP利器
每次手动创建script标签太麻烦?看我封装的神器:
后端代码
const http = require('http');
const server = http.createServer((req, res) => {
// 匹配 GET 请求 /say
// es6 字符串方法
if (req.url.startsWith('/say')) {
// 解析查询参数(简单处理)
const url = new URL(req.url, `http://${req.headers.host}`);
console.log(url, '/////');
const wd = url.searchParams.get('wd');
const callback = url.searchParams.get('callback');
console.log(wd); // Iloveyou
console.log(callback); // show
// 返回 JSONP 格式响应
res.writeHead(200, { 'Content-Type': 'application/javascript' });
const data = {
code: 0,
msg: '我不爱你'
}
res.end(`${callback}(${JSON.stringify(data)})`);
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000');
});
前端代码
<script>
function getJSONP({ url, params = {}, callback }) {
// DOM
return new Promise((resolve, reject) => {
let script = document.createElement('script')
params = { ...params, callback }
// queryString的对象 ?callback=show&a=1&b=2
let arr = []
for (let key in params) {
arr.push(`${key}=${params[key]}`)
}
console.log(arr);
// queryString 以?开始
script.src = `${url}?${arr.join('&')}`
window[callback] = function (data) {
resolve(data)
}
document.body.appendChild(script)
})
}
// 测试案列
getJSONP({
url: 'http://localhost:3000/say',
params: { // 查询参数
wd: 'I love you'
},
callback: 'show' // 必须的
}).then(data => {
console.log(data);
})
</script>
- 动态创建
<script>标签,src指向后端接口,并带上?callback=show - 在window全局创建callback函数
- 后端接收到请求后,读取
callback参数,把数据包装成该函数的调用返回 - 浏览器加载脚本,执行
show({...}),从而拿到数据
JSONP的七寸软肋
- 只支持GET请求:无法发送POST/PUT等复杂请求
- 安全隐患:动态创建的回调函数暴露在全局,可能被恶意利用,比如xss跨站脚本攻击、CSRF跨站请求伪造
- 错误处理难:无法精确捕获网络错误
某次我将回调函数名拼写错误,调试两小时才发现——这坑踩得真疼!
所以有更方便、更安全的方法横空出世
进阶秘籍:CORS跨域神功
既然JSONP有局限,我继续探索更强大的解决方案——CORS(跨域资源共享) 。它的核心思想是:"我开放,你随意!"
简单请求
对于GET/POST等简单请求,后端只需设置一个响应头:
fetch('http://localhost:8000/api/test')
.then(res => res.json())
.then(data => {
console.log(data);
})
if (req.url === '/api/test' && req.method === 'GET') {
res.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'// 允许所有域名访问,当然也可以设置具体的白名单
})
res.end(JSON.stringify({
msg: '跨域成功!!!'
}))
}
设置后,浏览器就会放行跨域请求,就像拿到了特别通行证!
复杂请求
可以看到有两次test请求,一个是预检请求
当我尝试发送PATCH请求时,发现浏览器先发了OPTIONS请求探路。这就引出了CORS的预检机制:
-
前端发起PATCH请求
fetch('http://localhost:8000/api/test', { method: 'PATCH' }) -
浏览器自动发送OPTIONS预检
OPTIONS /api/test HTTP/1.1 Origin: http://127.0.0.1:5500 Access-Control-Request-Method: PATCH
预检请求是用来检测后端是否允许这种复杂的PATCH请求,如果不允许,则真正的PATCH的请求就不会发起
-
后端响应放行
if (req.method === 'OPTIONS') { res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500');//设置允许请求的地址 res.setHeader('Access-Control-Allow-Methods', 'PUT,PATCH,GET,POST,DELETE');//设置允许请求的方式两者必须用时满足 res.writeHead(200); res.end(); return; } -
正式请求通行
if (req.url === '/api/test' && req.method === 'PATCH') { res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500'); res.end(JSON.stringify({ msg: '跨域成功!!!' })); }
原理揭秘:浏览器相当于严谨的安检员,遇到非常规请求(如PATCH)时,会先问服务器:"这位乘客能带PATCH这个'行李'吗?" 获得许可后才放行正式请求。
血泪经验总结
| 方案 | 适用场景 | 优势 | 致命缺陷 |
|---|---|---|---|
| JSONP | 老旧浏览器兼容 | 无需后端大改 | 仅支持GET,安全性低 |
| CORS | 现代Web应用 | 支持所有HTTP方法 | 需后端配合设置响应头 |
经过反复踩坑验证,我的最佳实践是:
-
优先使用CORS:主流方案,安全灵活
-
严格设置白名单:避免使用
Access-Control-Allow-Origin: *// 只允许特定域名访问 res.setHeader('Access-Control-Allow-Origin', 'https://trusted-site.com'); -
预检请求缓存:减少OPTIONS请求次数
res.setHeader('Access-Control-Max-Age', '86400'); // 缓存24小时
终极灵魂拷问
某次面试官突然问我:"为什么CORS能解决跨域,但后端还是收到了请求?" 真相是:
- 请求能到达服务器:跨域限制是浏览器的行为
- 响应被浏览器拦截:没有正确CORS头部的响应会被丢弃
- 服务器日志有记录:即使前端报错,后端也能看到请求日志
我们的请求依然会到达服务器,但是响应结果会被服务器拦截
这就好比快递能送到小区门卫(服务器),但门卫发现收件人不在本楼(跨域),直接拒收了包裹(响应)!
总结
从被CORS拦路时的崩溃,到掌握两大解决方案的从容,我深刻体会到:
- 浏览器安全机制是双刃剑:没有同源策略,CSRF攻击将泛滥成灾
- 理解原理比死记配置更重要:明白OPTIONS预检的触发条件,才能快速定位问题
- 真实项目要综合考量:对第三方老接口可用JSONP过渡,内部系统首选CORS
最后送上我的调试秘籍:在Chrome地址栏输入chrome://flags/#block-insecure-private-network-requests并禁用该选项,可解除本地开发时的CORS限制(仅限开发环境!)。
跨域这座大山,终于被我挖通了隧道!下次遇到时,你就有两种选择方案应对