很多人第一次学同源策略会困惑:不是说跨域不行吗?那我为啥能从 CDN 引一个 https://cdn.xxx.com/react.min.js,也能加载跨域图片、样式?

这里的关键点是:同源策略限制的是“读/拿到数据”,不是限制你“发起请求/加载资源”。很多标签天生就允许跨域加载资源,但你想在 JS 里去读它、操作它、拿到响应体,就会触发同源策略和 CORS 这一套限制。

什么是同源策略?

**同源策略(Same-Origin Policy)**是浏览器的一项重要安全机制,用于限制来自不同源的文档或脚本之间的交互。具体而言,只有当两个URL的协议、域名和端口号都相同时,才被视为同源。此策略旨在防止恶意网站读取用户敏感数据或执行未经授权的操作。

例如,假设有两个URL:

  • http://example.com/page1.html
  • http://example.com/page2.html

这两个URL被视为同源,因为它们的协议(http)、域名(example.com)和端口号(默认80)都相同。

然而,以下情况则被视为跨域:

  • 不同协议:http://example.comhttps://example.com
  • 不同域名:http://example.comhttp://sub.example.com
  • 不同端口:http://example.com:80http://example.com:8080

什么是跨域?

跨域(Cross-Origin) 是指在浏览器中,当前网页的源与所请求资源的源不一致的情况。由于同源策略的限制,跨域请求可能会被阻止,导致资源无法加载或访问。

常见的跨域场景包括:

  • 使用AJAX从一个域名请求另一个域名的数据
  • 在网页中嵌入来自不同域的图片、脚本或样式表

为什么CDN请求资源时不会有跨域限制?

尽管浏览器实施同源策略,但在以下情况下,CDN资源的请求通常不会遇到跨域限制:

  1. 静态资源的加载不受同源策略限制:浏览器允许通过<script><link><img>等标签加载跨域的静态资源,如JavaScript文件、CSS样式和图片等。这是因为这些资源的加载被视为安全操作,不涉及敏感数据的读取或修改。
  2. CDN配置了CORS策略:CDN服务器通常会配置跨域资源共享(CORS)策略,在HTTP响应头中添加Access-Control-Allow-Origin字段,明确允许哪些源可以访问资源。例如,设置为*表示允许所有源访问。
  3. CDN资源通常是公开的:CDN的设计目的是为了加速内容分发,资源通常是公开的,默认允许跨域访问。

解决跨域问题的常见方法

在实际开发中,遇到跨域问题时,常用的解决方案有以下几种:

1. JSONP(JSON with Padding)

JSONP是一种利用<script>标签不受同源策略限制的特性,来实现跨域请求的技术。它通过在请求中指定一个回调函数,服务器将数据包装在该回调函数中返回,从而实现跨域数据传输。

优点:

  • 兼容性强,适用于所有浏览器,尤其是IE10及以下版本。

缺点:

  • 只支持GET请求,无法处理POST请求。
  • 存在安全隐患,容易被利用进行XSS攻击。

示例:

客户端请求:

1
2
3
4
5
6
<script>
function handleResponse(data) {
console.log(data);
}
</script>
<script src="http://example.com/data?callback=handleResponse"></script>

服务器响应:

1
handleResponse({"name": "John", "age": 30});

2. CORS(跨域资源共享)

CORS是一种W3C标准,允许服务器在响应头中添加特定的HTTP头,指示浏览器允许来自其他域的请求。

优点:

  • 支持各种HTTP请求方法,包括GET、POST、PUT、DELETE等。
  • 安全性高,服务器可控性强。

缺点:

  • 需要服务器配置支持。

示例:

服务器配置(以Node.js为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require('express');
const app = express();

app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type');
next();
});

app.get('/data', (req, res) => {
res.json({ name: 'John', age: 30 });
});

app.listen(3000, () => {
console.log('Server running on port 3000');
});

3. 服务器代理

通过在同源服务器上设置代理,将跨域请求转发到目标服务器,从而避免浏览器的跨域限制。

优点:

  • 实现简单,前端无需修改代码。

缺点:

  • 增加了服务器的负担。

示例:

使用Node.js设置代理:

1
2
3
4
5
6
7
8
9
10
11
12
const express = require('express');
const request = require('request');
const app = express();

app.use('/proxy', (req, res) => {
const url = 'http://example.com' + req.url;
req.pipe(request(url)).pipe(res);
});

app.listen(3000, () => {
console.log('Server running on port 3000');
});

4. WebSocket

WebSocket是一种在单个TCP连接上进行全双工通信的协议,不受同源策略限制。

优点:

  • 适用于需要实时通信的场景。

缺点:

  • 需要服务器和客户端都支持WebSocket协议。

示例:

客户端:

1
2
3
4
const socket = new WebSocket('ws://example.com/socket');
socket.onmessage = (event) => {
console.log('Message from server ', event.data);
};

服务器(Node.js):

1
2
3
4
5
6
7
8
9
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
ws.on('message', (message) => {
console.log('received: %s', message);
});
ws.send('something');
});

5. postMessage

postMessage是HTML5引入的API,用于在不同源的窗口之间安全地传递消息。

优点:

  • 适用于iframe与父页面之间的通信。

缺点:

  • 仅限于窗口或iframe之间的通信,适用范围有限。

示例:

父页面:

1
2
const iframe = document.getElementById('myIframe');
iframe.contentWindow.postMessage('Hello from parent', 'http://example.com');

子页面:

1
2
3
4
window.addEventListener('message', (event) => {
if (event.origin !== 'http://parent.com') return;
console.log('Message from parent:', event.data);
});

总结

简单记住一句话:跨域资源能不能“加载”是一回事,能不能“读”是另一回事。JS/CSS/img 之所以能从 CDN 加载,是因为浏览器允许这些标签跨域发请求;但你想用 XHR/fetch 读跨域响应体、或者想把跨域图片画到 canvas 再导出,就得走 CORS 这一套规则。

理解了这个边界,很多“为什么我能引 CDN 但接口跨域报错”的问题就不神秘了。

Happy Coding!