同源策略

在讨论 CORS 之前,有必要先了解一下同源策略(Same Origin Policy 简称 SOP),因为跨域问题就是因为同源策略的存在而产生的

只存在于浏览器环境

同源策略是浏览器特有的安全机制。它的核心目的是限制不同源的网页脚本(如 JavaScript)对资源的访问权限,以保护用户数据和隐私。其他环境(如服务器、原生应用、桌面程序等)通常没有同源策略的限制,原因如下:

  1. 浏览器是开放的公共客户端
    • 用户通过浏览器访问任意网站,这些网站可能包含恶意代码。如果没有同源策略,恶意网站可以轻易窃取其他网站(如银行、邮箱)的用户数据。
    • 相比之下,服务器或原生应用是受控环境,代码由开发者完全掌控,不需要防范“随机第三方代码”的攻击。
  2. 其他环境的安全模型不同
    • 服务器:通过防火墙、身份认证、IP 白名单等机制保护接口,不依赖同源策略。
    • 移动应用:若需要网络请求,一般通过 HTTPS 证书校验或 Token 鉴权保证安全。
    • 桌面应用:权限控制由操作系统管理(如文件读写权限),而非网络资源的跨域限制。

浏览器为什么要有同源策略

主要为了解决以下问题:

  1. 防止恶意网站窃取用户数据

    • Cookie 劫持: 如果没有同源策略,假设用户登录了银行网站(bank.com),浏览器会存储该网站的 Cookie。此时,如果用户访问了一个恶意网站(evil.com),该网站可以通过 JavaScript 直接向 bank.com 发起请求,浏览器会自动携带 bank.com 的 Cookie,导致恶意网站以用户身份窃取数据或进行操作。

      // 恶意网站 evil.com 的代码
      fetch("https://bank.com/transfer?to=attacker&amount=1000", {
        method: "POST",
        credentials: "include" // 浏览器会自动携带 bank.com 的 Cookie
      });
      

      同源策略的防护:禁止 evil.com 的脚本直接读取 bank.com 的响应内容(即使请求被发送,结果也无法被恶意脚本获取)。

    • 本地存储(LocalStorage)和 DOM 数据泄露: 同源策略限制不同源的页面通过 JavaScript 访问彼此的 DOM、本地存储或 IndexedDB 数据,防止隐私泄露。

  2. 防范跨站请求伪造(CSRF)

    • CSRF 攻击原理:

      恶意网站诱导用户点击链接或加载图片,向用户已登录的其他网站(如 social.com)发起请求(如修改密码)。由于浏览器会自动携带目标网站的 Cookie,请求会被误认为是用户自愿发起的。

      <!-- 恶意网站通过图片标签发起 GET 请求 -->
      <img src="https://social.com/change-password?newPassword=hacked" />
      
    • 同源策略的限制: 虽然同源策略无法完全阻止 CSRF(因为请求仍会被发送),但它限制了恶意网站读取响应内容,降低了攻击的有效性。完整的 CSRF 防护还需结合 Token 校验等机制。

  3. 隔离不同网站的脚本执行环境

    • 如果两个不同源的页面共享同一个 JavaScript 执行环境,恶意脚本可能篡改其他页面的代码逻辑,或直接获取敏感信息。同源策略确保每个源的脚本只能操作同源的资源。
  4. 保护用户隐私

    • 假设用户访问了医疗网站(hospital.com),如果没有同源策略,其他网站可以通过 JavaScript 探测用户是否访问过 hospital.com(例如检测缓存、历史记录等),从而泄露用户的浏览习惯或健康状况。

同源条件

同源条件:协议(http/https)、域名(example.com)、端口(80/8080)三者必须完全相同

例如:https://example.com/page 与以下 URL 比较:

  • ✅ 同源:https://example.com/api
  • ❌ 跨源:http://example.com(协议不同)、https://sub.example.com(子域名不同)、https://example.com:8080(端口不同)

跨域

CORS: Cross-Origin Resource Sharing 跨域资源共享

比如前端网站部署在 https://www.myapp.com,后端 API 部署在 https://api.myapp.com,这两个资源是不同源的,当前端代码尝试访问后端 API 时就会遇到跨域问题,浏览器会报出类似这样的跨域错误:

Access to fetch at 'https://api.myapp.com/users' from origin 'https://www.myapp.com' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on 
the requested resource.

