浏览器的同源策略(Same-Origin Policy) 是 Web 安全的核心基石,其本质是限制不同源的页面对当前页面资源的 "非法访问",避免恶意网站窃取数据、篡改 DOM 或发起未授权请求。理解同源策略及跨域解决方案,是前端开发中处理多服务协作的关键。
一、什么是同源策略?
"同源" 指的是两个页面的协议(Protocol)、域名(Domain)、端口(Port) 三者完全一致。只要其中任意一项不同,就属于 "不同源",会被浏览器的同源策略拦截。
1. 同源判定规则
判断两个 URL 是否同源,需严格比对以下三点:
对比维度 | 示例 1(当前页面) | 示例 2(目标资源) | 是否同源 | 原因 |
---|---|---|---|---|
协议 + 域名 + 端口 | http://www.example.com:80 |
http://www.example.com:80/index.html |
是 | 三者完全一致 |
协议不同 | http://www.example.com |
https://www.example.com |
否 | 协议从 HTTP 变为 HTTPS |
域名不同 | http://www.example.com |
http://blog.example.com |
否 | 子域名不同(www vs blog) |
端口不同 | http://www.example.com:80 |
http://www.example.com:8080 |
否 | 端口从 80 变为 8080 |
2. 同源策略的限制范围
同源策略并非 "一刀切",而是对不同类型的资源访问做了差异化限制,核心限制包括:
-
1. 脚本访问限制(最核心) 不同源的 JavaScript 脚本,无法直接操作当前页面的 DOM(如修改
document
内容)、读取 Cookie/LocalStorage/SessionStorage,也无法通过XMLHttpRequest
或Fetch API
发起请求(会触发跨域拦截)。例:http://a.com
的脚本无法读取http://b.com
的 LocalStorage,也无法直接请求http://b.com/api/data
。 -
2. 资源加载限制(部分允许) 部分 "被动加载" 的资源不受同源限制,例如:
- 图片(
<img src="不同源URL">
):可加载但无法读取像素数据(避免窃取图片内容); - 样式表(
<link href="不同源URL">
):可加载但无法通过getComputedStyle
读取样式(避免信息泄露); - 脚本(
<script src="不同源URL">
):可加载执行(即 "JSONP" 的原理),但无法访问脚本的内部变量。
- 图片(
-
3. Cookie 访问限制 Cookie 的 "源" 判定依赖域名 (不包含端口和协议),不同源的页面无法读取对方的 Cookie;但如果 Cookie 设置了
Domain
属性(如Domain=example.com
),则www.example.com
和blog.example.com
等子域名可共享该 Cookie(仍受同源策略的 "域名匹配" 约束)。
二、为什么需要跨域?
同源策略保障了安全,但随着 Web 应用的架构演进(如前后端分离、微服务),"跨域访问" 成为刚需:
- 前后端分离:前端部署在
http://frontend.com
,后端 API 部署在http://backend.com
,前端需跨域请求后端数据; - 微服务协作:A 服务(
http://service-a.com
)需调用 B 服务(http://service-b.com
)的接口获取数据; - 第三方资源引用:如引用
https://cdn.example.com
的图片、脚本,或调用微信、支付宝的开放 API。
此时,需要在 "安全" 和 "可用性" 之间找到平衡,通过技术方案实现合法的跨域访问。
三、常见的跨域解决方案
跨域解决方案的核心思路是:在浏览器的同源策略规则内,通过 "协议约定" 或 "中间层转发",让不同源的资源访问被浏览器认可。以下是前端和后端常用的 4 种方案,按 "通用性" 和 "安全性" 排序:
1. CORS(Cross-Origin Resource Sharing):最推荐的方案
CORS 是 W3C 标准定义的跨域解决方案,本质是后端通过 HTTP 响应头,明确告知浏览器 "允许哪个源的请求访问" ,浏览器验证通过后放行请求。CORS 分为 "简单请求" 和 "预检请求(Preflight)" 两类,覆盖绝大多数跨域场景。
(1)简单请求(无需预检)
满足以下条件的请求,浏览器直接发送,无需额外验证:
- 请求方法为:
GET
、POST
、HEAD
; - 请求头仅包含:
Accept
、Accept-Language
、Content-Language
、Content-Type
(且值为application/x-www-form-urlencoded
、multipart/form-data
、text/plain
)。
实现方式 :后端在响应头中添加Access-Control-Allow-Origin
,示例:
js
# 后端响应头(以Node.js Express为例)
res.setHeader("Access-Control-Allow-Origin", "http://frontend.com"); // 允许指定源跨域
res.setHeader("Access-Control-Allow-Credentials", "true"); // 允许携带Cookie(可选)
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); // 允许的请求方法
res.setHeader("Access-Control-Allow-Headers", "Content-Type"); // 允许的请求头
Access-Control-Allow-Origin
:核心字段,值可以是具体域名(如http://frontend.com
)或*
(允许所有源,但不能与Credentials=true
同时使用 ,因为*
会暴露 Cookie 安全风险);Access-Control-Allow-Credentials
:设为true
时,前端请求需携带credentials: true
(如fetch
的credentials: 'include'
),且Allow-Origin
不能为*
;Access-Control-Expose-Headers
:默认情况下,前端只能读取响应头的Cache-Control
、Content-Language
等基础字段,通过该字段可指定额外允许读取的头(如X-Total-Count
)。
(2)预检请求(Preflight)
不满足 "简单请求" 条件的请求(如PUT
/DELETE
方法、自定义请求头X-Token
),浏览器会先发送一次OPTIONS
请求(预检),询问后端 "是否允许该跨域请求",后端响应通过后,才发送真正的业务请求。
流程示例:
- 前端发送
OPTIONS
预检请求,携带头信息:
http
OPTIONS /api/data HTTP/1.1
Origin: http://frontend.com
Access-Control-Request-Method: PUT // 告知后端真实请求方法
Access-Control-Request-Headers: X-Token // 告知后端真实请求的自定义头
- 后端响应预检请求,确认允许:
http
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE // 允许的所有方法
Access-Control-Allow-Headers: X-Token // 允许的自定义头
Access-Control-Max-Age: 86400 // 预检结果缓存时间(24小时,避免重复预检)
优势与适用场景
- 优势:标准方案、支持所有 HTTP 方法、安全性高(后端精确控制允许的源);
- 适用场景:前后端分离项目、微服务间 API 调用(几乎所有跨域场景的首选)。
2. 代理服务器(Proxy):前端无感知的方案
代理服务器的核心思路是:前端不直接请求跨域的后端 API,而是请求 "同源的代理服务器",由代理服务器转发请求到后端 API,再将响应返回给前端。由于前端与代理服务器 "同源",不存在跨域问题;代理服务器是后端服务,不受浏览器同源策略限制。
实现方式(以开发环境和生产环境为例)
- 开发环境(Webpack/Vite 代理) :前端工程化工具(如 Webpack、Vite)内置代理功能,只需配置代理规则即可。示例(Vite 配置
vite.config.js
):
js
export default defineConfig({
server: {
proxy: {
// 匹配以/api开头的请求,转发到后端API
'/api': {
target: 'http://backend.com', // 后端API地址
changeOrigin: true, // 转发时修改请求的Origin为target(避免后端验证Origin失败)
rewrite: (path) => path.replace(/^/api/, '') // 可选:去掉/api前缀(如前端请求/api/data,实际转发到http://backend.com/data)
}
}
}
});
配置后,前端只需请求/api/data
(同源路径),Vite 会自动转发到http://backend.com/data
。
- 生产环境(Nginx 代理) :生产环境通常用 Nginx 作为静态资源服务器和代理服务器,配置示例:
nginx
server {
listen 80;
server_name frontend.com; # 前端域名(同源)
# 静态资源(前端页面)
location / {
root /usr/share/nginx/html;
index index.html;
}
# 代理跨域请求到后端API
location /api {
proxy_pass http://backend.com; # 后端API地址
proxy_set_header Host $host; # 传递请求头Host
proxy_set_header X-Real-IP $remote_addr; # 传递真实IP
}
}
此时,前端请求http://frontend.com/api/data
,Nginx 会转发到http://backend.com/data
,实现跨域。
优势与适用场景
- 优势:前端代码无需修改(无感知跨域)、支持所有请求方法、安全性高(代理服务器可做权限校验);
- 适用场景:开发环境快速调试、生产环境前后端分离项目(尤其适合后端无法修改 CORS 配置的场景)。
3. JSONP:古老但仍有用的方案
JSONP(JSON with Padding)是利用 **<script>
标签不受同源策略限制 ** 的特性实现跨域的方案,本质是 "后端返回一段可执行的 JavaScript 代码,前端通过回调函数获取数据"。
实现流程
- 前端定义回调函数
handleData
,并通过<script>
标签请求后端接口(携带回调函数名):
js
// 1. 定义回调函数
function handleData(data) {
console.log("跨域获取的数据:", data); // 处理后端返回的数据
}
// 2. 创建<script>标签,请求后端接口(携带回调函数名callback=handleData)
const script = document.createElement("script");
script.src = "http://backend.com/api/data?callback=handleData"; // 后端需解析callback参数
document.body.appendChild(script);
// 3. 加载完成后移除<script>标签(可选,避免冗余)
script.onload = () => {
document.body.removeChild(script);
};
js
// 后端响应(以Node.js为例)
const callback = req.query.callback; // 获取前端传递的回调函数名
const data = { name: "张三", age: 20 }; // 要返回的数据
res.send(`${callback}(${JSON.stringify(data)})`); // 返回:handleData({"name":"张三","age":20})
优势与局限性
- 优势:兼容性好(支持所有浏览器,包括 IE)、实现简单;
- 局限性:仅支持
GET
请求(<script>
标签只能发起 GET)、安全性低(后端返回的代码可能被注入恶意脚本,需严格校验数据)。 - 适用场景:仅用于兼容老旧浏览器,或获取第三方公开资源(如百度地图 API 的 JSONP 接口)
4. WebSocket:全双工通信的跨域方案
WebSocket 是 HTML5 定义的全双工通信协议,其握手阶段基于 HTTP,但一旦建立连接,后续数据传输不受同源策略限制(因为 WebSocket 协议本身不依赖 "源" 的概念)。
实现方式
- 前端创建 WebSocket 连接(URL 以
ws://
或wss://
开头)
js
const ws = new WebSocket("ws://backend.com/ws"); // 跨域连接
// 连接成功
ws.onopen = () => {
ws.send("前端发送的消息"); // 发送数据
};
// 接收后端消息
ws.onmessage = (event) => {
console.log("后端返回:", event.data);
};
// 连接关闭
ws.onclose = () => {
console.log("连接关闭");
};
- 后端搭建 WebSocket 服务(以 Node.js 的
ws
库为例):
js
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 8080 });
// 监听连接
wss.on("connection", (ws) => {
// 接收前端消息
ws.on("message", (message) => {
console.log("前端消息:", message.toString());
// 发送消息给前端
ws.send("后端收到消息:" + message.toString());
});
});
优势与适用场景
- 优势:全双工通信(前后端可实时双向发送数据)、不受同源策略限制;
- 适用场景:实时通信场景(如聊天应用、实时数据看板、WebSocket API 调用)。
五、跨域相关的安全风险
跨域方案在解决 "可用性" 的同时,需警惕安全风险:
- CORS 配置不当 :若
Access-Control-Allow-Origin
设为*
且允许Credentials=true
,会导致 Cookie 被任意网站窃取;应尽量指定具体的允许源(如http://frontend.com
)。 - JSONP 注入 :后端未校验
callback
参数,可能被注入恶意代码(如callback=恶意函数
);需对callback
参数做白名单校验(仅允许字母、数字、下划线)。 - postMessage 滥用 :接收方未验证
event.origin
(消息来源),可能接收恶意网站的消息;应在message
事件中判断event.origin
是否为可信源(如if (event.origin === "http://trusted.com") { ... }
)。