前端跨域完全指南:从为什么报错到三种解决方案

40 阅读6分钟

适合读者:前端初学者,遇到过 Console 里的 CORS 红色报错但不知道为什么会这样。

你将收获:理解同源策略的本质、能判断什么情况会跨域、掌握三种解决方案的原理和适用场景。


一个熟悉的红色报错

你写了一个 Vue 前端跑在 localhost:5173,后端 Express 跑在 localhost:3000。前端发了一个 fetch:

fetch('http://localhost:3000/stream?prompt=hello')

Console 里出现:

Access to fetch at 'http://localhost:3000/stream' from origin
'http://localhost:5173' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

你很困惑:"我不就是请求自己电脑上的另一个端口吗,这也要拦?"

是的。这就是同源策略(Same-Origin Policy),浏览器最核心的安全机制之一。


什么是"源"(Origin)

浏览器判断"是不是自己人"的标准很粗暴:协议 + 域名 + 端口,三者必须完全一样。

你的前端页面: http://localhost:5173

以下地址的"源":
http://localhost:5173/api/stream      ✅ 同源(端口一样)
http://localhost:3000/stream           ❌ 跨域(端口不同!5173 vs 3000)
https://localhost:5173/xxx             ❌ 跨域(协议不同!http vs https)
http://127.0.0.1:5173/xxx              ❌ 跨域(域名不同!localhost vs 127.0.0.1 是两个字面量)
http://api.deepseek.com/v1             ❌ 跨域(完全不同)

即使它们指向的是同一台物理机器、同一个进程,只要字符串不一样,就是跨域。


发生了跨域时,浏览器到底做了什么

很多人的误解是"浏览器阻止了跨域请求发出去"。不是的——请求发出去了,服务器也处理了,数据也返回了。

浏览器的操作流程是这样的:

1. JS 代码执行 fetch('http://localhost:3000/stream')
   ↓
2. 浏览器发现目标地址跟当前页面不同源
   ↓
3. 浏览器自动在请求头上加 Origin: http://localhost:5173
   (告诉对方:这个请求是从 5173 端口发来的)
   ↓
4. 请求正常发出 → 服务器正常处理 → 响应正常到达浏览器
   ↓
5. 浏览器检查响应头里有没有:
   Access-Control-Allow-Origin: http://localhost:5173
   或者
   Access-Control-Allow-Origin: *
   ↓
6a. 有 → JS 拿到数据,一切正常
6b. 没有 → 浏览器拦截,JS 什么都拿不到,Console 报错

跨域限制的不是"发请求",而是"JS 能不能拿到返回的数据"。 请求本身已经成功完成了。


为什么要有同源策略

假设没有同源策略:

  • 你打开了 http://evil-site.com 这个页面
  • 页面里的 JS 偷偷发了一个 fetch('http://your-bank.com/api/balance')
  • 由于你之前登录过银行,浏览器自动带了 Cookie
  • 攻击者的 JS 拿到了你的账户余额
  • 这就是 CSRF 攻击的基本思路

同源策略阻止了 A 网站的 JS 随意读取 B 网站的接口返回数据。这不是网络层面的安全,是浏览器层面的沙箱隔离


什么时候会触发跨域检查

场景是否触发跨域
JS 发 fetch() / XMLHttpRequest 到不同源会触发
<img src="不同源的图片">❌ 不触发
<script src="不同源的 JS">❌ 不触发(历史原因)
<link href="不同源的 CSS">❌ 不触发
浏览器地址栏直接输入 URL❌ 不触发(不是 JS 发起的)
Node.js 服务器发 fetch()❌ 不触发(没有浏览器,同源策略是浏览器规则)

注意最后一条——跨域是浏览器的规则,不是网络的规则。 服务器之间互相发请求不存在跨域限制。这是理解所有解决方案的关键。


解决方案一:CORS(让目标服务器说"我允许")

