退出后重登卡在"登录中":一次靠 Network 面板定位的 NextAuth session 时序问题
用户反馈退出登录后再登录会卡在"登录中"。后端 curl 全链路正常,但 Network 面板显示登录成功后根本没有请求 /api/auth/session——signIn(redirect:false) 后 useSession 没刷新,status 卡在 unauthenticated。修复:成功后主动调用 update()。
退出后重登卡在"登录中":一次靠 Network 面板定位的 NextAuth session 时序问题
用户反馈:退出登录后,再次用账号密码登录,会卡在"登录中"很久。听起来像是"登录慢",但真实原因和"慢"没关系——是登录成功了,但前端永远不知道。
现象
- 首次登录:正常,秒进。
- 退出后再登录:点完"登录 / 注册",按钮转圈显示"登录中…",然后……就停在那了。没有报错,也不跳转,能一直等到地老天荒。
- 小应账号快捷登录:正常。只有账号密码登录卡。
第一层:后端真的没问题
先怀疑后端。用 curl 模拟完整链路:登录 → 拿 session cookie → 调用 /api/auth/signout 清 cookie → 再登录。
结果一路正常:
首次登录: 302 + set-cookie session-token
退出: signout 302 + session-token 被清
重登: 302 + 新 session-token
后端响应毫秒级,密码校验、验证码、JWT 签发全对。问题不在服务端。
第二层:看请求序列,真相浮出
用浏览器 DevTools 的 Network 面板(或者抓开发服务器的访问日志),对比"正常登录"和"卡住登录"两次的请求序列:
正常登录:
POST /api/auth/callback/credentials → 302(成功,签发 cookie)
GET /api/auth/session → 200(前端重新拉取 session)
(随后跳转 /)
卡住的登录:
POST /api/auth/callback/credentials → 302(成功,cookie 已签发!)
(然后……就没了)
没有 GET /api/auth/session! 后端已经成功签发 session cookie,但前端没有重新拉取 session 状态。于是 useSession() 的 status 一直停留在 unauthenticated,登录页里那个"等 status === 'authenticated' 就跳转"的 useEffect 永远不触发——按钮就永远停在"登录中"。
根因:signIn(redirect: false) 的 session 刷新时机
代码里用的是:
const res = await signIn('credentials', { ..., redirect: false })
redirect: false 是为了自己在 SPA 里拿到结果、手动处理错误。但代价是:NextAuth 客户端在 redirect: false 场景下,成功登录后不会可靠地触发 useSession 的刷新。
最典型的就是"退出后同一会话内再次登录":SessionProvider 已经把状态缓存成 unauthenticated,signIn 内部对 session 的更新在这个路径上会失效(这是 NextAuth v4 的已知行为/坑)。首次登录往往没事,是因为页面刚加载、SessionProvider 恰好触发了一次初始 fetch,把新状态带了出来——纯属时机巧合,掩盖了 bug。
而小应快捷登录为什么没事?它走的是 window.location.href = '/api/auth/xiaoying/start' 全页跳转,OIDC 回调完成后再整页重载,useSession 重新初始化,天然绕开了这个时序问题。
修复:登录成功后主动刷新 session
NextAuth 的 useSession() 返回值里有一个 update() 方法,作用就是强制重新从服务端拉取 session。在 signIn 成功分支里显式调用它,再跳转:
const { status, update } = useSession()
const res = await signIn('credentials', { ..., redirect: false })
if (res?.error) {
setError(res.error)
fetchCaptcha()
setIsLoading(false)
} else {
// signIn(redirect:false) 后 useSession 可能不自动刷新(尤其退出后重登)
const newSession = await update() // 强制重取 session
router.push(newSession?.user ? (callbackUrl || '/') : '/auth/signin')
}
update() 会触发 GET /api/auth/session,拿到刚签发的 session 后,status 变成 authenticated,跳转自然发生。update() 万一抛错,兜底直接 router.push,让服务端鉴权在目标页接管——不会再次滞留"登录中"。
经验
- "慢"未必是慢。登录"卡住"大概率不是网络/数据库慢,而是状态没更新。先看请求序列,后端 302 成功 + 前端无后续请求,基本就是客户端状态问题。
signIn(redirect: false)要配update()。一旦选择手动控制跳转,就得自己负责刷新 session,别指望useSession自动帮你。- 全页跳转的登录方式天然免疫这类 bug。OIDC/整页重载每次都会重新初始化 session,SPA 内的
signIn才是重灾区。 - Network 面板是排障第一现场。不用猜,看一眼有没有
GET /api/auth/session就知道前端知不知道登录成功。