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

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

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


一个熟悉的红色报错

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

js 复制代码
fetch('http://localhost:3000/stream?prompt=hello')

Console 里出现:

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

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

bash 复制代码
你的前端页面: 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             ❌ 跨域(完全不同)

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


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

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

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

markdown 复制代码
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 中间件。

js 复制代码
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 在每个响应里自动加上:

arduino 复制代码
Access-Control-Allow-Origin: http://localhost:5173

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

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


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

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

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

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

开发阶段:Vite Proxy

js 复制代码
// 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 反向代理

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
相关推荐
三8441 小时前
路由策略/控制 配置双点双向路由重发布
服务器·前端·javascript
Qimooidea2 小时前
祁木 CAD Translator 工程图纸出海实战指南
服务器·前端·安全
小林ixn2 小时前
从BFF到SSE:我在Vue项目里藏了个“AI翻译官”
前端·vue.js·vite
元气少女小圆丶3 小时前
unity发布web嵌入到前端页面的接受参数
前端·unity·webgl
工业HMI实战笔记3 小时前
【拯救HMI】:低碳制造:自动化技术如何助力企业节能降耗
运维·前端·自动化·交互·制造
不简说5 小时前
JS 代码技巧 vol.4 — 10 个异步并发控制,Promise.all 这帮兄弟的踩坑实录
前端·javascript·面试
Sinclair5 小时前
安企CMS的安装-PHPStudy(小皮面板)部署
前端·后端
会飞的特洛伊5 小时前
IAM身份认证
前端·后端
钛态6 小时前
AI 组件生成评测:别只看页面能不能渲染
前端·vue·react·web