跨域有哪些解决方案?JSONP 和代理是怎样工作的?
首选服务端配置 CORS;开发环境用开发服务器代理,生产环境用 Nginx 这类反向代理,让浏览器只请求同源地址;JSONP 利用 script 标签不受同源策略限制来拿数据,只支持 GET,而且等于执行对方返回的脚本,现在基本不用;跨窗口通信用 postMessage。
简答
页面在
https://shop.example.com,接口在https://api.example.com,两者域名不同,浏览器的同源策略默认不让页面脚本读取这个接口的响应,这就是跨域问题。解决办法可以按“谁来放行”分成几类。最标准的做法是 CORS:服务器在响应里加上
Access-Control-Allow-Origin等响应头,告诉浏览器“这个来源可以读我的响应”。浏览器看到允许就把响应交给脚本。复杂请求还会先发一个OPTIONS预检。它要后端配合,但最规范。第二类是代理。同源策略只管浏览器,服务器之间互相请求不受限制。所以让浏览器请求同源的地址,比如
/api/user,再由同源的服务器把请求转发给真正的接口。开发时用 Vite、webpack dev server 的proxy配置,上线后用 Nginx 反向代理,或者把页面和接口部署在同一个域名下。第三类是老办法 JSONP。
<script>标签加载别的域名的脚本不受同源策略限制,于是让接口返回一段调用函数的脚本,比如handleData({...}),页面事先定义好handleData,脚本一执行,数据就作为参数传进来了。要注意,它只能发 GET,出错很难捕获,最大的问题是等于无条件执行对方返回的代码,接口被篡改就能在你的页面上为所欲为,现在应该用 CORS 代替。
代理为什么能解决跨域
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
};
页面里请求 /api/user,浏览器发给的是开发服务器自己,和页面同源,不存在跨域。Vite 文档的说法是,路径以 /api 开头的请求,会被代理转发到配置的 target。开发服务器收到后,去掉 /api 前缀,向 https://api.example.com/user 发请求,再把结果原样返回给浏览器。
changeOrigin: true 有什么用?它会把转发请求的 Host 头改成目标服务器的地址。有些服务器按 Host 区分站点,不改的话会收到 localhost,可能返回 404 或拒绝请求。
要注意,Vite 的 server.proxy 只在开发服务器上生效,打包出来的静态文件里没有这个代理。上线后要由 Nginx 或网关做同样的转发,不然请求 /api 会直接 404。
JSONP 是怎样拿到数据的
function jsonp(url, callbackName) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
window[callbackName] = (data) => {
resolve(data);
script.remove();
delete window[callbackName];
};
script.onerror = () => reject(new Error('JSONP 请求失败'));
script.src = `${url}?callback=${callbackName}`;
document.head.append(script);
});
}
页面先在全局定义一个函数,再插入一个 <script>,把函数名通过 callback 参数告诉服务器。服务器返回的不是 JSON,而是一行脚本 handleData({"name":"小明"})。浏览器加载并执行这段脚本,就调用了页面事先定义的函数,数据作为参数传了进来。
为什么只能发 GET?因为 <script> 加载资源只会发 GET 请求,也没法设置请求头。为什么不安全?返回的内容是直接执行的脚本,接口要是返回了别的代码,页面也会照样执行。开启了 CSP 的页面,还要把接口域名加进 script-src 才能加载。
常见追问
CORS 报错了,请求到底发出去没有?
简单请求已经发出去了,服务器也可能已经执行了,只是浏览器不让脚本读响应。需要预检的请求,预检没通过时真正的请求不会发出。具体规则见《什么是跨域?CORS 为什么需要服务端配合?》。
页面和 iframe、新窗口之间跨域怎么通信?
用
postMessage。发送时指定目标来源,接收时检查event.origin,见《跨窗口 postMessage 怎样避免把消息发给错误对象?》。document.domain以前也能让同一主域下的页面互相访问,现在浏览器正在逐步禁用它,不要再用。