为什么禁止我请求别的网站的接口?——跨域与CORS

43 阅读6分钟

你有没有遇到过这种情况:在自己的网页上想请求别人的API,结果浏览器直接报错:Access-Control-Allow-Origin' header is missing。为什么浏览器要阻止你?服务器不响应不就完了吗?

今天,用小区门禁的故事,来讲讲 跨域CORS


原文地址

墨渊书肆/为什么禁止我请求别的网站的接口?——跨域与CORS


什么是"跨域"?

同源策略 — 浏览器的安全基石

浏览器有个同源策略Same-Origin Policy):只有来自同一个"家"的资源才能随便用。

什么叫"同一个家"?看三个条件:协议(http/https)、域名(example.com)、端口(:8080)。三个都一样,才是同源;有一个不一样,就是跨域。

跨域的例子

http://example.com 和 http://example.com/profile     // 协议+域名+端口都相同 → 同源
✅ https://example.com 和 https://example.com           // 协议+域名+端口都相同 → 同源
❌ http://example.com 和 https://example.com           // 协议不同 → 跨域
❌ http://example.com 和 http://api.example.com        // 域名不同(子域名)→ 跨域
❌ http://example.com:8080 和 http://example.com:3000  // 端口不同 → 跨域

跨域限制了什么?

浏览器的同源策略主要限制了三件事:

  • DOM 访问:无法读取不同源的 iframe 内容、无法修改不同源的 iframe DOM
  • AJAX 请求:无法请求不同源的 API
  • Cookie/LocalStorage:无法访问不同源的数据

为什么要限制跨域?

模拟一个攻击场景

想象一下:你登录了银行网站 bank.com,浏览器保存了你的登录 Cookie。

然后你手滑点进了一个恶意网站 evil.com,这个网站里有一段代码:

<form action="http://bank.com/transfer" method="POST">
  <input type="hidden" name="to" value="hacker">
  <input type="hidden" name="amount" value="1000000">
</form>
<script>document.forms[0].submit();</script>

如果没有同源策略,这个表单请求会自动带上 bank.com 的 Cookie,银行服务器以为是你本人操作的——钱就没了。

同源策略就是浏览器的"门禁":只有同一家人才能进,陌生人要查证件。

💡 注意:<img> 标签的 GET 请求虽然也会带 Cookie,但现代浏览器有 SameSite Cookie 保护。上面表单 POST 场景更典型。


CORS — 跨域的"通行证"

CORS 是什么?

CORS(Cross-Origin Resource Sharing)= 跨域资源共享。

它的工作原理很简单:让服务器告诉浏览器,"我允许来自这些源的请求"

简单请求 vs 预检请求

简单请求

满足以下条件的请求是"简单请求":

条件要求
请求方法GETPOSTHEAD
请求头部只有几种常见类型
Content-Type只能是 application/x-www-form-urlencodedmultipart/form-datatext/plain

简单请求的流程:

1. 浏览器发送请求(自动带上 Origin 头)
   
2. 服务器检查 Origin,决定是否允许
   
3. 服务器返回响应头 Access-Control-Allow-Origin
   
4. 浏览器检查响应头,允许就完事

服务器端示例(Node.js):

app.get('/api/data', (req, res) => {
  const origin = req.headers.origin;

  if (origin === 'https://example.com') {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }

  res.json({ data: '这是返回的数据' });
});

响应头

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Content-Type: application/json

{"data": "这是返回的数据"}

预检请求(Preflight)

不满足"简单请求"条件的,浏览器会先发一个 OPTIONS 请求"探路":

1. 浏览器发送 OPTIONS 预检请求
   
2. 服务器检查方法/头部/Origin
   
3. 服务器返回允许的头 Access-Control-*
   
4. 浏览器发送实际请求

预检请求检查什么?

预检请求(OPTIONS)就像登机前的安检——先检查你带没带危险品。

浏览器会问服务器三件事:

  • 我从哪来?(Origin)
  • 我想用什么方法?(Access-Control-Request-Method)
  • 我想带什么头?(Access-Control-Request-Headers)

服务器回答"可以",浏览器才放行实际请求。

# 请求(浏览器发给服务器)
OPTIONS /api/data HTTP/1.1
Origin: https://example.com              # 我从哪来
Access-Control-Request-Method: PUT        # 我想用 PUT 方法
Access-Control-Request-Headers: Content-Type, Authorization  # 我想带这些头

---

