重新走一遍登录用了 20 秒,读懂那个 403 响应体用了半小时
早上打开自己写的一个控制台工具,页面照常渲染:logo、菜单、卡片全都出来了, 静态资源一条 200 都没少。但所有需要登录态的接口都返回了同一个状态码:403。
页面能开、接口全 403,这两件事同时成立的时候,最容易得出的结论是「网络抖动,重试一下」。 我重试了三次,403 一次都没变。状态码不变,说明这不是抖动。
403 的响应体里有两句不同的话
把响应体原样打出来,事情就清楚了。同一套接口,两种不同的 body:
// 第一种
HTTP/1.1 403 Forbidden
{"error_b_msg": "not login"}
// 第二种
HTTP/1.1 403 Forbidden
{"error_msg": "token not found"}
状态码一样,字段名不一样:一个是 error_b_msg,一个是 error_msg。 在我对接的这套体系里,这两个前缀对应不同的一层:
| 响应体字段 | 拒绝发生在 | 含义 |
|---|---|---|
error_b_msg |
网关/单点登录层 | 请求根本没带上有效的票据,网关直接挡掉 |
error_msg |
应用层 | 票据收到了,但应用侧拿不到对应的 token |
这个区别不是文字游戏。第一种情况,重新走一遍登录流程就能拿到新票据;第二种情况, 票是新的、但应用侧的 token 已经不是同一个了,这时候只重新登录、不清掉旧的会话数据, 下一次请求大概率还是 403——看起来像「登录没用」。
页面为什么还能开
这是我当时判断错的地方,值得单独记一笔。页面能渲染,只说明 HTML 和静态资源能拿到; 它们走的是另一个域名、另一套缓存策略,跟接口的会话 credential 不是一件事。 把「页面能打开」当作「会话有效」的证据,等于用 A 系统的可用性去推断 B 系统的登录态。
同理,反过来也不成立:接口 200 不代表页面没问题。判据只能落在同一个东西上—— 身份接口自己的返回。
两次重新登录的实际耗时
把时间戳记下来之后,这事的量级和我猜的完全不一样。从打开登录页到身份接口重新返回 200, 两次分别是 20 秒 和 21 秒。也就是说,登录本身根本不慢; 前面那半小时全花在「这到底是网络问题还是会话问题」的分辨上。
耗时分布大概是这样的:分辨现象半小时,动手修复二十秒。这个比例说明排查的方向找错了, 而找错方向的原因只有一个——我一开始没有把响应体打出来,只看了状态码。
现在这段判据写成了这样
不再问「页面还在不在」,直接问身份接口,并且要求返回 200 加上业务字段齐全:
const r = await fetch(API.me, { credentials: 'include' });
const text = await r.text();
if (r.status === 403) {
// 403 一定要把 body 留着,字段名比状态码有信息量
console.warn('403 body:', text);
return { state: 'expired', layer: text.includes('error_b_msg') ? 'sso' : 'app' };
}
if (!r.ok) return { state: 'error', status: r.status };
const data = JSON.parse(text);
return data && data.name ? { state: 'ok', name: data.name } : { state: 'unknown' };
这段代码里最没技术含量的一行其实是 console.warn('403 body:', text), 但它是最有用的:没有这一行,我连「有两种 403」这件事都不会知道。
可以拿走的三条
- 404/403 只是 HTTP 层的分类,真正的原因写在响应体里。抓 403 的时候把 body 原样留下来, 否则你面对的是一个只有状态码的黑盒。
- 「页面能渲染」和「会话有效」是两个独立事实。判断登录态只认身份接口:200 + 业务字段齐全, 两个条件缺一不可。
- 失效层不同,处置不同:网关层没票,重新走登录;应用层 token 缺失,得先清掉旧会话数据再登录, 不然重登也是空转。分清这一层,比多试几次有用得多。
- 点赞
- 收藏
- 关注作者
评论(0)