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

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

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


原文地址

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


什么是"跨域"?

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

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

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

跨域的例子

ruby 复制代码
✅ 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,这个网站里有一段代码:

html 复制代码
<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

简单请求的流程:

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

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

javascript 复制代码
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 复制代码
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Content-Type: application/json

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

预检请求(Preflight)

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

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

预检请求检查什么?

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

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

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

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

http 复制代码
# 请求(浏览器发给服务器)
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小时

服务器端处理

javascript 复制代码
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 是否允许带 Cookie true

credentials 模式

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

前端

javascript 复制代码
fetch('/api/data', {
  credentials: 'include'
});

服务端

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

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


跨域的解决方案

1. JSONP(已不推荐)

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

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

2. 代理服务器

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

md 复制代码
浏览器 ──> 我的服务器(同一源) ──> 目标服务器

Nginx 代理

nginx 复制代码
location /api/ {
  proxy_pass http://target-server.com/;
}

Node.js 代理

javascript 复制代码
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 开发代理

开发环境配置代理:

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

4. postMessage

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

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

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

深入了解 CORS 🔬

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

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

CORS 和 CSRF 的区别

CORS CSRF
是什么 跨域资源共享机制 跨站请求伪造攻击
作用 服务端允许/禁止跨域请求 利用用户已登录状态发起攻击
防御 服务端配置 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 不匹配),处理方式不一样的。

相关推荐
D_C_tyu6 分钟前
JavaScript | 数独游戏核心算法实现
javascript·算法·游戏
海天鹰8 分钟前
EXIF-JS
javascript
清汤饺子21 分钟前
【译】我的 AI 进阶之路:从怀疑到深度整合
前端·javascript·后端
@菜菜_达30 分钟前
Vue生命周期
前端·javascript·vue.js
每天吃饭的羊31 分钟前
UMD和IIfe
开发语言·前端·javascript
gCode Teacher 格码致知1 小时前
Javascript提高:自定义的占位符替换-由Deepseek产生
开发语言·javascript·ecmascript
前端那点事1 小时前
Vue线上代码调试全攻略(安全无侵入,新手也能上手)
前端·vue.js
前端那点事1 小时前
Vue批量文件上传并发踩坑指南:3步解决阻塞、限流、进度混乱
前端·面试
桔筐1 小时前
Vue3 v-model 双向绑定导致循环触发的坑
前端·javascript·vue.js
Alice-YUE1 小时前
前端图片优化完全指南:从格式到加载的全面提速方案
前端·笔记·学习