# 响应(服务器告诉浏览器)
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com  # 允许这个源
Access-Control-Allow-Methods: GET, POST, PUT, DELETE  # 允许这些方法
Access-Control-Allow-Headers: Content-Type, Authorization  # 允许这些头
Access-Control-Max-Age: 86400          # 预检结果缓存24小时

服务器端处理

app.options('/api/data', (req, res) => {
  const origin = req.headers.origin;

  if (origin === 'https://example.com') {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Max-Age', '86400');
  }

  res.status(204).send();
});

CORS 响应头详解

常用响应头

响应头作用例子
Access-Control-Allow-Origin允许的源*https://example.com
Access-Control-Allow-Methods允许的方法GET, POST, PUT
Access-Control-Allow-Headers允许的头部Content-Type, Authorization
Access-Control-Max-Age预检缓存时间86400(秒)
Access-Control-Allow-Credentials是否允许带 Cookietrue

credentials 模式

默认情况下,CORS 不带 Cookie。如果需要携带 Cookie:

前端

fetch('/api/data', {
  credentials: 'include'
});

服务端

res.setHeader('Access-Control-Allow-Origin', 'https://example.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');

注意:Access-Control-Allow-Origin 不能用 *,必须是具体域名。


跨域的解决方案

1. JSONP(已不推荐)

利用 <script> 标签不受同源策略限制的特性:

<script>
  function handleData(data) {
    console.log(data);
  }
</script>
<script src="http://api.example.com/data?callback=handleData"></script>
缺点说明
只支持 GET无法处理 POST 等请求
有安全风险可能被注入恶意代码
无法捕获错误错误处理困难

2. 代理服务器

在自己的服务器上转发请求,"伪装"成同源:

浏览器 ──> 我的服务器(同一源) ──> 目标服务器

Nginx 代理

location /api/ {
  proxy_pass http://target-server.com/;
}

Node.js 代理

app.get('/api/data', async (req, res) => {
  const response = await fetch('http://target-server.com/data');
  const data = await response.json();
  res.json(data);
});

3. Webpack/Vite 开发代理

开发环境配置代理:

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://target-server.com',
        changeOrigin: true
      }
    }
  }
};

4. postMessage

不同窗口/iframe 之间的通信:

window.addEventListener('message', (event) => {
  if (event.origin === 'https://example.com') {
    console.log('收到消息:', event.data);
  }
});

iframe.contentWindow.postMessage('hello', 'https://example.com');

深入了解 CORS 🔬

第三方 Cookie 的限制

现代浏览器正在逐步限制第三方 Cookie:

浏览器政策
Chrome计划逐步淘汰第三方 Cookie
Safari默认阻止第三方 Cookie
Firefox提供第三方 Cookie 阻止选项

CORS 和 CSRF 的区别

CORSCSRF
是什么跨域资源共享机制跨站请求伪造攻击
作用服务端允许/禁止跨域请求利用用户已登录状态发起攻击
防御服务端配置 Access-Control-*Token、SameSite Cookie、验证码

为什么 OPTIONS 叫"预检"?

"预检"就像登机前的安检——先检查你带没带危险品(方法、头部),没问题了才让你登机(发送实际请求)。


常见错误排查

错误 1:No 'Access-Control-Allow-Origin' header

原因解决
服务端没配置 CORS添加 Access-Control-Allow-Origin
Origin 不匹配检查配置的域名是否正确
credentials 时用了 *必须指定具体域名

错误 2:Method not allowed

原因解决
请求方法(如 PUT)不在允许列表检查 Access-Control-Allow-Methods

错误 3:Header not allowed

原因解决
请求头部(如 Authorization)不在允许列表检查 Access-Control-Allow-Headers

错误 4:预检请求 404

原因解决
服务端没有处理 OPTIONS 请求中间件或网关要放行 OPTIONS

总结

概念像什么作用
同源策略小区门禁限制不同源的访问,保护安全
CORS通行证告诉浏览器哪些跨域请求是允许的
简单请求普通访客不需要预检,直接请求
预检请求安检验票先检查再放行,更安全的请求
JSONP走后门已不推荐,有安全风险
代理同一个家门绕过跨域,最推荐的开发方案

写在最后

现在你应该明白了:

  • 跨域是浏览器的安全机制,不是为了刁难你
  • CORS 是服务器授权机制,服务器说可以,浏览器才放行
  • 预检请求 = 安检,OPTIONS 通过了才能发送实际请求
  • 生产环境推荐用代理,开发环境用 webpack/vite 代理

下次遇到跨域错误,先看浏览器控制台的报错信息——是"缺通行证"(header 缺失)还是"通行证不对"(origin 不匹配),处理方式不一样的。