跨域资源共享(CORS)
跨域资源共享(CORS) 是一种机制,允许浏览器从不同域(协议、域名或端口)加载资源。它通过使用额外的 HTTP 头来告诉浏览器允许哪些资源可以被访问,以及哪些 HTTP 方法和头部可以被使用。
跨域的背景和同源策略
Web 为什么会出现跨站访问?
早期网页以静态页面为主,页面、图片、脚本通常都来自同一个站点。后来 Web 应用逐渐拆分成多套服务:
- 前端页面部署在
https://app.example.com - API 服务部署在
https://api.example.com - 静态资源放在 CDN,例如
https://cdn.example.com - 第三方登录、支付、地图、埋点服务来自外部域名
这类架构是现代 Web 的常态。浏览器既要允许页面加载图片、脚本、样式等跨站资源,又要防止一个恶意页面读取用户在另一个站点中的隐私数据。因此浏览器需要一条清晰的安全边界: 跨站资源可以被使用到一定程度,但不能随意被 JavaScript 读取。
同源策略(Same-Origin Policy)
浏览器内置了一项安全机制——同源策略(SOP),规定:一个网页只能自由读取来自"同一来源"的资源。
"同源"需要三者完全一致:
| 维度 | 示例 |
|---|---|
| 协议(Scheme) | https:// |
| 域名(Host) | app.example.com |
| 端口(Port) | 443 |
以下均属于跨域:
| 当前页面 | 请求地址 | 原因 |
|---|---|---|
https://app.example.com | https://api.example.com | 子域名不同 |
https://app.example.com | http://app.example.com | 协议不同 |
https://app.example.com | https://app.example.com:8080 | 端口不同 |
https://app.example.com | https://other.com | 域名不同 |
为什么要有这个限制?
同源策略的核心目标不是让后端接口更难调用,而是防止“用户已经登录某站点”这件事被恶意页面利用。
若缺少同源策略,风险链路如下:
用户登录了 bank.com(Cookie 保存在浏览器)
用户打开了 evil.com
evil.com 的页面悄悄向 bank.com/transfer 发请求
浏览器自动带上 bank.com 的 Cookie
evil.com 可读取 bank.com 返回的账户余额、交易记录、个人资料
在这种模型下,只要用户访问了恶意网站,恶意网站就能以用户身份读取另一个网站的数据。同源策略用于阻断这类跨站读取行为,是浏览器最重要的安全基石之一, 它保护的是用户,而不是开发者。
同源策略重点解决的是“跨站读取响应”的问题,但它不等于完整的跨站攻击防护。另一个相关风险是 CSRF(Cross-Site Request Forgery,跨站请求伪造):恶意页面诱导浏览器携带用户已有登录态,向目标站点发起转账、删除、修改配置等有副作用的请求。
两者的防护边界不同:
- 同源策略主要限制浏览器中的 JavaScript 读取跨域响应
- CSRF 关注的是恶意页面能否借用户身份 发起有副作用的请求
- 即使浏览器因为 CORS 拦截了响应,服务端也可能已经收到了请求,所以写接口仍然需要 CSRF Token、SameSite Cookie、权限校验等保护
同源策略限制的是浏览器中的 JavaScript 读取响应内容。请求本身可能已经发出,服务端也可能已经收到并处理,只是浏览器拒绝把响应交给
JS。curl、Postman 等非浏览器工具不受同源策略约束。
CORS 的工作原理
CORS(Cross-Origin Resource Sharing,跨源资源共享)是浏览器和服务器协商的一套机制,**用于让服务端声明哪些来源可以跨域读取资源 **。
CORS 协议本质上是浏览器向服务端发起授权确认:
请求页面来源: https://app.example.com
目标资源地址: https://api.example.com/data
授权目标: 是否允许该来源读取响应
服务端通过 Access-Control-* 响应头声明授权结果。响应头满足 CORS 规则时,浏览器才会把响应暴露给 JavaScript;否则浏览器会在控制台报告
CORS 错误。
CORS 拦截的是哪一步?
CORS 的限制点不总是在请求发送阶段。更准确的执行过程如下:
| 阶段 | 是否可能发生 | 说明 |
|---|---|---|
| 发送预检请求 | 可能发生 | 非简单请求会先发 OPTIONS,预检失败则不会发送真实请求 |
| 发送真实请求 | 可能发生 | 简单请求会直接发送;预检通过后也会发送真实请求 |
| 服务端处理请求 | 可能发生 | 服务端可能已经执行业务逻辑 |
| JavaScript 读取响应 | 受 CORS 控制 | 响应头不符合 CORS 规则时,浏览器拒绝把响应内容暴露给 JS |
定位 CORS 问题时,需要先区分两个层面:
- 请求是否到达服务端:通过服务端访问日志、网关日志、DevTools Network 确认
- 响应是否暴露给 JavaScript:通过响应中的
Access-Control-*头确认
简单请求(Simple Request)
满足以下全部条件时,浏览器直接发请求,不做预检:
- 方法为
GET、POST、HEAD之一 - 请求头仅包含安全头(
Accept、Content-Type: text/plain|form等) - 无自定义头
浏览器在请求中自动添加 Origin,服务端响应中若包含合法 的 Access-Control-Allow-Origin,浏览器放行。
浏览器(app.example.com) 服务器(api.example.com)
│── GET /data ────────────────────→ │
│ Origin: https://app.example.com │
│ │
│ ←─ 200 OK ────────────────────── │
│ Access-Control-Allow-Origin: https://app.example.com
│ │
浏览器放行,JS 可读取响应
预检请求(Preflight Request)
当请求不满足"简单请求"条件时(如使用 PUT/DELETE、自定义头、Content-Type: application/json),浏览器先发一个 OPTIONS
预检请求,获得服务端许可后才发真实请求。
浏览器(app.example.com) 服务器(api.example.com)
│── OPTIONS /data ──────────────────→ │
│ Origin: https://app.example.com │
│ Access-Control-Request-Method: POST
│ Access-Control-Request-Headers: Authorization, Content-Type
│ │
│ ←─ 204 No Content ──────────────── │
│ Access-Control-Allow-Origin: https://app.example.com
│ Access-Control-Allow-Methods: POST
│ Access-Control-Allow-Headers: Authorization, Content-Type
│ │
│── POST /data ──────────────────────→ │ (真实请求)
│ ←─ 200 OK ──────────────────────── │
浏览器放行
关键响应头详解
Access-Control-Allow-Origin
声明允许哪些源访问。
Access-Control-Allow-Origin: * # 允许任意源(不能与凭证同用)
Access-Control-Allow-Origin: https://app.example.com # 只允许指定源
只能填一个值,不能填多个域名。如需支持多域名,需在服务端动态判断 Origin 请求头并响应对应值。
Access-Control-Allow-Methods
声明预检通过后,允许的 HTTP 方法。
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS