一、跨域问题原因
跨域问题源于浏览器的同源策略。同源策略是一种安全策略,它限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。所谓同源,是指域名、协议和端口号都相同。
例如,当一个网页的脚本试图访问另一个域名下的资源(如通过 XMLHttpRequest
或 fetch
请求其他域名的接口)、不同协议(如 http
请求访问 https
资源)或者不同端口(如在 localhost:8080
下请求 localhost:3000
的资源)时,浏览器就会阻止这种跨域请求,以防止恶意网站窃取用户数据等安全问题。
二、跨域问题场景
前后端分离场景
当前端项目部署在一个域名(例如 www.frontend.com
)下,后端接口部署在另一个域名(如 api.backend.com
)下,前端通过 JavaScript 脚本请求后端接口时就会出现跨域问题。
不同子域名下资源访问场景
比如企业内部有 sub1.example.com
和 sub2.example.com
两个子域名,分别对应不同的应用,当 sub1.example.com
下的脚本想访问 sub2.example.com
下的资源时,也会遇到跨域限制。
三、跨域问题解决方案
JSONP(JSON with Padding)
原理
它利用了 <script>
标签没有域跨限制的特性。其基本原理是动态创建一个 <script>
标签,然后将这个标签的 src
属性设置为要请求的跨域 URL,同时在 URL 中包含一个回调函数名的参数。服务器收到请求后,会将数据作为回调函数的参数返回,这样就可以在客户端执行这个回调函数,从而实现跨域数据获取。
场景
主要适用于跨域获取数据,并且只支持 GET
请求的场景,比如一些 API 接口只允许 GET
请求获取数据,并且可以接受 JSONP 格式的请求。
示例
前端代码:
ini
function handleData(data) {
console.log(data);
}
var script = document.createElement('script');
script.src = 'http://api.example.com/data?callback=handleData';
document.body.appendChild(script);
服务器端返回:
erlang
handleData({"name": "John", "age": 30});
优点
实现相对简单,不需要服务器进行复杂的配置,只要服务器能够支持 JSONP 请求格式即可。
缺点
只支持 GET
请求,不能处理其他类型的 HTTP 请求方法,如 POST
、PUT
等。并且对数据格式有一定的要求,需要服务器返回特定格式的 JSONP 响应。
CORS(Cross - Origin Resource Sharing)
原理
它是目前主流的解决跨域问题的方法。它通过服务器返回特定的 HTTP 头来告诉浏览器,允许某个外部域名的脚本访问资源。浏览器在发起跨域请求时,会先检查服务器返回的 CORS 相关头信息,如 Access - Control - Allow - Origin
(指定允许哪些源访问资源)、Access - Control - Allow - Methods
(指定允许的 HTTP 请求方法)等,如果符合要求,就会允许请求。
场景
适用于各种跨域场景,包括前后端分离、不同子域名下资源访问等,几乎可以满足所有正常的跨域请求需求。
示例
服务器返回的响应头:
yaml
Access - Control - Allow - Origin: http://localhost:3000
Access - Control - Allow - Methods: GET, POST, PUT, DELETE
Access - Control - Allow - Headers: Content - Type
前端使用 fetch
发起请求:
javascript
fetch('http://api.example.com/data', {
method: 'GET',
headers: {
'Content - Type': 'application/json'
}
})
.then(response => {
return response.json();
})
.then(data => {
console.log(data);
});
优点
功能强大,可以灵活地指定允许的源、方法、请求头等,而且可以支持各种类型的 HTTP 请求方法。它是一种标准化的跨域解决方案,得到了广泛的支持。
缺点
需要服务器端进行配置,对于一些没有权限修改服务器配置的情况可能无法使用。而且,在开发过程中,可能会因为一些复杂的 CORS 配置问题导致请求失败,需要仔细排查服务器返回的头信息。
代理服务器
原理
通过在同源服务器(通常是后端服务器)上设置一个代理,前端请求先发送到这个代理服务器,代理服务器再将请求转发到目标跨域服务器,并将返回的响应转发给前端。因为对于前端来说,请求是发送到了同源的代理服务器,所以不存在跨域问题。
场景
适用于比较复杂的跨域场景,尤其是在企业内部,当后端可以方便地搭建代理服务器时。或者在开发环境中,用于解决本地开发环境与后端服务器跨域的问题。
示例
假设前端在 http://localhost:8080
,后端接口在 http://api.example.com
,在 http://localhost:8080
上搭建一个代理服务器。
使用 Node.js 的 http - proxy - middleware
搭建代理:
php
const { createProxyMiddleware } = require('http - proxy - middleware');
module.exports = function (app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
})
);
};
前端请求:
ini
fetch('/api/data')
.then(response => {
return response.json();
})
.then(data => {
console.log(data);
});
优点
可以完全避免跨域问题,因为请求看起来都是同源的。并且可以对请求进行一些额外的处理,如添加请求头、修改响应内容等。
缺点
增加了系统的复杂性,需要搭建和维护代理服务器。而且可能会带来性能开销,因为增加了请求的中间转发环节。
document.domain 方式(适用于不同子域名下的跨域通信)
原理
在浏览器中,可以通过设置 document.domain
属性来改变文档的域。一般来说,这个属性默认是完整的域名,但如果两个页面属于同一个父域名,例如 sub1.example.com
和 sub2.example.com
,我们可以通过设置 document.domain = 'example.com'
来使它们看起来属于同一个域,从而实现一定程度的跨域通信。
场景
主要适用于不同子域名下的页面之间的通信,比如在一个企业网站的不同子域名下的页面之间共享一些简单的数据。
示例
在 sub1.example.com
页面中:
ini
document.domain = 'example.com';
window.parent.postMessage('Hello from sub1', 'http://sub2.example.com');
在 sub2.example.com
页面中:
javascript
document.domain = 'example.com';
window.addEventListener('message', function (event) {
// 只接受来自 example.com 域的消息
if (event.origin !== 'http://sub1.example.com') return;
console.log(event.data);
}, false);
优点
实现相对简单,对于子域名之间的通信比较有效,并且可以直接操作 DOM 等元素。
缺点
只能用于同父域名下不同子域名的通信,并且只能对 document.domain
进行设置,设置后的域必须是父域名,而且会带来一定的安全风险,因为两个子域名下的页面可以互相访问。
四、跨域解决方案总结
解决方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
JSONP | 跨域获取数据,仅支持 GET 请求 | 实现简单,无需复杂服务器配置 | 仅支持 GET 请求,要求服务器返回特定格式的 JSONP 响应 |
CORS | 各种跨域场景 | 功能强大,支持多种 HTTP 方法,标准化解决方案 | 需要服务器端配置,配置错误可能导致请求失败 |
代理服务器 | 复杂跨域场景,企业内部或开发环境 | 完全避免跨域问题,可额外处理请求和响应 | 增加系统复杂性和性能开销,需搭建和维护代理服务器 |
document.domain | 同父域名下不同子域名通信 | 实现简单,可直接操作 DOM | 仅适用于同父域名下不同子域名,存在安全风险 |