一、什么是跨域?
同源策略(Same-Origin Policy)
浏览器的同源策略是 Web 安全的基石。它规定:只有当两个 URL 的 协议(Protocol)、域名(Host)、端口(Port) 三者完全一致时,才被视为"同源"。
以 https://app.rho.im 为基准:
| 目标 URL | 是否同源 | 原因 |
|---|---|---|
https://app.rho.im/api/data | ✅ 同源 | 协议、域名、端口均相同 |
http://app.rho.im/api | ❌ 跨域 | 协议不同(http vs https) |
https://api.rho.im/data | ❌ 跨域 | 域名不同(子域名也算不同源) |
https://app.rho.social/page | ❌ 跨域 | 域名完全不同 |
https://app.rho.im:8443/api | ❌ 跨域 | 端口不同(443 vs 8443) |
同源策略限制了什么?
- XMLHttpRequest / Fetch 请求:不能读取跨域响应(最常见的场景)
- DOM 访问:不能通过 JS 访问跨域 iframe 的 DOM
- Cookie / LocalStorage / IndexedDB:不能读取其他源的存储数据
注意:同源策略不阻止请求发出,而是阻止浏览器读取响应。服务器实际上收到了请求并返回了响应,只是浏览器拒绝将响应交给 JS 代码。这个细微区别非常重要。
二、为什么需要同源策略?——安全性分析
场景 1:防止敏感数据窃取
假设没有同源策略:
- 你登录了
bank.com,浏览器存有会话 Cookie - 你访问了恶意网站
evil.com evil.com的 JS 向bank.com/api/account发请求(浏览器会自动带上 Cookie)- 如果没有同源策略,
evil.com就能读取你的银行账户信息
这就是经典的 CSRF(跨站请求伪造) 攻击的基础。同源策略确保 evil.com 无法读取 bank.com 的响应。
场景 2:防止 DOM 篡改
如果允许跨域 DOM 访问:
恶意网站嵌入 <iframe src="bank.com/login">
→ JS 读取 iframe 中用户输入的密码
→ 密码被发送到攻击者服务器核心安全原则
同源策略本质上实现了一个沙箱隔离机制:每个源(origin)都在自己的沙箱里运行,无法访问其他沙箱的数据。这是最小权限原则在浏览器中的体现。
三、常见跨域场景
1. 前后端分离架构
最常见的场景。前端部署在 app.rho.im,API 服务在 api.rho.im:
前端 https://app.rho.im → fetch("https://api.rho.im/users") → 跨域!2. 单点登录(SSO)
企业常见需求——用户登录一次即可访问多个子系统:
认证中心: https://sso.rho.im
应用 A: https://app1.rho.im
应用 B: https://app2.rho.socialSSO 涉及的跨域问题包括:
Cookie 共享:
sso.rho.im设置的 Cookie,app1.rho.im能否读取?- 同一父域(
.rho.im)下可以通过设置domain=.rho.im共享 - 不同域(
.rho.imvs.rho.social)之间无法直接共享 Cookie
- 同一父域(
- Token 传递:认证中心如何将登录状态安全地传递给各应用?
- 跨域重定向:OAuth 流程中的 302 重定向跨越多个域
3. 第三方服务集成
你的网站 https://app.rho.im
→ 调用地图 API: https://api.mapbox.com
→ 嵌入支付页面: https://pay.stripe.com
→ 加载字体: https://fonts.googleapis.com4. 微前端架构
不同团队的微应用部署在不同子域,需要在主应用中集成:
主应用: https://portal.rho.im
微应用 A: https://team-a.rho.im
微应用 B: https://team-b.rho.social四、跨域解决方案
方案 1:CORS(跨域资源共享)— 最标准的方案
服务器通过 HTTP 响应头声明"允许哪些源访问我的资源":
Access-Control-Allow-Origin: https://app.rho.im
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true简单请求 vs 预检请求(Preflight):
- 简单请求:GET/POST + 常规 Header → 直接发送,浏览器检查响应头
- 预检请求:PUT/DELETE 或自定义 Header → 浏览器先发
OPTIONS请求询问服务器是否允许,通过后才发送实际请求
浏览器 服务器
|-- OPTIONS /api/data -------->| ← 预检请求
|<-- 200 + CORS Headers ------| ← 服务器回复"允许"
|-- PUT /api/data ------------>| ← 实际请求
|<-- 200 + Data ---------------|方案 2:反向代理 — 最常用的生产方案
通过 Nginx 等反向代理,让前端和 API 看起来在同一个源:
server {
server_name app.rho.im;
# 前端静态资源
location / {
root /var/www/frontend;
}
# API 请求代理到后端服务
location /api/ {
proxy_pass http://backend-server:3000/;
}
}这样前端访问 https://app.rho.im/api/users 实际被代理到后端,浏览器看到的始终是同源请求。
方案 3:JSONP — 历史遗留方案
利用 <script> 标签不受同源策略限制的特点。仅支持 GET,已基本被 CORS 取代,了解即可。
方案 4:postMessage — 跨窗口通信
用于 iframe、window.open 等场景的安全跨域通信:
// 父窗口 (https://app.rho.im) 向 iframe 发消息
iframe.contentWindow.postMessage({ type: 'login', token: 'xxx' }, 'https://sso.rho.social');
// iframe (https://sso.rho.social) 接收消息
window.addEventListener('message', (event) => {
if (event.origin !== 'https://app.rho.im') return; // 验证来源!
console.log(event.data);
});方案 5:Cookie 跨域策略
| 策略 | 适用场景 | 示例 |
|---|---|---|
domain=.rho.im | 同一父域下的子域共享 | sso.rho.im ↔ app.rho.im |
SameSite=None; Secure | 跨站 Cookie 传递 | 第三方登录 |
| Token(JWT)方案 | 完全不同的域 | rho.im ↔ rho.social |
五、SSO 单点登录的跨域实现
方案 A:共享 Cookie(同父域)
适用于 *.rho.im 下的多个子域:
1. 用户访问 app1.rho.im → 未登录 → 重定向到 sso.rho.im/login
2. 用户在 sso.rho.im 登录成功
3. sso.rho.im 设置 Cookie: Set-Cookie: token=xxx; Domain=.rho.im; Path=/
4. 用户回到 app1.rho.im → 浏览器自动带上 Cookie → 已登录
5. 用户访问 app2.rho.im → Cookie 同样可用 → 自动登录方案 B:CAS / OAuth 重定向(跨域)
适用于 rho.im 与 rho.social 之间:
1. 用户访问 app.rho.social → 未登录
2. 重定向到 sso.rho.im/authorize?redirect_uri=https://app.rho.social/callback
3. 用户在 sso.rho.im 登录(或已登录)
4. sso.rho.im 重定向回 app.rho.social/callback?code=AUTHORIZATION_CODE
5. app.rho.social 后端用 code 换 token(后端到后端,不受同源策略限制)
6. app.rho.social 设置自己域的 Cookie 或返回 JWT方案 C:前端 Token + postMessage
1. 主应用打开隐藏 iframe 指向 sso.rho.im/check-session
2. sso.rho.im 检查自己的 Cookie → 如已登录,通过 postMessage 回传 token
3. 主应用收到 token → 存入内存 → 完成登录六、教学演示建议
准备工作
使用你的域名搭建如下环境:
https://app.rho.im → 前端应用(端口 8080)
https://api.rho.im → 后端 API(端口 3000)
https://sso.rho.social → SSO 认证中心或者本地演示:
http://localhost:8080 → 前端
http://localhost:3000 → API(跨端口 = 跨域)演示步骤
- 先让学员看到错误:前端直接请求跨域 API,打开 DevTools 观察报错
- 分析请求:在 Network 面板观察 OPTIONS 预检请求
- 逐步修复:添加 CORS 响应头,观察请求成功
- 对比方案:切换到反向代理方案,展示无需 CORS 也能工作
- 高级场景:演示带 Cookie 的跨域请求(
credentials: 'include')
配套的交互式演示代码
点击一下链接查看教学演示:
https://vistart.github.io/examples/articles/cors-demo.html
https://gist.github.com/vistart/1ad7113fcabe411a545536795374d7d6
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。