
1. 登录成功却卡在门外现象复现与链路梳理1.1 我遇到的现象长什么样两三个月前我在跑黑马点评项目把登录接口调通了后端返回200Redis里也有数据但前端就是进不了主页。具体现象有两类第一类是登录页提交后没有任何反应控制台也没有报错前端页面始终停留在登录页第二类是登录跳转成功了但页面瞬间闪一下又跳回登录页或者跳转后主页内容区全是空白F12里能看到主页接口返回401。这个问题之所以让人头大是因为登录成功这个信号本身就是模糊的。前端可能已经拿到token但后续的存储、路由、请求头携带、后端拦截器校验每一环都有可能导致登录了但进不去主页。如果你正跟着黑马点评项目做到登录模块或者自己手写一个前后端分离项目时遇到了类似的登录跳转失效问题这篇可以帮你少走弯路。文章会把登录后到主页显示之间所有容易断链的位置都过一遍并且讲清楚每一步怎么验证。我的处理思路不要求你有多深的底子只要你肯打开浏览器F12、肯打开Redis命令行窗口就能一步一步定位到问题。1.2 先分清问题出在哪一层在动手改代码之前先要在心里把登录到主页这条链路分成4个环节登录请求环节前端把账号密码发给后端后端校验通过生成token并返回。前端保存环节前端拿到token后要把它放在一个跳转后还能取到的地方。路由跳转环节登录成功后定向到主页路由同时路由守卫对未登录状态做拦截。主页接口请求环节主页组件加载完发请求拉取数据请求头带着token后端拦截器校验通过才能返回数据。判断问题在哪个环节最快的方法是打开浏览器的Network面板然后重新走一遍登录流程。看登录成功后浏览器有没有发出访问主页的请求如果连主页请求都没有问题多半在路由跳转之前可能是前端保存或路由守卫环节。如果主页请求发出去了但返回401/403/500问题大致在后端拦截器、路径匹配或Redis读取环节。如果主页请求返回200但页面还是跳回了登录页问题可能是前端response拦截器误判或者路由守卫逻辑写死。我用一个简单的表格做了速查后面排查时你可以直接参照现象最大嫌疑环节第一验证点登录后完全没跳转前端保存token路由跳转前localStorage里有没有token跳转后闪回登录页路由守卫beforeEach的判断条件跳转后白屏接口401后端拦截器/Redis请求头Authorization字段是否存在登录后接口404代理路径不匹配控制台的请求URL和后端Controller路径1.3 黑马点评这个项目的登录态链路黑马点评的登录方案在同类项目里算很有代表性的后端用Redis保存登录token前端用Vue3 Pinia管理登录状态登录成功后把token放到请求头的Authorization字段里后端通过拦截器拦截请求并校验Redis中的token。整个系统包括商户查询缓存、优惠券秒杀、达人探店、关注推流等模块每个模块都有需要登录才能访问的接口所以登录态一旦出问题基本所有页面都会瘫痪。这个方案里有两个拦截器一个是刷新token有效期的拦截器另一个是校验是否需要登录的拦截器。配置文件里通过addPathPatterns和excludePathPatterns来决定一个接口是否需要登录。跳转主页问题最高发的地方就是这两个拦截器的路径匹配和token读取逻辑。后文我会逐个展开说明并且把我在实际调试时看到的现象、改过的代码、踩过的坑都写出来尽量让你照着做就能找到自己项目里的病灶。2. 登录态机制里最容易被忽视的三处代码2.1 Redis里token的真实存活状态在黑马点评的登录流程里用户登录成功后后端并不是生成一个JWT再返回而是生成一个随机字符串token然后以token为key把用户对象序列化成JSON存进Redis。核心代码大概是String token UUID.randomUUID().toString().replace(-, ); String userJson JSONUtil.toJsonStr(loginForm.getUser()); stringRedisTemplate.opsForValue().set(LOGIN_USER_KEY token, userJson, 30, TimeUnit.MINUTES);这里的LOGIN_USER_KEY是常量通常定义成login:token:。很多登录后跳转不成功的根子就埋在这个key的前缀上。比如说后面前端提交请求时后端用login:token:去Redis里查但登录成功时存的是login:开头那必然查不到。这种错误在复制代码时很容易出现因为常量名一样但赋值内容被改过。查这个问题的时候最直接的方式是在Redis里手动看keyredis-cli KEYS login:* TTL login:token:xxxTTL如果显示-1说明你写expire时没生效token永远不会过期显示-2说明这个key根本不存在。而如果key存在且TTL正常那就往下看拦截器。另外还需要注意Redis超时时间的单位。代码里写的是30分钟但如果你为了演示把超时改成30, TimeUnit.SECONDS页面停留一会儿再操作就会遇到明明登录了突然跳回登录页的情况。我当时第一次调试就是因为赶时间把超时设成了30秒结果每次跳主页都要重登看起来完全像登录功能坏了其实是超时环境问题。2.2 双拦截器的刷新与校验逻辑这个项目配置了两个拦截器第一个拦截器叫RefreshTokenInterceptor作用有两个一是从请求头里取出token二是在Redis中查到用户后把用户对象塞进ThreadLocalUserHolder顺便用expire刷新一次过期时间。第二个拦截器叫LoginInterceptor它的preHandle只看UserHolder里有没有用户没有就返回401有就直接放行。两个拦截器必须约定好顺序registry.addInterceptor(refreshTokenInterceptor) .addPathPatterns(/**) .order(0); registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/user/login, /user/code, /shop/**, /shop-type/**, /blog/hot, /upload/**, /**/*.js, /**/*.css, /**/*.png, /**/*.jpg) .order(1);这里有个常见认知偏差很多人觉得只要把loginInterceptor的excludePathPatterns写好就行却没有注册refreshTokenInterceptor或者order写反了。如果第一个拦截器没注册第二个拦截器里UserHolder永远为空所有需要登录的接口都会返回401。主页自然进不去。用生活化的比喻第一个拦截器是门卫负责检查进门的人有没有带工作证并在签到表上续期第二个拦截器是场馆检票员负责放行到特定区域。如果门卫岗位压根没人检票员看到所有观众都没签到就全拦下了。这个顺序问题正是我后来在排查第二个项目时揪出来的关键点后面会单独展开。2.3 前端路由守卫与axios拦截器的配合前端有两个跟登录态强相关的文件router/index.js和utils/request.js。路由守卫在跳转前判断登录状态常见的写法是router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path ! /login !token) { next(/login); } else { next(); } });axios拦截器则负责在请求发出前把token塞进请求头request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[authorization] token; } return config; });这个配合里有一个很隐蔽的坑如果登录页在提交时用了一个独立封装的fetch而主页请求用的是axios且那份axios封装里没有request拦截器那么token就不会带上去。还有一种情况是response拦截器看到401就执行localStorage.removeItem(token)然后跳转登录页。这个逻辑本身没问题但需要确认跳转是不是无条件的。我见过有人写成router.push(/login)结果主页接口哪怕只是暂时性的401也会被强制踢出去。对于这种情况我会建议用window.location.href做纯页面跳转或者在response拦截器里加一个白名单判断只对真正需要重新登录的场景才清除token。3. 我的实际排查过程从Network面板到源码逐行比对3.1 第一步看Network面板并抓住下一个请求排查的时候先打开Network面板把Preserve log勾上这样页面跳转时之前的请求记录不会消失。然后重新提交登录。我当时看到的网络请求序列是这样的POST /user/login返回200响应体里有token。GET /shop/1返回401。紧接着页面自动跳转回/login。这说明两点登录接口本身没问题问题出在登录之后的携带token或校验环节。接下来要确认的是GET /shop/1这个请求的请求头里到底有没有Authorization字段。点击这条请求在Headers面板里找Request Headers区域。如果发现Authorization字段是空的或者完全没这个字段基本可以确定是前端没把token放进去或者axios拦截器没生效。如果Authorization字段有值则换方向去后端看拦截器取token的逻辑。这一看能帮你省下至少半小时的无效排查时间因为很多人一看见401就去改后端Redis逻辑结果问题压根不在后端。3.2 第二步到Redis里直接验证token如果请求头带了token但后端仍然返回401那么下一步就是查Redis。我当时的做法是复制登录响应体里的token到redis-cli里执行KEYS *然后再执行TTL login:token:xxxx如果这个key不存在说明登录时就没写进去或者写进去的key前缀跟查询时不一致。如果key存在注意看value。这里有个细节你可能遇到登录接口返回的token是脱敏后的还是原始token如果在前端代码里自己拼了一个token比如把token从uuid截取了前8位那后端自然查不到。黑马点评这类项目里前端应保持后端返回什么就存什么的原则不要对token做任何加工。我在这一步还养成了一个习惯登录成功后第一时间在Redis里截图当前key的TTL等操作几分钟后再查一次TTL。如果TTL没有变化或者反而缩短了说明刷新过期时间的代码根本没执行那后面就会出现登录态突然失效的问题。3.3 第三步比对拦截器的路径匹配规则Redis没问题后我把注意力放到Interceptor配置上了。登录跳转主页失败时最容易踩的就是excludePathPatterns和addPathPatterns写错。我当时差点忽略的一点是主页需要的资源不止一个接口它可能需要CSS、JS、图片等。如果一个页面里的静态资源被LoginInterceptor拦截浏览器加载不出样式虽然地址栏是主页路径但内容看起来完全没加载出来观感上就像跳转失败了。对比方法很简单把loginInterceptor的excludePathPatterns里关于静态资源的规则列出来确认是否覆盖了常见的/js/**、/css/**、/img/**或者/**/*.js等。如果你用的是Nginx或代理还要确认代理后的路径是否仍匹配。这里有一个更稳妥的思路把静态资源全部交给Nginx或Vite脚手架直接处理后端接口只关心/api/**这类动态路径避免静态资源和业务接口混在一起互相影响。3.4 第四步走读前端token的存取时机前端的问题往往表现在页面刷新后。黑马点评项目的前端如果用了Pinia来管理用户信息登录后把token保存到了Pinia的state里那刷新页面后Pinia会被重新初始化token就丢了。虽然路由守卫从Pinia里取不到token会跳回登录页但此时用户明明刚登录过。我当时在代码里看到的是const token userStore.token;登录后路由跳转时能取到所以第一次跳转没问题但只要页面一刷新token就没了。而axios拦截器也从同一个store取token所以刷新后主页接口必然401。修复方法是落一个localStorage持久化登录成功时不仅要写store还要写localStorage路由守卫和axios拦截器都改成从localStorage读取页面初始化时再恢复store。这样即使刷新页面登录态依然存在也符合大多数真实项目的做法。3.5 第五步检查路由守卫的死循环和默认值路由守卫是跳转后又被拉回登录页的高发地。常见的三种写法问题beforeEach里判断了to.path ! /login但在next跳转时没有结束执行代码会继续走到下面的分支导致守卫逻辑混乱。meta.requiresAuth没有默认值任何未声明requiresAuth的路由都被当作需要登录主页也被拦。守卫里判断的是user store而不是tokenstore在页面刷新后本来就为空于是无论token是否有效都会被拉回登录页。面对跳转主页问题建议把路由守卫设计成只判断token是否存在不要判断用户信息是否完整。用户信息是否从后端拉取成功属于主页组件自己的职责不应该成为路由跳转的障碍。这就像你进商场只需要看有没有会员卡不需要保安先翻一下你的购物车。4. 被锁定的三个实际根因与修复方案4.1 根因一登录接口返回的token被前端加工了第一个项目里的问题是前端封装登录请求时写了一段const token res.data.token.substring(7)本意是想去掉Bearer 前缀但后端返回的token本身就没有前缀结果存进localStorage的是一个残缺字符串。修复方案也很直接前后端约定好token格式不要在前端做过多的字符串处理。后端统一返回Result.ok(token)前端直接存res.data.data。如果想用Bearer前缀也应该由axios拦截器在设置请求头时统一添加而不是在存储时处理// 推荐做法axios拦截器统一添加 config.headers[authorization] Bearer localStorage.getItem(token);这样做的另一个好处是以后如果后端换token方案前端只需要改一处拦截器不用每个页面都去改存储逻辑。4.2 根因二刷新token的拦截器没有注册第二个项目里我把LoginInterceptor注册了但RefreshTokenInterceptor没有注册。页面请求主页接口时一看UserHolder为空直接401。这类问题的排查最坑的点在于登录接口虽然是放行的但它依赖的Redis写入是正常的所以你不会第一时间怀疑拦截器。可一旦进入主页所有接口都被拦现象就是跳转后一片空白。修复方案是在WebMvcConfigurer里注册两个拦截器并且指定order顺序registry.addInterceptor(refreshTokenInterceptor).addPathPatterns(/**).order(0); registry.addInterceptor(loginInterceptor).addPathPatterns(/**).excludePathPatterns(...).order(1);注册完后后端日志里如果能看见RefreshTokenInterceptor的preHandle日志说明生效了。我建议在调试阶段给两个拦截器都加上日志输出因为有没有进入拦截器和拦截器里有没有数据是两回事必须分开验证。4.3 根因三Redis的过期策略导致跳转后token丢失第三个案例更隐蔽登录接口确实写入RedisTTL设置为30分钟前端第一次访问主页也成功。可没过多久用户再点击主页里的子页面时又被弹回登录页。原因在于RefreshTokenInterceptor虽然有刷新过期时间的代码但刷新逻辑写在了判断UserHolder.getUser() null之后导致用户信息为空时跳过刷新。实际上只要Redis里能查到token就应该刷新TTL。正确写法是这样的public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(authorization); if (StrUtil.isBlank(token)) { return true; } String key LOGIN_USER_KEY token; Object user stringRedisTemplate.opsForValue().get(key); if (user ! null) { UserHolder.saveUser(JSONUtil.toBean((String) user, UserDTO.class)); // 每次请求都刷新过期时间 stringRedisTemplate.expire(key, 30, TimeUnit.MINUTES); } return true; }注意这里的拦截器无论有没有token都返回true真正的拦截判断交给LoginInterceptor做。这个设计思路很多人没转过弯来导致写完刷新逻辑后发现所有接口都不拦截了又改成在拦截器里直接return false结果所有接口都被拦。正确的分工是RefreshTokenInterceptor只看token有没有、用户存不存在不去决定要不要拦截LoginInterceptor才做最后的拦截决策。4.4 顺带说一个跨域与代理的隐蔽原因如果你用了前端代理转发比如Vite的proxy把/api前缀转发到http://localhost:8080而后端Controller里写的路径不带/api就会出现登录请求成功但主页请求404的现象——页面地址是主页内容却加载不出来。此时不一定会弹回登录页因为404不会触发401的response拦截逻辑表现更像是页面白屏。我当时花了将近一个小时才排查到因为所有登录相关的链路都正常只有URL路径在代理层多了一段。修复办法建议统一路径风格要么后端Controller带/api前缀要么前端proxy把/api重写到空字符串只取一种约定。我见过更多人采取后者因为它不用改后端代码路径proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } }这个例子也说明跳转主页的问题并不总是出在登录态本身有时候是看起来像登录问题的代理问题。排查时不要被现象带偏要顺着请求链路一直看下去。5. 修复后的验证清单与后续开发建议5.1 五分钟跑通验证流程修复完代码不要只点一次登录就判断好了。我建议按下面的清单走一遍登录页面输入账号密码提交成功观察Network面板中有POST /user/login响应体里有token。打开redis-cli确认login:token:xxx存在TTL在正常范围内。前端页面跳转到主页打开Network面板确认主页的所有接口请求头都携带authorization字段。按F5刷新页面确认刷新后路由没有跳回登录页。停留在主页超过1分钟再随便点击进入一个页面确认token过期时间被刷新了可以对比操作前后的TTL值。这5步里第4步是最容易出问题的。很多登录后跳转主页正常但一刷新就失败的现象都会被这一步暴露出来。如果第4步就挂了直接回看前端的token存储方式基本是Pinia没有持久化到localStorage。5.2 在后端日志中留下拦截器的痕迹排查这类问题最忌讳的是盲调。与其反复改代码重启服务不如先在拦截器里加上几行日志System.out.println(登录拦截器: request.getRequestURI() - UserHolder.getUser());生产环境不建议用System.out.println但项目调试阶段这是最快的方式。日志会告诉你两件事拦截器到底有没有进入、UserHolder里到底有没有数据。当你看到主页接口的日志打印出用户信息而前端还是401那就该怀疑Response写回的编码问题或者axios响应拦截器误判了。日志里还有一个值得留意的字段是request.getHeader(authorization)是不是null。如果null说明前端压根没把token带到后端来如果不是null但有值再比对Redis里的key。这样一条链路走下来错误范围就能缩到很小。5.3 后续做登录功能时我会怎么设计经历过这次问题我后续再做类似项目时会把登录态设计收敛成一套固定模式后端统一用常量类维护Redis的key前缀禁止在不同类里各自写字符串。拦截器分为刷新解析和校验鉴权两层两层各司其职前者只负责把用户信息塞进ThreadLocal后者只管是否需要登录。token存取统一放在前端的一个modules/auth.js里所有组件和axios只通过这个模块读写token。路由守卫不依赖store中的用户信息只依赖localStorage中的token。登录后的第一个页面接口在首页组件挂载时就发起请求并把401处理下沉到response拦截器里。这样设计之后登录跳转问题基本就只剩下后端Redis宕机、前端代理配置错误这类基础设施问题了。最后分享一个我现在还在用的习惯每次改完登录相关代码都会打开redis-cli一遍登录一边盯着KEYS。看到key出现、TTL被刷新、再看到首页接口返回200心里才踏实。这个习惯帮我少踩了很多坑。如果你现在也被登录后跳转主页失败这件事卡住不用急顺着这条链路一点一点查总会找到那个被你忽略的细节。