前端 React 跑在:
http://localhost:5173
后端 Express 跑在:
http://localhost:3000
结果请求一发出去,浏览器直接报错:
Access to XMLHttpRequest has been blocked by CORS policy
是浏览器限制,不是服务器限制
很多人误以为跨域是发不了请求,其实并不是后端拒绝了请求。
真正限制的通常是:JavaScript 跨站读取响应内容。
很多情况下:
- 请求已经发送成功
- 服务器也已经返回数据
但浏览器不会把响应结果交给当前页面的 JavaScript。
比如:
localhost:5173
访问:
localhost:3000
服务器可能已经返回:
{
"username": "admin"
}
但浏览器会拦住:不让前端 JavaScript 拿到这个数据。因为浏览器不知道你是不是恶意网站。
因为浏览器担心:
- 恶意网站盗取数据
- 用户 Cookie 泄漏
- 非法请求用户信息
所以默认禁止跨域访问。
这是浏览器的一个非常重要的安全机制:
同源策略(Same-Origin Policy)
所谓“同源”,必须同时满足:
- 协议相同
- 域名相同
- 端口相同
比如:
| 地址 | 是否同源 |
|---|---|
| localhost:3000 → localhost:3000 | ✅ |
| localhost:5173 → localhost:3000 | ❌ |
| http → https | ❌ |
即使都是本地 localhost,只要端口不同,也算跨域。
为什么浏览器要这么严格?
因为浏览器是“用户身份中心”。
里面保存着:
- Cookie
- Token
- 登录状态
- 用户隐私
- 本地存储
所以浏览器必须非常严格。
假设: 你已经登录了微博,浏览器里已经保存了微博登录状态:
Cookie:
session_id=abc123
这时候你又打开了一个恶意网站:
https://bad.com
bad.com 页面里偷偷写了:
fetch("https://weibo.com/api/userinfo", {
credentials: "include"
})
credentials是fetch 请求时“是否携带 Cookie”的配置。
credentials: "same-origin"表示只有同源请求才自动带 Cookie(默认)
credentials: "include"表示即使跨域,也把 Cookie 带上
credentials: "omit"表示永远不携带 Cookie
那么此时,浏览器会携带上cookie
GET /api/userinfo
Cookie: session_id=abc123
Cookie 是什么?
当你登录微博时:
账号 + 密码发送给微博服务器,登录成功后,微博服务器会返回Cookie:
Set-Cookie: session_id=abc123于是浏览器会偷偷保存:
weibo.com -> session_id=abc123以后只要你访问 weibo.com,浏览器都会自动带:
Cookie: session_id=abc123所以:Cookie 本质上就是“登录凭证”。
于是微博服务器会认为: “用户已经登录”。 那么相当于已经完成了认证。
恶意网站就可以:
- 冒充你的身份发送请求
- 以你的登录状态执行操作
如果浏览器再允许读取响应内容,那么用户隐私数据也会泄漏。
不仅能“读取”,还能“操作”。可以直接替你发微博:
fetch("https://weibo.com/api/post", {
method: "POST",
credentials: "include",
body: JSON.stringify({
content: "我是笨蛋"
})
})
浏览器还是会自动带 Cookie。于是微博服务器认为:是你本人在发微博。
这种:
借助用户已登录状态,
冒充用户发送请求
的攻击方式,
其实就是经典的:CSRF(跨站请求伪造)。
所以:浏览器不允许当前网站读取另一个网站返回的数据。同源策略主要限制JavaScript 读取响应。
即使请求已经发送成功,浏览器也可能拦截响应结果,不允许前端 JavaScript 获取。
那么浏览器会阻止所有跨域请求吗?
这也是个很大的误区,如果浏览器阻止所有跨域,那么怎么加载图片资源?
比如网页里:
<img src="https://other.com/a.png">
这个其实就是:跨域请求,当前网站请求不同域名的图片。但是浏览器允许,否则网页无法加载外部资源。因为:
- CDN图片
- 外部字体
- 外部JS
- 外部CSS
全都跨域。
浏览器真正限制的,是:JavaScript 跨站读取数据。
因为:
- 加载图片
- 加载脚本
- 加载 CSS
这些行为本身并不一定危险。
真正危险的是:恶意网站读取用户隐私数据。
很多跨域情况下: 浏览器允许请求发送,但会限制前端 JavaScript 获取响应内容。 不过,对于某些不符合 CORS 规则的请求,浏览器也可能直接终止请求。
虽然 <img> 可以跨域加载图片,
但如果你尝试:
canvas.drawImage(img)
再读取像素数据:
canvas.toDataURL()
浏览器仍然会报跨域错误。因为浏览器允许“显示资源”,不代表允许“读取资源内容”。
跨域最核心的一句话其实是:浏览器限制的不是“请求”,而是“跨站读取数据”。
理解原理之后,下面我们真正动手,亲自复现一次跨域问题。因为只有真正看到浏览器报错,才会对 CORS 有最深的理解。
实战:用一个小demo彻底理解跨域
先写一个没有cros跨域的后端代码
const express = require("express");
const app = express();
app.get("/user", (req, res) => {
console.log("接口被请求了");
res.json({
username: "admin",
role: "frontend"
});
});
app.listen(3000, () => {
console.log("server running at 3000");
});
前端React发送请求
import axios from "axios";
function App() {
async function getUser() {
try {
const res = await axios.get("http://localhost:3000/user");
console.log(res.data);
} catch (err) {
console.error(err);
}
}
return (
<div>
<button onClick={getUser}>
获取用户信息
</button>
</div>
);
}
export default App;
启动好前后端后,就可以点击前端按钮发送请求,我们可以看到浏览器的同源策略的报错:
此时再打开network面板
查看请求头发现返回码是200,说明已经成功返回但是response中没有数据
此时如果我们用Postman同样请求后端,可以看到数据正常返回:
查看后端发现接口正常请求:
说明是浏览器不拦截请求,而是拦截了js读取响应。
怎么解决跨域?
1、最常用:后端配置 CORS
后端加上cors中间件即可
const express = require("express");
const cors = require("cors");
const app = express();
app.use(cors());
app.get("/user", (req, res) => {
console.log("接口被请求了");
res.json({
username: "admin",
role: "frontend",
});
});
app.listen(3000, () => {
console.log("server running at 3000");
});
前端正常返回
cors() 到底干了什么?
其实它本质上只是给响应头加了:
Access-Control-Allow-Origin: *
在network中可以看到添加的响应头内容,表示服务器允许跨域读取
其实不用 cors 包,你自己也能写:
app.use((req, res, next) => {
res.setHeader(
"Access-Control-Allow-Origin",
"http://localhost:5173"
);
res.setHeader(
"Access-Control-Allow-Methods",
"GET,POST,PUT,DELETE"
);
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization"
);
next();
});
有的人可能会看到network里有两个请求
明明只写了一个:
axios.post(...)但 Network 里却出现:
OPTIONS /user POST /user这是因为浏览器发现: 这个跨域请求“可能不安全”。
比如:
- PUT
- DELETE
- Authorization
- application/json
于是浏览器会先发送:
OPTIONS询问服务器:“你允许这个跨域请求吗?”
服务器返回:
Access-Control-Allow-Origin Access-Control-Allow-Headers Access-Control-Allow-Methods浏览器确认允许后: 才会真正发送 POST 请求。
这个过程叫:预检请求(Preflight Request)
2、指定允许的前端地址(真实项目推荐)
实际项目一般不会:
Access-Control-Allow-Origin: *
而是只允许指定网站(origin 表示允许跨域的网站)
app.use(cors({
origin: "http://localhost:5173"
}));
这样只有:localhost:5173 能访问,更安全。
3、携带 Cookie 时(非常重要)
如果请求默认携带cookie
fetch(..., {
credentials: "include"
})
axios.defaults.withCredentials = true
那么后端不能写:
Access-Control-Allow-Origin: *
浏览器会直接报错。因为一旦请求携带cookie+允许所有网站跨域访问,那么黑客可以通过cookie拿到用户的隐私信息。而没有携带cookie时最多只会暴露公开信息,风险相对较小。
正确写法:后端必须指定可以跨域的网站
app.use(cors({
origin: "http://localhost:5173",
credentials: true
}));
浏览器最终会看到:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
这样浏览器才允许跨域携带 Cookie。
4、前端代理(开发环境最常见)
这是React/Vite/Vue 开发最常用的方法。核心思想是“骗过浏览器”
由于跨域问题只是浏览器的机制,那么我只要前端请求时同源,代理服务器再偷偷转发给后端不就可以了吗?本质是服务器的跨域,避免了浏览器的跨域。
浏览器的跨域:前端协议/地址/端口 与 要请求的协议/地址/端口 不一致。
5173 → 3000
现在前端先请求:
5173 → 5173
同源。
然后Vite 服务器偷偷帮你转发到 3000。于是浏览器以为根本没跨域。
Vite 配置 proxy
export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:3000",
changeOrigin: true,
},
},
},
});
前端
axios.get("/api/user");
然后Vite 自动代理,转发到3000/user,Vite 代替浏览器去请求后端。
所以 proxy 本质是:“中间服务器转发”,也叫反向代理(Reverse Proxy)
代理只能开发用,因为真正跨域问题并没消失。只是开发服务器帮你中转了(只存在于开发环境,生产环境没有vite),生产环境还是需要后端正确配置 CORS。(Nginx、网关、Node 中间层)
5、Nginx、网关、Node 中间层 反向代理(生产环境)
原理就是代理转发。它们虽然都是代理请求,但是职责完全不同:
| 组件 | 更偏向 | 主要职责 |
|---|---|---|
| Nginx | 网络层 | 静态资源、反向代理、负载均衡 |
| API Gateway | 系统入口层 | 鉴权、限流、服务路由 |
| Node BFF | 业务层 | 聚合接口、适配前端、SSR |
Nginx
Nginx 是一个高性能 Web 服务器 + 反向代理服务器。最大的特点是: 非常快、并发能力非常强。
Nginx是事件驱动(Event Driven),异步非阻塞 IO
它的思想是:一个线程管理很多连接,并发能力极强。
而不是传统服务器模型的:一个连接开一个线程,如果有10000个用户那么就会有10000个线程,线程切换成本巨大,CPU会爆炸。
你可以先把它理解成:“网站门口的总管”,用户访问网站时,其实很多时候第一时间接触到的不是 Node/Java/Python 后端,而是Nginx。
为什么需要 Nginx?
因为后端服务不适合直接暴露给用户。
如果直接给用户访问会有很多问题:
- 性能差
- 并发能力弱
- 不适合处理静态资源
- HTTPS 麻烦
- 容易崩
- 无法负载均衡
Nginx 最核心的几个作用:
一. 静态资源服务器
index.html
main.js
style.css
logo.png
静态资源本质就是“读文件”,浏览器请求资源,Nginx直接从磁盘读取文件返回。
Nginx 是 C 语言写的,并且极度优化了文件 IO。
- 内存占用低
- 系统调用少
- 文件读取快
二. 反向代理(最重要)
解决浏览器跨域问题。
三. 负载均衡
比如你的网站火了,一台 Node 扛不住。
于是:
Node1
Node2
Node3
Nginx:有很多算法,自动帮你分流。
- 轮询(默认)
请求A → Node1
请求B → Node2
请求C → Node3
请求D → Node1
请求E → Node2
请求F → Node3
2. 权重,性能强的机器分更多请求。
-
IP Hash,同一用户固定同一服务器。
-
最少连接,谁最闲给谁。
用户
↓
Nginx
↓
Node1 / Node2 / Node3
四. HTTPS
Nginx 本质上是专门为“高性能网络通信”设计的,专注解决“网络层问题”。
-
TCP 连接
-
HTTP 请求
-
文件传输
-
HTTPS 加密
-
请求转发
-
并发连接
网关(Gateway)
网关是所有后端服务的统一入口。
真实项目中往往会有多个后端,也就是微服务。
微服务就是把一个巨大的后端系统,拆成很多“小服务”。
每个服务:
- 独立开发
- 独立运行
- 独立部署
- 独立数据库(很多时候)
用户服务
订单服务
支付服务
商品服务
聊天服务
推荐服务
...
如果没有网关,前端要自己请求:
user.xxx.com
order.xxx.com
pay.xxx.com
chat.xxx.com
问题巨大:
- 接口太乱
- 域名很多
- 权限难统一
- 登录难统一
- 安全难管理
- 跨域复杂
- 前端耦合严重
于是需要一个“总入口”,这就是:Gateway(网关)。
网关最核心作用就是统一入口,用户永远只访问:
api.xxx.com
比如:
api.xxx.com/user
api.xxx.com/order
api.xxx.com/pay
网关内部再转发:
/user → 用户服务
/order → 订单服务
/pay → 支付服务
Gateway 常见功能:
- 登录鉴权(特别重要)
比如:JWT。
用户请求:
/api/order
Gateway 先检查:
Authorization: Bearer xxx
验证:
- token 是否合法
- 是否过期
- 用户权限
验证成功再转发给:订单服务,后端服务不用重复鉴权。
2、API 聚合(特别经典)
比如:前端首页需要:
- 用户信息
- 商品列表
- 推荐数据
如果前端自己请求:
/user
/products
/recommend
会很多请求。
Gateway 可以统一返回:
{
"user": {},
"products": [],
"recommend": []
}
这叫:BFF(Backend For Frontend)
3、限流
比如: 防止有人疯狂刷接口。
Gateway:
1秒最多10次
超过则直接拒绝。
4、灰度发布
比如:新功能只给:
10% 用户
Gateway:可以控制流量。
5、服务发现
微服务很多:
user-service
order-service
pay-service
IP 还会动态变化。
Gateway负责:找到真正服务地址。
Node中间层(BFF)
真实后端接口往往很难用
比如首页需要:
- 用户信息
- 商品列表
- 推荐数据
结果后端给你:
/user/detail
/product/list
/recommend/list
/banner/list
/activity/list
前端请求一堆。
await fetch(...)
await fetch(...)
await fetch(...)
await fetch(...)
更恶心的是:不同后端数据格式还不统一。
于是出现了: Node 中间层(BFF),“专门服务前端” 所以: BFF = Backend For Frontend
Node 中间层会做什么?
- 聚合接口
前端:
/home
Node内部:
/user
/product
/recommend
然后统一返回:
{
"user": {},
"products": [],
"recommend": []
}
前端只请求一次,性能更好,代码更简单。
- 统一数据格式
Node可以统一不同语言写的后端接口
return {
code: 0,
data: result
}
3. 处理权限逻辑
比如:JWT。
Node可以统一验证,后端服务不用重复写。
jwt.verify(token)
4. 防止前端直接暴露真实后端
比如:真实后端接口:
internal-user-service.xxx
前端根本不知道,只知道:
/api/user
安全性更高。
- SSR(特别重要)
SSR(服务端渲染)是指:页面 HTML 由服务器提前生成,而不是浏览器拿到 JS 后再渲染。
普通 React(CSR 客户端渲染)是先返回空的HTML,再下载JS,最后渲染。
你现在写 React:
比如: Next.js。
Node 中间层可以服务器提前请求数据,然后直接返回 HTML,SEO 更好。
6、JSONP(历史方案,了解即可)
早期浏览器没有 CORS,于是利用<script> 可以跨域的特点。
比如:
<script src="https://cdn.com/vue.js"></script>
浏览器会:
第一步:请求
GET https://cdn.com/vue.js
第二步:服务器返回 js
window.Vue = ...
第三步:浏览器直接执行这段 JS。
所以页面里就有:
Vue
那么JSONP 的本质就是模仿这个过程(script 请求到的内容会被“当 JS 执行”)
前端先定义函数:
function getUser(data) {}
后端返回:
getUser({...})
浏览器把返回内容当 JS 执行,于是函数被调用,数据就传进去了,实现:“跨域获取数据”。
但 JSONP 有巨大缺点,现在基本都用 CORS。
- 只能 GET
- 不安全
- 本质是执行 чужое JS
- 现代项目几乎不用