最简单的办法:在 Express 里加 CORS 中间件。

import express from 'express';
import cors from 'cors';

const app = express();

app.use(cors());  // 一行搞定,所有路由对所有来源开放

// 或者精细控制:
app.use(cors({
  origin: 'http://localhost:5173'  // 只允许这个来源
}));

app.get('/stream', (req, res) => {
  // ……
})

原理:Express 在每个响应里自动加上:

Access-Control-Allow-Origin: http://localhost:5173

浏览器看到这个头,就知道"哦,目标服务器允许我访问",于是把数据交给 JS。

适用场景:你拥有目标服务器的控制权(比如你自己的 BFF)。


解决方案二:代理(让浏览器觉得没跨域)

既然跨域是浏览器和目标服务器之间的事,那就在中间加一层代理:

没有代理:
浏览器 ──跨域请求──→ 目标服务器

有代理:
浏览器 ──同源请求──→ 代理服务器 ──服务端请求──→ 目标服务器
                        ↑                ↑
                   跟浏览器同源        没有浏览器参与
                                     不受同源策略限制

开发阶段:Vite Proxy

// vite.config.js
export default defineConfig({
  server: {
    proxy: {
      '/api': {                              // 拦截 /api 开头的请求
        target: 'http://localhost:3000',      // 转发到真正的后端
        rewrite: path => path.replace(/^\/api/, '')  // /api/stream → /stream
      }
    }
  }
})

前端请求 /api/stream(同源,不跨域),Vite 开发服务器收到后转发给 http://localhost:3000/stream。第二步转发是服务器之间的通信,不受同源策略限制。

生产阶段:Nginx 反向代理

server {
    listen 80;
    server_name example.com;

    # 前端静态文件
    location / {
        root /var/www/dist;
        try_files $uri /index.html;
    }

    # API 请求转发到 BFF
    location /api/ {
        proxy_pass http://localhost:3000/;
    }
}

原理跟 Vite Proxy 完全一样。前端始终只跟同源的 /api/xxx 通信。

适用场景:开发用 Vite Proxy,生产用 Nginx。前端代码不需要改,始终请求 /api/xxx 就行。


解决方案三:JSONP(历史遗迹,不推荐)

利用 <script> 标签不受同源策略限制的特点。只支持 GET 请求,需要后端配合,基本被淘汰了。了解就行,不要再用了。


三种方案对比

方案原理需要改谁适用
CORS 中间件目标服务器在响应里声明"允许"后端你拥有后端控制权
Vite Proxy浏览器和代理同源,代理转发前端项目配置本地开发
Nginx 反向代理浏览器和 Nginx 同源,Nginx 转发运维配置生产部署

实践中,通常是开发用 Vite Proxy,上线用 Nginx。前后端代码都不用因为跨域问题而妥协。


一个特例:为什么直接调 DeepSeek 不跨域

你可能会发现,前端直接 fetch('https://api.deepseek.com/...') 并不会报跨域错误。

原因很简单:DeepSeek 作为对外提供 API 的公司,它主动在响应头里加了 Access-Control-Allow-Origin: *

跨域不是"浏览器禁止你访问别人",而是"浏览器要求别人明确说可以访问"。DeepSeek 说了"可以",所以不报错。

那为什么还要加 BFF?因为 API Key。 DeepSeek 解决的是跨域问题,解决不了"你的 Key 暴露在浏览器里"的问题。


总结

  • 同源 = 协议 + 域名 + 端口完全一致,任何一个不同就是跨域
  • 跨域发生时,请求正常发送、返回,只是浏览器不让 JS 拿到结果
  • 跨域是浏览器的安全机制(沙箱),服务端之间通信不受限制
  • 三种解法
    • CORS:让后端加响应头说"允许"
    • 代理(Vite/Nginx):让浏览器请求代理,代理去转发
    • JSONP:别用了
  • 实战组合:开发 Vite Proxy + 生产 Nginx