浏览器同源策略:安全基石,也是跨域问题的根源
一、什么是跨域问题?
跨域(Cross-Origin)指浏览器禁止当前页面向不同协议(protocol)、域名(domain)或端口(port) 的服务器发起请求的安全限制。这是浏览器核心安全策略------同源策略(Same-Origin Policy) 的直接体现。
三者必须完全相同:
http://www.example.com:80/dir/page.html
└──协议─┘ └───主机───┘└─端口┘
举例说明:
请求URL | 目标URL | 是否跨域 | 原因 |
---|---|---|---|
https://a.com |
https://a.com/api |
否 | 同源 |
http://a.com |
https://a.com |
是 | 协议不同 |
http://a.com |
http://b.com |
是 | 域名不同 |
http://a.com:80 |
http://a.com:8080 |
是 | 端口不同 |
http://b.a.com |
http://api.a.com |
是 | 子域名不同 |
二、为什么需要同源策略?
同源策略规定了来自同一源(协议、域名、端口三者都相同)的脚本,才能互相访问彼此的资源。
- 防止CSRF攻击:阻止恶意网站利用用户登录状态发起请求
- 保护用户隐私:避免敏感数据被第三方脚本窃取
- 隔离潜在威胁:限制不同源之间的DOM访问和操作
三、跨域场景
-
前后端分离
前端页面部署在 http://localhost:3000,后端接口在 http://api.example.com。
-
CDN 与主站点
静态资源如图片、脚本、样式文件托管在 https://cdn.example.com,主站在 https://www.example.com。
-
第三方 API 调用
网页需要调用 https://api.thirdparty.com 提供的地图、支付、社交登录接口。
-
跨端口访问
同域名但不同端口:http://example.com:8000 与 http://example.com:3000。
-
嵌入式 iframe
父页面与嵌入的 iframe 内容在不同域名下,需要交互数据。
四、解决方案
1. CORS(跨域资源共享)⭐️ 现代首选方案
原理:浏览器自动实现的W3C标准,需要服务端配合设置响应头。CORS 通过服务器在响应头里添加一系列标识,告诉浏览器哪些跨域请求被允许。
-
基本响应头
httpAccess-Control-Allow-Origin: https://www.example.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Allow-Credentials: true Access-Control-Max-Age: 3600
-
简单请求 vs 预检请求
- 简单请求 :GET/POST(
Content-Type
为application/x-www-form-urlencoded
、multipart/form-data
或text/plain
) - 预检请求 :对于其他方法或自定义头部,浏览器会先发送
OPTIONS
请求,服务器返回上述头部后才正式发起主请求。
- 简单请求 :GET/POST(
优点
- 标准化、浏览器原生支持
- 支持带凭证请求(
withCredentials=true
)
缺点
- 需要后端代码或配置支持
- 部分旧浏览器兼容性差
服务端配置示例(Node.js):
javascript
// 允许的跨域来源白名单(可维护多个域名)
const ALLOWED_ORIGINS = new Set([
'https://yourdomain.com',
'https://yourotherdomain.com'
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
// 动态设置允许跨域的源,仅限白名单内域名
if (origin && ALLOWED_ORIGINS.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
// 允许携带凭证(如 Cookie)
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 避免 CDN 缓存误用跨源响应
res.setHeader('Vary', 'Origin');
}
// 允许的 HTTP 方法(跨域请求中使用的)
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
// 允许的自定义请求头(需要与前端 fetch 的 header 保持一致)
res.setHeader('Access-Control-Allow-Headers', [
'Content-Type',
'Authorization',
'X-Requested-With',
'X-CSRF-Token' // 防止 CSRF 时前端发送的自定义头
].join(', '));
// 预检请求缓存时间(单位:秒)
res.setHeader('Access-Control-Max-Age', '86400');
// 安全性增强:防止 MIME 嗅探(内容类型攻击)
res.setHeader('X-Content-Type-Options', 'nosniff');
// 如果是预检请求,提前响应 204(无内容)
if (req.method === 'OPTIONS') {
res.setHeader('Content-Length', '0');
return res.status(204).end(); // 更符合语义的响应
}
next(); // 继续执行后续中间件
});
客户端处理:
javascript
fetch('https://api.example.com/data', {
credentials: 'include' // 需要发送Cookie时
});
2. 反向代理 ⭐️ 开发环境首选
原理:前端向同源的代理服务器发请求,代理服务器再转发到目标域,最后将结果返回给前端。对浏览器来说始终是同源请求。
前端开发服务器(如Webpack)或Nginx代理请求
Webpack配置示例:
javascript
// vue.config.js / webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://backend-server.com',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
}
Nginx配置:
nginx
server {
listen 80;
server_name frontend.com;
location /api/ {
proxy_pass http://backend-server.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
优点
- 无跨域限制
- 可隐藏真实接口地址,提高安全性
- 可统一做鉴权、日志、限流等
缺点
- 部署成本较高,需要额外的服务器或配置
- 增加网络跳数,可能带来性能开销
3. JSONP(历史方案)⚠️ 仅限GET请求
⚠️ JSONP 已基本被 CORS 所取代,仅在兼容老浏览器或特殊场景下使用。
利用 <script>
标签不受同源策略限制的特点,通过动态创建 <script src="...">
,并约定回调函数名,将服务器返回的 JSON 数据包裹在函数调用里。
服务端:
javascript
app.get('/data', (req, res) => {
const data = { message: "Hello from JSONP" };
const callback = req.query.callback;
// 返回函数调用包裹的JSON
res.send(`${callback}(${JSON.stringify(data)})`);
});
客户端:
javascript
function handleJSONP(data) {
console.log('Received:', data);
}
// 动态创建script标签
const script = document.createElement('script');
script.src = 'http://api.example.com/data?callback=handleJSONP';
document.body.appendChild(script);
优点
- 简单,无需浏览器额外设置
- 支持老旧浏览器
缺点
- 只支持
GET
请求 - 存在一定安全隐患(XSS 风险)
- 无法访问响应头
4. WebSocket协议
全双工通信协议,不受同源策略限制,但仍建议服务端校验 Origin
请求头,以防止非法的跨站连接。
客户端:
javascript
const socket = new WebSocket('wss://api.example.com');
socket.addEventListener('message', (event) => {
console.log('Message from server:', event.data);
});
socket.addEventListener('open', () => {
socket.send('Hello Server!');
});
优点
- 双向通信,无需频繁轮询
缺点
- 需服务端特别处理
- 浏览器兼容需关注
5. postMessage API ⭐️ 跨窗口通信
- 跨域的父子页面(iframe)之间通信
- WebWorker 与主线程间通信
原理:通过 window.postMessage()
发送消息,目标窗口通过 message
事件监听接收,并验证来源。
主页面:
javascript
// 向iframe发送消息
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage('Hello from main page', 'https://child-domain.com');
iframe页面:
javascript
window.addEventListener('message', (event) => {
// 验证来源
if (event.origin !== 'https://main-domain.com') return;
console.log('Received message:', event.data);
// 回复消息
event.source.postMessage('Message received!', event.origin);
});
优点
- 灵活,支持各种消息格式
- 安全,需显式验证
origin
缺点
- 仅限于窗口间或 Worker 通信,不适用于普通 AJAX
五、方案对比
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
CORS | 现代API调用 | 标准安全、支持所有HTTP方法 | 需要服务端改造 |
反向代理 | 本地开发、同域部署 | 前端无感知、无缝切换环境 | 生产环境需运维配合 |
JSONP | 旧浏览器兼容 | 支持老式浏览器 | 仅GET、安全性低 |
WebSocket | 实时双向通信 | 高性能、全双工 | 非HTTP协议、复杂度高 |
postMessage | 跨窗口通信 | 安全可控、支持跨域 | 仅限窗口间通信 |
六、注意事项
-
CORS配置风险:
- 避免滥用
Access-Control-Allow-Origin: *
- 生产环境应指定精确域名
- 敏感接口需要验证
Origin
头
- 避免滥用
-
CSRF防护:
javascript// 服务端验证示例 app.post('/transfer', (req, res) => { const origin = req.headers.origin; const allowedOrigins = ['https://trusted-site.com']; if (!allowedOrigins.includes(origin)) { return res.status(403).send('Forbidden'); } // 处理业务逻辑... });
-
Cookie安全:
- 使用
SameSite
属性限制Cookie - 敏感操作增加二次验证
- 使用
总结
推荐优先采用CORS+反向代理的组合方案,开发阶段通过代理解决跨域,部署阶段启用后端 CORS 支持,兼顾开发效率与生产安全。随着Web技术的发展,更优雅的跨域解决方案正在不断涌现,开发者应持续关注。
推荐阅读: