今日遇到一个奇怪的问题,页面刷新就报登录失效,我刚登录也是报这个问题,经过仔细查找,发现SPA 单页项目部署 Nginx 后刷新子路由报错,算是老生常谈的经典坑了。但这次遇到的情况和常规 404 不一样,是子路由刷新直接报 401 未登录,坑点藏得更深,很多新手甚至中级前端都会被绕进去。
今天完整复盘一下这次线上真实问题,从现象、根因、踩坑点、最终落地方案一次性讲透,彻底根治 SPA 部署路由异常问题。
一、问题现象(线上真实场景)
项目技术栈:React + Vite + Java SpringBoot 后端,Nginx 反向代理部署。
线上环境出现诡异问题:
-
网站首页 https://admin\.xxx\.com/ 访问、登录、所有功能完全正常
-
登录后通过前端菜单跳转页面(如 /admin/model)一切正常
-
只要手动刷新子路由页面,直接弹出 401 未登录/登录过期
返回报文就是后端标准拦截提示:{"code":401,"message":"未登录或登录已过期"}
最让人迷惑的一点:本地 Vite 开发环境 完全没有任何问题,只有打包上线后才复现。
二、初期误区(绝大多数人都会踩的坑)
刚开始排查的时候,我第一反应也是:是不是 token 刷新失效、cookie 丢失、登录态没存住?
反复检查了一遍前端逻辑:
-
localStorage 存储 token 正常
-
请求拦截器统一携带 Authorization 请求头正常
-
本地代理调试所有接口鉴权正常
前端逻辑毫无问题。
接着又怀疑后端拦截器规则问题,但后端接口本身鉴权是稳定的,正常页面跳转、接口请求全部不会 401,唯独刷新路由报错。
三、真正根因(核心本质)
这里必须搞懂一个绝大多数前端模糊的知识点:Vite 本地代理 和 线上 Nginx 代理完全是两套逻辑。
本地开发时:
浏览器访问 localhost:5175/admin/model,是 Vite 服务接管路由,前端路由匹配页面,接口走 Vite 代理转发到 Java 8082 端口。
页面路由不会经过后端,只有接口会经过后端,所以永远不会 401。
线上部署时,出问题的根源就在最初的 Nginx 配置:
最初配置是全量代理,所有请求(不管是页面路由还是接口)全部转发给 Java 后端:
浏览器刷新https://admin.xxx.com/admin/model ➜ Nginx 直接转发到 Java 服务
Java 后端的逻辑是:所有非接口路径,没有对应接口,统一走全局拦截器鉴权。
后端把 /admin/model 当成了一个后端接口请求,而不是前端页面路由,没有登录态,直接拦截返回 401。
一句话总结根因:
SPA 前端路由刷新,被 Nginx 错误转发给 Java 后端,后端鉴权拦截器误拦截页面 GET 请求,导致刷新子路由 401。
四、理清架构:页面路由 和 接口路由 必须拆分
我们这套项目架构非常典型:
-
前端:纯静态 SPA 资源(打包后 index.html + js/css)
-
后端:Java SpringBoot 只负责提供接口,不负责页面渲染
-
接口分为两类:
/api/xxx业务接口、/auth/xxx登录鉴权接口
正确的线上转发逻辑必须是:
-
所有页面路由(/、/admin、/admin/model...):交给 Nginx,返回 index.html,由前端 JS 路由解析
-
所有接口路由(/api、/auth):反向代理转发给 Java 8082 后端
之前的致命错误就是:页面路由也被当成接口转发给后端了。
五、最终落地解决方案(生产可用、稳定根治)
1、Nginx 核心配置修正(关键)
拆分路由规则,页面走静态兜底,接口走后端代理,彻底解决刷新 401/404 问题:
单独代理后端两类接口,其余全部交给前端 SPA 路由:
nginx
# 业务接口代理
location /api/ {
proxy_pass http://127.0.0.1:8082;
proxy_http_version 1.1;
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;
}
# 登录鉴权接口代理(对应Vite本地/auth代理)
location /auth/ {
proxy_pass http://127.0.0.1:8082;
proxy_http_version 1.1;
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;
}
# 所有页面路由兜底,解决SPA刷新问题
location / {
try_files $uri $uri/ /index.html;
}
2、本地 Vite 配置保留(无需改动)
本地开发代理逻辑正常,保留双代理配置,兼顾业务接口和登录鉴权接口:
typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
server: {
port: 5175,
host: true,
proxy: {
'/api': {
target: 'http://localhost:8082',
changeOrigin: true,
rewrite: (p) => p,
},
'/auth': {
target: 'http://localhost:8082',
changeOrigin: true,
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
});
六、改完之后的正确逻辑
1、刷新 /admin/model
Nginx 找不到对应静态文件 → 兜底返回 index.html → 前端 JS 加载后自行匹配路由 → 不会请求后端,无 401
2、页面发起登录、业务请求
/auth/login、/api/xxx 匹配 Nginx 代理规则 → 正常转发 Java 后端 → 正常鉴权、正常返回数据
七、开发多年总结的 SPA 部署核心经验
1、永远不要让 Nginx 把前端路由转发给后端接口服务,这是 90% SPA 部署诡异报错的根源。
2、Vite、Webpack 的本地代理只在开发环境生效,打包后完全失效,千万不要用本地开发逻辑去臆测线上环境。
3、常规刷新 404 是路由找不到,刷新 401 一定是页面路由被后端拦截器拦截,优先排查 Nginx 转发规则,而非前端 token。
4、前后端分离架构,必须严格区分:Nginx 管页面,Java 管接口,职责边界一旦混乱,线上必出诡异问题。
八、最终效果
修改 Nginx 配置并重载后:
-
所有子路由页面刷新正常,不再 401
-
登录、登出、token 刷新、所有业务接口完全正常
-
首页、嵌套路由、多级路由全部稳定运行
问题彻底根治,线上稳定无复发。