跨域解决方案

  1. CORS(跨域资源共享)

    • 原理:后端通过 HTTP 响应头声明允许的跨域请求

    • 关键响应头:

      • Access-Control-Allow-Origin: *(或按推荐做法:指定允许的域名)

      • Access-Control-Allow-Methods: GET, POST, PUT(允许的 HTTP 方法)

      • Access-Control-Allow-Headers: Content-Type, Authorization(允许的请求头)

      • Access-Control-Allow-Credentials: true(允许携带 Cookie,需与 credentials: 'include' 前端配置配合)

    • 预检请求(Preflight Request):复杂请求(如 Content-Type: application/json)会先发送 OPTIONS 请求进行预检,确认服务器是否允许跨域

  2. JSONP

    • 原理:利用 <script> 标签不受同源策略限制的特性,通过回调函数获取数据
    • 限制:仅支持 GET 请求,存在 XSS 风险
    • 现在用的很少了
  3. 代理服务器

    • 原理:通过同源的后端或开发服务器转发请求,绕过浏览器限制
    • 实现方式:配置 Nginx 反向代理

一些有趣的问题

跨域时浏览器是拦截的请求还是响应?

跨域实际上是浏览器拦截响应而不是拦截请求。我们可以通过实际抓包观察到:

  1. 请求确实已经发送到服务器
  2. 服务器也确实处理了请求并返回了响应
  3. 但是浏览器接收到响应后,发现响应头中没有正确的 CORS 相关字段(比如 Access-Control-Allow-Origin),就会拦截这个响应,不让 JavaScript 读取响应内容

这也就是为什么我们在浏览器开发者工具的 Network 面板中,能看到请求确实发出去了,也能看到响应返回了,但 JavaScript 代码仍然收到跨域报错。这种设计是出于安全考虑:即使请求发出去了,未经授权的跨域响应数据也无法被 JavaScript 读取和使用。

为什么开发环境没遇到跨域问题?

在本地开发时,虽然你在浏览器访问的是 localhost 的前端页面,而后端接口是另一个地址(比如 api.example.com),按理说会有跨域问题,但实际上并没有。这是因为:你用的前端开发工具会在你电脑上运行一个 Node.js 服务器,当浏览器要发送请求时,这个请求会被拦截并改道 - 不是直接发给真正的后端接口,而是先发给本地的这个 Node.js 服务器,然后 Node.js 服务器再把请求转发给真正的后端接口。这样做有效的原因是:跨域限制只存在于浏览器中,而 Node.js 程序是不受跨域限制的,它可以请求任何服务器的接口

简单来说就是:本地开发时有一个中间人(Node.js 服务器)帮你处理了跨域问题

为什么可以在网站里随便链接别的域名的图片资源?这不也跨域了?

主要原因在于浏览器对不同类型资源的安全策略不同,详情见 Content-Security-Policy (CSP)

简单说一下 CSP:CSP 通过白名单机制,针对不同类型的资源制定不同的加载策略。通过 CSP,我们可以清晰地看到浏览器对不同资源类型的默认处理差异:

  • 图片/CSS/字体 等被动资源通常允许跨域(可通过 img-src * 显式允许)
  • 脚本/XHR 等主动资源默认严格限制(需通过 script-src 明确授权)

浏览器同源策略主要限制的是以下两类行为:

  1. 数据交互:通过 JavaScript 发起的跨域请求(如 AJAX/fetch)
  2. DOM 访问:不同源页面之间的 DOM 操作

而对于以下资源加载行为,浏览器默认允许:

  • <img> 标签加载跨域图片
  • <link> 标签加载跨域 CSS
  • <script> 标签加载跨域 JS
  • <iframe> 加载跨域页面(但无法直接访问其 DOM)

也就是说,当在网页中使用跨域的图片时,浏览器会正常加载并显示图片,但如果尝试通过 JavaScript 操作该图片的像素数据,就会触发跨域限制(比如用 Canvas 的 drawImage 方法然后提取数据)

浏览器地址栏的请求会有跨域问题吗?为什么?

浏览器地址栏的请求(例如直接输入URL或点击链接跳转)不会有跨域问题

地址栏输入网址或点击链接导致页面跳转,属于导航行为,这时候浏览器会加载新的页面,整个过程是不会触发 CORS 机制的,因为 CORS 主要是针对 AJAX、Fetch 等由脚本发起的请求。而导航请求属于浏览器的默认行为,而非脚本发起的请求,不受同源策略的限制

浏览器为什么不限制导航请求呢,可以想到:

  • 这是用户的主动行为,被视为用户明确意图的操作,浏览器应该默认允许
  • 无脚本交互风险,导航请求仅加载新页面,不涉及脚本跨源读取数据(如读取其他域的DOM或资源)