记一次前端登录白屏问题排查:RSA 密钥时序竞态导致的服务端校验失败
1. 问题现象
生产环境中,部分用户反馈打开门户系统登录页后,扫码登录成功或手动刷新页面时会出现白屏,页面无任何报错信息,F12 控制台也无明显错误。而同一浏览器版本的其他用户使用正常,问题具有偶发性,难以稳定复现。
2. 排查思路
面对这种"部分用户出问题、大部分正常"的场景,排查思路如下:
确认现象 → 后端日志定位异常时间点 → 分析请求链路 → 前端代码验证 → 定位根因2.1 后端日志定位
首先在网关/后端服务日志中,以出现白屏的时间窗口为线索过滤请求记录。关键发现如下:
| 时间戳 | 线程 | 事件 |
|---|---|---|
08:10:55.089 | exec-293 | 登录接口返回成功,token 已写入缓存 |
08:10:55.097 | exec-293 | 写入登录审计记录 |
08:10:55.155 | exec-307 | /core/service请求异常 |
可以看到登录本身是成功的(线程 exec-293),但紧接着仅 66ms 后,另一个请求(线程 exec-307)就触发了异常。
异常堆栈定位到RefreshTokenFilter.java:
// RefreshTokenFilter.java:119StringRSAPrivateKey=(String)uCache.get(RSAPublicKery,true);if(RSAPrivateKey==null){thrownewEcpSysException("invalid_key,nonexistent!!");}逻辑很清晰:前端请求/core/service时携带了一个 RSA 公钥标识,后端去缓存中查对应的 RSA 私钥,查不到就直接抛异常。
问题来了:为什么登录成功了,RSA 密钥却不在缓存里?
2.2 分析请求链路
梳理正常流程的请求时序:
正常情况下,步骤 3 的密钥交换在步骤 4 的业务请求之前完成,所以私钥已经在缓存里了。
但实际出问题的时序是:
核心矛盾:location.reload()之后,业务请求/core/service抢在了 RSA 密钥交换接口的前面。
2.3 前端代码验证
带着上述假设去查前端源码,很快确认了两处缺少等待机制:
路由守卫(router/nportal.js):
// 问题代码:dispatch 后没有 await,fire-and-forgetstore.dispatch('GetRsaPublicKey').then()// 紧接着就放行路由,页面开始渲染,业务组件立即发起请求next()路由守卫触发了GetRsaPublicKey的 dispatch,但没有等它完成就放行了。如果这个接口响应慢,业务组件的加密请求就会先到达后端。
登录成功回调(Login.vue):
// 问题代码:存完 token 后直接 reload,没有任何等待LoginRear:function(d){// ... 存 token ...location.reload()// 立即刷新,不等密钥就绪}两处代码的共同点:都是 fire-and-forget 模式,发起异步操作后不等待结果就继续往下走。
2.4 为什么只有部分用户出问题?
这是一个典型的时序竞态(Race Condition)问题,能不能触发取决于网络延迟:
- 网络延迟低:密钥交换接口 50ms 就返回了,
location.reload()后页面加载再发业务请求至少需要 100ms+,此时密钥已在缓存中,不会出问题 - 网络延迟高:密钥交换接口需要 200ms+,而页面 reload 后业务请求可能 100ms 就发出去了,此时密钥还没写入缓存,触发异常
大多数用户网络条件好,时序上碰巧避开了这个坑;网络稍慢的用户就会稳定复现。
3. 修复方案
修复原则:只在 key 不存在时才增加等待,已有 key 时零延迟,不影响正常流程。
3.1 路由守卫修复
// 修复后:确保 RSA 公钥就绪后再放行路由awaitstore.dispatch('GetRsaPublicKey')next()3.2 登录回调修复
// 修复后:async 函数,reload 前兜底检查 keyLoginRear:asyncfunction(d){// ... 存 token ...// 兜底:如果 RSA 密钥尚未就绪,等待它完成if(!store.state.rsaPublicKey){awaitstore.dispatch('GetRsaPublicKey')}location.reload()}修复前后对比
| 场景 | 修复前 | 修复后 |
|---|---|---|
| 正常用户(低延迟) | 碰巧不出问题 | 行为不变,零额外等待 |
| 慢网络用户 | 稳定白屏 | 等待密钥就绪后再 reload,不再白屏 |
| 极端情况(密钥接口超时) | 白屏 | 会被接口自身的超时/错误机制捕获 |
4. 经验总结
| 维度 | 要点 |
|---|---|
| 排查策略 | 先从后端日志锁定异常时间点和线程,再逆推前端请求链路,比盲目在前端打断点高效得多 |
| 问题本质 | 时序竞态(Race Condition)—— 多个异步操作的完成顺序不确定,在特定条件下会暴露 |
| 代码规范 | 关键异步操作必须 await,不能 fire-and-forget,尤其是在涉及安全凭证(token、密钥)的场景 |
| 影响面判断 | “只有某个人出问题” ≠ “个性化问题”,很可能是环境差异触发了共性 bug,需要从代码层面根治 |
| 前端安全链路 | 登录 → token 存储 → 密钥交换 → 业务请求,这条链路上每个环节都应串行等待,避免竞态 |
这类问题在生产环境中往往表现为"偶发性白屏"或"部分用户反馈异常",排查难度不高但容易被忽视。建议在登录流程等关键链路上建立完整的异步等待机制,从源头杜绝竞态风险。