刚开始我也以为跨域就是个配置项的事儿,结果连踩三个坑,差点把产品经理送走。今天我把这些坑一个一个挖出来,顺便告诉你该怎么填。
第一个坑:JSONP 不能发 POST
先说最老的 JSONP。这东西原理很简单:<script> 标签的 src 属性加载外部脚本时不受同源策略限制,所以可以加载别的域上的脚本。但问题来了——JSONP 只能用 GET,不能用 POST。
有个朋友(不是我,真的)曾经在对接第三方支付接口时,后端说“啊我们用 JSONP”,前端兴冲冲写了个 POST,结果返回 405。调了一天,最后发现 JSONP 就只支持 GET。这个设计真的反人类。
“javascript${url}${separator}callback=${callbackName}
// JSONP 的正确用法 —— 只能 GET
function jsonpRequest(url, callbackName) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
// 后端返回的是 callbackName({ data: ... })
// 处理 URL 中已有查询参数的情况
const separator = url.includes('?') ? '&' : '?';
script.src = ;`
window[callbackName] = (data) => {
resolve(data);
document.body.removeChild(script);
delete window[callbackName];
};
script.onerror = reject;
document.body.appendChild(script);
> });
}
// 调用
jsonpRequest('https://api.example.com/user', 'handleData')
.then(data => console.log(data));
为什么要这么写:JSONP 本质上是制造一个临时全局函数,后端调用它。所以 URL 里必须传一个回调函数名。你写 POST?浏览器直接报跨域错误,因为 <script> 的 src 只发 GET。
踩坑总结:JSONP 可以处理 GET 跨域请求,但千万别用它搞 POST。如果你的后端只能用 JSONP 且需要 POST,基本可以放弃了——让后端换方案。
核心方案:CORS
CORS 是现在最主流的跨域方案,没有之一。原理简单地说,就是后端在响应头里加个 Access-Control-Allow-Origin,告诉浏览器“这个源是允许的”。
但官方文档那段文档不够清晰,什么预检、简单请求、非简单请求,一看就晕。我来用人话解释:
- 当你发 GET、POST(只能 Content-Type: application/x-www-form-urlencoded
、multipart/form-data或text/plain)、HEAD 请求,并且没有自定义头时,不会预检。这叫简单请求。 - 其他情况(比如 Content-Type: application/json
、自定义头、PUT 方法),浏览器会先发一个OPTIONS请求,这叫预检。预检通过后才发真实请求。
*图1:CORS 预检流程示意图,浏览器先发 OPTIONS 问服务器“我能发这个请求吗”,服务器说“行”后才发真实请求。*
`javascript
// 前端代码 —— 真正需要关注的几个场景
// 场景1:简单请求 —— 直接发
fetch('https://api.example.com/data')
.then(res => res.json());
// 场景2:JSON 请求 —— 触发预检
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // 这一行就是预检的导火索
'Authorization': 'Bearer token123' // 自定义头也会触发预检
},
body: JSON.stringify({ name: '张三' })
});
// 场景3:携带 cookie —— 后端必须允许credentials
fetch('https://api.example.com/data', {
credentials: 'include' // 让浏览器带上 cookie
});
`
为什么要这样:Content-Type: application/json 不是简单请求的 MIME 类型,所以会预检。而 Authentication 头是自定义的,也必须预检。预检其实就是个保护机制,防止恶意脚本随便往你的服务器发敏感数据。
后端配置(以 Node.js Express 为例):
`javascript
const express = require('express');
const app = express();
app.use((req, res, next) => {
// 允许的源 —— 不能是 *
res.setHeader('Access-Control-Allow-Origin', 'https://your-frontend.com');
// 如果携带 cookie,必须指定具体的 Origin,不能用 *,且必须同时设置下面这个头
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 预检请求返回哪些方法
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
// 预检请求返回允许的请求头
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 预检结果缓存 86400 秒(1天),减少预检次数
res.setHeader('Access-Control-Max-Age', '86400');
// 如果是 OPTIONS 请求(预检),直接返回 204
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
`
有个坑:生产环境千万别写 Access-Control-Allow-Origin: * 再加 credentials: true,这会导致浏览器报错。必须指定具体域名。
另一个坑:Dev 环境的代理
刚入门前端时,我在本地开发环境配 CORS 配置费了好大劲。后来发现,开发环境根本不需要搞 CORS —— 用 Webpack/Vite 的代理就行了。
为什么要用代理:因为 CORS 是浏览器端的限制,服务器之间没有跨域问题。你本地开发时,让 Webpack Dev Server 做一个反向代理,把请求转发到后端,浏览器看到的全是同域请求。
Vite 配置代理:
`javascript`
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true, // 这个必须开,不然请求头里的 Host 还是 localhost
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
};
// 前端调用 —— 直接写 /api,不要写全域名
fetch('/api/user') // 自动转到 https://api.example.com/user
Webpack 配置代理:
`javascript`
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
};
这个真的反人类: 我在某个项目里发现,Vite 代理有时会丢失 cookie。原因是 changeOrigin: true 改变了 Host 头,后端检查 Host 不对就拒绝。解决方法是改后端逻辑,或者用 cookieDomainRewrite 选项。
`javascript`
// 解决 cookie 丢失问题
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
cookieDomainRewrite: 'localhost' // 重写 cookie 的 domain
}
}
*图2:本地开发时,代理把 /api/user 的请求转发到后端服务器,浏览器看到的是 localhost 的响应,不会触发跨域。*
还有个技巧:Nginx 反向代理
生产环境怎么办?要么后端配 CORS,要么用 Nginx 做反向代理。我更喜欢后者,因为后端不用改代码,所有跨域都在网关层解决。
为什么要用 Nginx:如果你有多个前端应用(比如 admin.example.com 和 app.example.com),每个都要访问 api.example.com,后端不可能写死 Origin 为两个域名。Nginx 可以统一处理。
`nginx
nginx.conf 片段
先定义允许的 Origin 白名单
map $http_origin $allowed_origin {
default "";
"https://admin.example.com" "https://admin.example.com";
"https://app.example.com" "https://app.example.com";
}
server {
listen 80;
server_name app.example.com;
location /api/ {
proxy_pass https://api.example.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 关键:处理跨域响应头,使用白名单变量,避免安全风险
add_header Access-Control-Allow-Origin $allowed_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header Access-Control-Allow-Headers 'Content-Type, Authorization' always;
# 处理预检请求
if ($request_method = 'OPTIONS') {
return 204;
}
}
}
`
安全警告: 生产环境中千万别直接用 $http_origin 作为 Access-Control-Allow-Origin` 的值,那样等于允许任何网站跨域访问你的 API,非常危险。一定要用白名单机制限制允许的来源。
本文仅供参考,不构成医疗建议。
本文由AI辅助创作,仅供参考。