一、什么是跨域?

同源策略(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:防止敏感数据窃取

假设没有同源策略:

  1. 你登录了 bank.com,浏览器存有会话 Cookie
  2. 你访问了恶意网站 evil.com
  3. evil.com 的 JS 向 bank.com/api/account 发请求(浏览器会自动带上 Cookie)
  4. 如果没有同源策略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.social

SSO 涉及的跨域问题包括:

  • Cookie 共享sso.rho.im 设置的 Cookie,app1.rho.im 能否读取?

    • 同一父域(.rho.im)下可以通过设置 domain=.rho.im 共享
    • 不同域(.rho.im vs .rho.social)之间无法直接共享 Cookie
  • Token 传递:认证中心如何将登录状态安全地传递给各应用?
  • 跨域重定向:OAuth 流程中的 302 重定向跨越多个域

3. 第三方服务集成

你的网站 https://app.rho.im
  → 调用地图 API: https://api.mapbox.com
  → 嵌入支付页面: https://pay.stripe.com
  → 加载字体: https://fonts.googleapis.com

4. 微前端架构

不同团队的微应用部署在不同子域,需要在主应用中集成:

主应用:     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.imapp.rho.im
SameSite=None; Secure跨站 Cookie 传递第三方登录
Token(JWT)方案完全不同的域rho.imrho.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.imrho.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(跨端口 = 跨域)

演示步骤

  1. 先让学员看到错误:前端直接请求跨域 API,打开 DevTools 观察报错
  2. 分析请求:在 Network 面板观察 OPTIONS 预检请求
  3. 逐步修复:添加 CORS 响应头,观察请求成功
  4. 对比方案:切换到反向代理方案,展示无需 CORS 也能工作
  5. 高级场景:演示带 Cookie 的跨域请求(credentials: 'include'
配套的交互式演示代码

点击一下链接查看教学演示:

https://vistart.github.io/examples/articles/cors-demo.html

https://gist.github.com/vistart/1ad7113fcabe411a545536795374d7d6


vistart
8 声望4 粉丝

未破壳的雏。