浏览器的同源策略 是 Web 安全的核心基石,它的本质是限制不同源的文档 / 脚本,对当前源的资源进行未授权的访问 。简单来说:非同源的页面,无法随意操作对方的 DOM、Cookie、LocalStorage,也无法发送跨域 AJAX 请求。
一、 什么是 "同源"
同源的判定标准有 3 个核心要素 ,三者必须完全一致,才被视为同源:
- 协议(Protocol) :如
http:和https:是不同源 - 域名(Host) :如
example.com和sub.example.com是不同源 - 端口(Port) :如
example.com:80和example.com:8080是不同源
同源判定示例
以 http://www.example.com:80 为基准,判断以下 URL 是否同源:
| 对比 URL | 是否同源 | 原因 |
|---|---|---|
http://www.example.com:80 |
✅ 是 | 协议、域名、端口完全一致 |
https://www.example.com:80 |
❌ 否 | 协议不同(https vs http) |
http://sub.example.com:80 |
❌ 否 | 域名不同(sub.example vs www.example) |
http://www.example.com:8080 |
❌ 否 | 端口不同(8080 vs 80) |
http://www.example.com |
✅ 是 | 端口默认 80,三者一致 |
注意:
- 域名的大小写不敏感 ,
Example.com和example.com视为同源。- IP 地址和域名不互通,
http://192.168.1.1和http://example.com是不同源。
二、 同源策略限制的核心场景
同源策略的限制主要针对 浏览器端的脚本(如 JavaScript),目的是防止恶意网站通过脚本窃取其他网站的敏感数据。核心限制分为以下几类:
1. 限制 DOM 操作
非同源的页面,无法通过脚本访问对方的 DOM 元素。
- 场景 1 :如果
a.com的页面中通过<iframe>嵌入了b.com的页面,那么a.com的 JS无法读取b.com页面的document对象、DOM 节点、表单数据。 - 场景 2 :反过来,
b.com的 JS 也无法操作a.com的 DOM。
示例(禁止跨源 DOM 访问)
html
<!-- a.com 的页面 -->
<iframe id="iframe" src="https://b.com"></iframe>
<script>
const iframe = document.getElementById('iframe');
// 尝试访问 iframe 内部的 DOM → 报错!
iframe.onload = function() {
// 跨源时,会抛出 "Uncaught DOMException: Blocked a frame from accessing a cross-origin frame."
console.log(iframe.contentDocument.body);
}
</script>
2. 限制 Cookie/LocalStorage 等存储的访问
非同源的脚本,无法读取或修改对方域名下的 Cookie、LocalStorage、SessionStorage 等存储数据。
- 示例 :
a.com的 JS 无法通过document.cookie获取b.com下的 Cookie;也无法通过localStorage.getItem()读取b.com的本地存储。 - 例外 :如果 Cookie 的
Domain属性设置为父域名(如example.com),那么子域名(如sub.example.com)的脚本可以访问该 Cookie(属于同源策略的宽松规则)。
3. 限制 AJAX/Fetch 请求(跨域请求拦截)
浏览器禁止脚本向非同源的服务器发送 AJAX 或 Fetch 请求,或者说,即使发送了请求,服务器响应的数据也会被浏览器拦截,无法被脚本获取。
示例(跨域 AJAX 请求被拦截)
javascript
// a.com 的页面中,尝试请求 b.com 的接口 → 报错!
fetch('https://b.com/api/data')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.log(err));
// 报错:"Access to fetch at 'https://b.com/api/data' from origin 'https://a.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource."
关键说明:
- 跨域请求本身是发送成功的(服务器能接收到请求并返回响应);
- 浏览器会拦截响应,不允许脚本读取响应数据 ------ 这是同源策略的核心保护点。
三、 同源策略的 "例外情况"(允许跨源的操作)
同源策略并非 "一刀切",有一些场景是允许跨源操作的,主要是为了保障 Web 的基本功能:
<script>标签加载跨域 JS 文件 :比如加载 CDN 上的 jQuery、Vue 等库(src指向非同源地址)。- 限制:脚本执行后,仍受同源策略约束,无法访问加载页面的 DOM / 存储。
<link>标签加载跨域 CSS 文件:同理,CSS 可以跨域加载,浏览器会解析并应用样式。<img>标签加载跨域图片 :可以显示非同源的图片,支持防盗链(通过Referer控制)。<iframe>嵌入跨域页面:可以嵌入,但父子页面的脚本无法互相访问 DOM。- 表单提交(
<form>) :可以提交到非同源的服务器(比如登录表单提交到auth.example.com),提交后会跳转页面,不受同源策略限制。 - WebSocket 连接 :WebSocket 协议(
ws:///wss://)不受同源策略限制,可以直接连接非同源的服务器。
四、 如何突破同源策略
在实际开发中,经常需要跨域请求数据(如前后端分离项目、微服务),浏览器提供了合法的跨域方案,核心有以下几种:
1. CORS(跨域资源共享)
CORS(Cross-Origin Resource Sharing) 是最主流的跨域方案,由服务器端配置实现,浏览器自动适配。
- 核心原理 :服务器在响应头中添加
Access-Control-Allow-Origin等字段,明确告知浏览器 "允许哪些源的请求访问"。 - 工作流程 :
- 浏览器发送跨域请求时,会先发送一个 OPTIONS 预检请求(复杂请求才会触发,如带自定义头、POST JSON),询问服务器是否允许跨域;
- 服务器响应预检请求,返回允许的源、方法、头等;
- 预检通过后,浏览器发送真实的请求;
- 服务器返回响应数据,浏览器允许脚本读取。
示例(服务器配置 CORS,以 Node.js/Express 为例)
javascript
const express = require('express');
const app = express();
// 配置CORS,允许 a.com 跨域访问
app.use((req, res, next) => {
// 允许的源:可以是具体域名,也可以是 *(允许所有源,不推荐生产环境)
res.header('Access-Control-Allow-Origin', 'https://a.com');
// 允许的请求方法
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
// 允许的请求头
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 允许携带Cookie(需要前后端同时配置)
res.header('Access-Control-Allow-Credentials', 'true');
// 处理OPTIONS预检请求
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
// 接口路由
app.get('/api/data', (req, res) => {
res.json({ message: '跨域请求成功!' });
});
app.listen(3000);
2. JSONP(JSON with Padding)
JSONP 是一种利用 <script> 标签不受同源策略限制 的 "hack 方案",仅支持 GET 请求。
- 核心原理 :
- 前端定义一个回调函数(如
handleJsonp); - 通过
<script>标签的src拼接请求参数和回调函数名,发送到服务器; - 服务器返回一段 JS 代码,格式为
回调函数名(数据); - 浏览器执行这段 JS 代码,回调函数被触发,获取到数据。
- 前端定义一个回调函数(如
示例(JSONP 跨域请求)
html
<!-- 前端页面(a.com) -->
<script>
// 定义回调函数
function handleJsonp(data) {
console.log('JSONP获取到的数据:', data);
}
</script>
<!-- 动态创建script标签,发送请求 -->
<script src="https://b.com/api/jsonp?callback=handleJsonp"></script>
javascript
// 服务器端(b.com,Node.js/Express)
app.get('/api/jsonp', (req, res) => {
const callback = req.query.callback; // 获取回调函数名
const data = { message: 'JSONP跨域成功!' };
// 返回 JS 代码:handleJsonp({...})
res.send(`${callback}(${JSON.stringify(data)})`);
});
缺点:仅支持 GET 请求,存在 XSS 安全风险(服务器返回恶意代码会被执行),逐渐被 CORS 替代。
3. 反向代理(服务器代理)
反向代理 是前端无感知 的跨域方案,核心是利用同源策略只限制浏览器端,不限制服务器端的特点。
- 核心原理 :
- 前端请求同源的后端代理服务器 (如
a.com/api/proxy); - 代理服务器接收到请求后,转发请求 到目标非同源服务器(如
b.com/api/data); - 代理服务器获取目标服务器的响应,再将数据返回给前端。
- 前端请求同源的后端代理服务器 (如
示例(Nginx 反向代理配置)
# Nginx配置
server {
listen 80;
server_name a.com;
# 代理 /api/proxy 路径到 b.com
location /api/proxy {
# 转发目标地址
proxy_pass https://b.com/api/;
# 携带请求头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
前端请求时,直接请求同源的代理地址:
javascript
// 前端请求 a.com 的代理接口(同源)
fetch('http://a.com/api/proxy/data')
.then(res => res.json())
.then(data => console.log(data));
优点:支持所有请求方法,无前端代码修改,适合生产环境;缺点:需要配置服务器代理。
五、具体案例
我们以一个典型的电商平台为例,拆解跨域问题的产生过程:
1. 微服务拆分方案(真实场景)
假设电商平台拆分为以下核心微服务,每个服务独立部署:
| 微服务名称 | 服务地址(域名 + 端口) | 核心功能 |
|---|---|---|
| 前端静态服务 | https://shop.example.com |
提供前端页面(Vue/React) |
| 用户中心服务 | https://user.example.com |
登录、注册、用户信息查询 |
| 商品中心服务 | https://goods.example.com |
商品列表、商品详情查询 |
| 订单中心服务 | https://order.example.com |
创建订单、查询订单、支付 |
| 购物车服务 | https://cart.example.com:8081 |
购物车增删改查(端口不同) |
2. 跨域问题的具体触发场景
前端页面部署在 https://shop.example.com,当用户操作时,前端需要调用多个微服务的接口,每一次调用都会触发同源策略限制:
场景 1:用户登录(调用用户中心服务)
- 前端操作:用户在
shop.example.com页面输入账号密码,点击登录,前端通过 AJAX/Fetch 调用https://user.example.com/api/login接口; - 同源判定:
shop.example.com(前端)和user.example.com(后端)域名不同 → 非同源; - 浏览器行为:直接拦截响应,前端控制台报错
Access to fetch at 'https://user.example.com/api/login' from origin 'https://shop.example.com' has been blocked by CORS policy。
场景 2:浏览商品(调用商品中心服务)
- 前端操作:用户登录后,前端调用
https://goods.example.com/api/list获取商品列表; - 同源判定:
shop.example.com和goods.example.com域名不同 → 非同源; - 浏览器行为:同样拦截响应,商品列表无法加载。
3.微服务架构中解决跨域的典型方案(业务视角)
在上述电商案例中,实际开发不会让每个微服务单独配置 CORS(维护成本高),而是采用更适配微服务的方案:
方案 1:API 网关统一处理跨域(主流方案)
- 核心思路:在所有微服务前端部署一个 API 网关(如 Nginx、Spring Cloud Gateway、Kong),所有前端请求先经过网关,由网关统一配置 CORS 规则;
- 具体落地:
- 前端只请求网关地址
https://gateway.example.com(与前端shop.example.com可配置为同源,或网关统一返回 CORS 头); - 网关将请求转发到对应的微服务(用户 / 商品 / 订单);
- 网关在响应头中统一添加
Access-Control-Allow-Origin: https://shop.example.com等 CORS 字段;
- 前端只请求网关地址
- 优势:只需配置一次,所有微服务都受益,符合微服务 "统一入口" 的设计理念。
方案 2:微服务统一配置 CORS(兜底方案)
- 核心思路:通过微服务框架的全局配置,给所有服务统一添加 CORS 响应头;
- 举例(Spring Boot 微服务):所有微服务引入统一的跨域配置依赖,配置允许的源为
https://shop.example.com,允许的方法为 GET/POST/PUT/DELETE; - 劣势:每个微服务都要配置,若前端域名变更,需修改所有微服务的配置,维护成本高。