前后端联调:跨域与 Vite 代理
1. 一个让后端开发者崩溃的现象
本地起前端 http://localhost:5173,Spring Boot 在 http://localhost:8080。前端调接口:
await axios.get('http://localhost:8080/api/notes')
报错:
Access to XMLHttpRequest at 'http://localhost:8080/api/notes'
from origin 'http://localhost:5173' has been blocked by CORS policy
后端完全无感——日志里请求根本没到。前端明明能访问后端,浏览器却拦了自己。
这就是同源策略(Same-Origin Policy)。理解它是联调一切问题的起点。
2. 同源的定义
同源 = 协议 + 域名 + 端口 都相同。
| URL A | URL B | 是否同源 |
|---|---|---|
http://a.com/x |
http://a.com/y |
✅ 同源 |
http://a.com |
https://a.com |
❌ 协议不同 |
http://a.com |
http://b.a.com |
❌ 域名不同 |
http://a.com |
http://a.com:8080 |
❌ 端口不同 |
http://localhost:5173 |
http://localhost:8080 |
❌ 端口不同 |
浏览器强制执行同源策略:非同源的 XHR/fetch 请求,响应回来时浏览器拒绝让 JS 读(请求其实发出去了、后端也收到了,只是前端代码拿不到结果)。
Java 对照:类似网关的权限校验——后端放行了,前面这道"安检"仍然可能拦。只是这道安检不在网关,而在用户浏览器里。
3. 为什么要同源策略
防止攻击。如果不限制:
<!-- 你打开恶意网站 evil.com -->
<script>
// evil.com 上的 JS 去访问你的银行网站
fetch('https://mybank.com/transfer?to=hacker&amount=1000', {
credentials: 'include' // 带上你的银行 cookie
})
</script>
你登录过银行网站,cookie 还在浏览器。恶意网站如果能任意跨域请求,就能用你的身份转账。
同源策略的核心作用:限制一个站点上的 JS 不能随便读其他站点的数据。
4. 解决跨域的几种思路
有 4 种主流方案,按使用场景:
| 方案 | 谁改 | 适合场景 |
|---|---|---|
| CORS | 后端 | 正式上线的前后端(最主流) |
| 开发代理(Vite proxy) | 前端 | 开发环境,前后端本地分别跑 |
| Nginx 反向代理 | 运维/部署 | 生产环境同域部署 |
| JSONP | 后端 | 已过时,别用 |
in4vue 的策略:
- 开发环境 → Vite proxy(前后端本地各跑各的)
- 生产环境 → 同域部署,不需要跨域(所有请求都是
/api/*)
5. CORS:跨域资源共享
CORS (Cross-Origin Resource Sharing) 是浏览器和服务器约定好的一套"允许跨域"的规则——通过 HTTP 响应头协商。
核心流程:
浏览器 → 发跨域请求 → 服务器
浏览器 ← 响应 + Access-Control-Allow-Origin: https://你的前端.com ← 服务器
浏览器: "服务器允许这个源访问,放行"
服务器响应里没有对应的 CORS 头,浏览器就拦住。
5.1 简单请求 vs 预检请求
简单请求(直接发,服务器响应带 CORS 头即可):
- 方法是
GET/POST/HEAD - Content-Type 是
text/plain/application/x-www-form-urlencoded/multipart/form-data - 没有自定义 header
预检请求(Preflight)(浏览器先发 OPTIONS 问服务器):
- 方法是
PUT/DELETE/PATCH - Content-Type 是
application/json(大多数 REST API) - 带自定义 header(如
Authorization)
浏览器 → OPTIONS /api/note → 服务器 (预检,问"我能发 DELETE 吗")
浏览器 ← 200 + Access-Control-Allow-Methods: DELETE ← 服务器 (行)
浏览器 → DELETE /api/note → 服务器 (真正的请求)
实战里 99% 是预检请求——带 JSON + 自定义 header(Authorization)就触发。
5.2 Spring Boot 配 CORS
方式 A:全局
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:5173", "https://in4vue.pages.dev")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true) // 允许带 Cookie
.maxAge(3600); // 预检缓存 1 小时
}
}
方式 B:Spring Security(如果在用)
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:5173"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
坑点:
allowCredentials(true)时,allowedOrigins不能是*——必须明确列出域名- Spring Security 和 WebMvc 各自配 CORS 会互相覆盖——优先用 Spring Security 的,因为它的过滤器链更早
OPTIONS预检不要被 Security 拦了(默认放行,但自定义 filter 链要注意)
5.3 核心响应头
| 响应头 | 含义 |
|---|---|
Access-Control-Allow-Origin |
允许哪些源(* 或具体域名) |
Access-Control-Allow-Methods |
允许的 HTTP 方法 |
Access-Control-Allow-Headers |
允许的自定义 header |
Access-Control-Allow-Credentials |
允许带 Cookie(true / false) |
Access-Control-Max-Age |
预检结果缓存多久 |
Access-Control-Expose-Headers |
暴露给 JS 的响应头(默认只能读常见的) |
6. Vite 代理:开发环境的首选
开发时前后端同时跑在本机但端口不同——不想折腾后端 CORS,就用 Vite 代理。
原理:前端代码里请求 /api/notes → Vite dev server 拦截这个请求 → 转发到 http://localhost:8080/notes。浏览器看到的是同域,完全没有跨域问题。
6.1 配置
// vite.config.ts
export default defineConfig({
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
})
参数解释:
target—— 转发到哪个真实后端changeOrigin: true—— 修改请求的 Host 头为 target 的 host(绝大多数后端需要)rewrite—— 重写路径。示例:前端请求/api/notes→ 代理后变/notes
6.2 不改路径的情况
如果后端本身就以 /api 开头:
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
// 不要 rewrite,保留 /api 前缀
}
前端请求 /api/notes → 代理到 http://localhost:8080/api/notes。
6.3 多个后端
一个前端连多个后端:
proxy: {
'/api': { target: 'http://localhost:8080', changeOrigin: true },
'/ai': { target: 'http://localhost:8787', changeOrigin: true }, // AI 代理
'/ws': {
target: 'ws://localhost:8080',
ws: true, // WebSocket
changeOrigin: true,
},
},
6.4 前端代码这样写
// src/api/request.ts
const service = axios.create({
baseURL: '/api', // 注意:不是 http://localhost:8080
// ...
})
开发时走 Vite 代理,生产时这个 /api 也对(见后面 Nginx 章节)。一套代码两套环境通用,这才是 Vite 代理的精髓。
6.5 调试代理
代理出问题看不到日志?加 configure:
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq, req) => {
console.log('[proxy]', req.method, req.url, '→', proxyReq.path)
})
proxy.on('error', (err, req) => {
console.error('[proxy error]', req.url, err.message)
})
},
}
7. 生产环境:Nginx 反向代理
in4vue 的生产部署:前端放 Cloudflare Pages / Nginx,后端在另一台机器。最佳实践是同域部署,避免跨域:
用户浏览器 → https://in4vue.example.com/
├─ / → Nginx 返静态文件(前端)
├─ /api/* → Nginx 反代到 http://backend:8080/
└─ /ws → Nginx 反代 WebSocket
7.1 Nginx 配置
server {
listen 443 ssl http2;
server_name in4vue.example.com;
ssl_certificate /etc/ssl/in4vue.crt;
ssl_certificate_key /etc/ssl/in4vue.key;
# 前端静态文件
root /var/www/in4vue/dist;
index index.html;
# SPA 路由回退(必须)
location / {
try_files $uri $uri/ /index.html;
}
# 后端 API 反向代理
location /api/ {
proxy_pass http://localhost:8080/; # 注意末尾斜杠,会吃掉 /api 前缀
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket
location /ws {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
}
# 静态资源长缓存
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
7.2 proxy_pass 斜杠的坑
# A: 末尾有斜杠
location /api/ {
proxy_pass http://backend:8080/;
}
# /api/notes → http://backend:8080/notes (吃掉 /api)
# B: 末尾无斜杠
location /api/ {
proxy_pass http://backend:8080;
}
# /api/notes → http://backend:8080/api/notes (保留 /api)
选哪个:看后端接口是否自带 /api 前缀。Spring 项目一般配 server.servlet.context-path=/api,用方式 B;没配就用方式 A。
7.3 Cloudflare Pages 的反代
in4vue 目前部在 Cloudflare Pages。做反代有两种:
A. Edge Function(已有 edge/ 目录就是干这个):
// edge/functions/api/[[path]].ts (Cloudflare Pages Functions)
export async function onRequest(context: any) {
const url = new URL(context.request.url)
const target = 'https://backend.example.com' + url.pathname.replace('/api', '')
return fetch(target, context.request)
}
B. _routes.json:声明哪些路径走 Function、哪些走静态:
{
"version": 1,
"include": ["/api/*", "/ai/*"],
"exclude": []
}
Java 对照:Cloudflare Pages Function = Spring 的过滤器/拦截器 —— 在请求路由到真实业务前动手脚。
8. Cookie 跨域的坑
Authorization: Bearer xxx 这种走 header 的 token 不涉及 cookie 跨域。但项目用 httpOnly Cookie 存 token,会遇到:
8.1 前端要求带 cookie
// axios
const service = axios.create({
baseURL: '/api',
withCredentials: true, // 跨域时带上 cookie
})
// fetch
fetch('/api/user', { credentials: 'include' })
8.2 后端 CORS 配合
config.setAllowCredentials(true);
config.setAllowedOrigins(List.of("https://in4vue.pages.dev")); // 不能是 *
8.3 Cookie 本身的属性
Set-Cookie: token=abc;
Path=/;
HttpOnly; ← JS 读不到,防 XSS
Secure; ← 只走 HTTPS
SameSite=None; ← 跨站可带(配合 Secure 才生效)
SameSite 是关键:
Strict—— 完全不跨站带(安全但苛刻)Lax—— 默认值,导航跳转会带None—— 跨站带(必须配合Secure)
前后端不同域时,Cookie 必须 SameSite=None; Secure,否则浏览器不发。
同域部署就没这麻烦——这也是为什么生产推荐 Nginx 反代同域。
9. 前后端联调清单
新搭前后端对接,按这个顺序排查:
1. 能 ping 通后端吗?
[后端日志能看到请求] → 进下一步
[后端日志看不到] → 前端请求根本没出去(路径错/代理错)
2. 响应状态码对吗?
[200] → 进 CORS / 数据格式
[404] → 路径不对
[401 / 403] → 鉴权
[500] → 后端异常
[0 / (failed)] → 网络错误或 CORS 预检失败
3. 浏览器报 CORS 错?
→ 后端 CORS 配置缺失或域名没列
→ 或者用 Vite proxy 绕过
4. 能拿到数据但字段不对?
→ 接口字段命名约定(驼峰还是下划线)
→ 日期格式、枚举值
5. 能跑但偶发失败?
→ 网络问题 → axios 重试
→ 后端偶发 5xx → 找后端
→ 用户端慢 → 加超时和友好提示
10. Network 标签的实战用法
Chrome DevTools → Network —— 联调时要一直开着。
关键列:
- Status —— 状态码(红色 = 失败)
- Type —— xhr / fetch / document / ...
- Size —— 响应体大小
- Time —— 总耗时
- Waterfall —— 时间轴瀑布图
查错姿势:
- 失败的请求点开 → Headers 看请求头、Payload 看请求体、Response 看响应
- Preview 自动格式化 JSON
- Timing 看耗时分布(DNS / Connect / Wait / Download)
- Copy → Copy as cURL —— 把请求复制成 curl 命令,直接给后端复现
11. Mock:后端没写完的过渡方案
联调最烦"后端接口没写好前端只能干等"。几种 Mock 方案:
11.1 前端自己 mock(最快)
// src/api/note.ts
const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true'
export const noteApi = {
page: USE_MOCK
? async () => ({ list: [...mockNotes], total: 100 })
: (params) => request.get('/note/page', { params }),
}
优点:零依赖 缺点:switch 散在业务代码里,上线前要清理
11.2 Mock Service Worker (MSW)
拦截浏览器层面的请求,不污染业务代码:
pnpm add -D msw
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/note/page', () => {
return HttpResponse.json({ list: [...], total: 100 })
}),
]
开发模式启动 Service Worker,业务代码原样发请求——MSW 拦下来返假数据。
11.3 独立 mock 服务
起个 json-server 或 mockjs:
pnpm dlx json-server db.json --port 8080
配合 Vite proxy 指向它,完全模拟真实后端。
in4vue 的实战选择:开发早期用 MSW(本地无依赖),接口稳定后关掉走真后端。
12. 常见坑点
| 现象 | 原因 | 解法 |
|---|---|---|
CORS preflight Access-Control-Allow-Origin header is empty |
后端没配 CORS 或 OPTIONS 被拦 | 后端加 CORS 配置,确保 OPTIONS 返 2xx |
| Cookie 跨域不发 | SameSite 或 Secure 问题 | SameSite=None; Secure,或同域部署 |
| 代理配了还是 404 | 路径 rewrite 错了 | console.log 代理后的 path 排查 |
| 本地跨域 OK 上线不行 | 生产没配 Nginx 反代 | 配 /api 反代或后端加正式 CORS |
| 接口超时但后端已经返回 | axios 默认 0ms = 不限时,但某些代理层超时 | 设 timeout: 10000 |
| 文件下载 Content-Disposition 读不到 | 没在 CORS 暴露这个 header | Access-Control-Expose-Headers: Content-Disposition |
| POST 请求变 OPTIONS,真请求没发 | 预检失败 | 看 OPTIONS 响应,多半是 Allow-Headers 少了 Authorization |
13. 三个经典场景完整例子
13.1 本地开发(前后端分别跑)
// vite.config.ts
server: {
proxy: {
'/api': { target: 'http://localhost:8080', changeOrigin: true },
},
}
// src/api/request.ts
baseURL: '/api'
后端不用配 CORS,因为走代理后浏览器看到的是同域。
13.2 生产同域部署(推荐)
前端构建产物和后端在同一个域名下(不同路径)。Nginx 反代。前端 baseURL: '/api' 不变,后端也不用配 CORS。
13.3 前端和后端不同域(跨域正经部署)
frontend.example.com + api.example.com。后端必须配 CORS,前端 baseURL: 'https://api.example.com'。
14. 心智模型
前端请求接口的本质:
浏览器 ──HTTP──> 某个 URL
"跨域"只是浏览器的一条规则:
"你从 A 站访问 B 站数据,我帮用户把把关"
绕开这条规则的三条路:
1. 让 B 站(后端)说"允许 A 站来访" → CORS
2. 让 A 站的请求走同一个"门卫" → Nginx / Vite proxy (反代)
3. 把前后端部到同一个域名下 → 没有跨域问题
生产推荐: 反代 + 同域
开发推荐: Vite proxy
小练习
- in4vue 的
vite.config.ts已经配了 proxy,本地跑起来,开 DevTools 看请求路径 - 故意改错
target看代理失败是什么样 - 假设有个 Spring Boot 后端
localhost:8080/notes,配 CORS 允许 5173 访问 - 把 axios 的
baseURL临时改成http://localhost:8080,不走代理,观察 CORS 报错 - 写一个 Nginx 配置,前端静态 +
/api反代的完整示例