SSR 为什么不适合登录态从水合冲突到缓存串号

SSR 阶段拿不到 localStorage,服务端也没有记忆,水合迟早和客户端对不上。强登录页走 CSR;SSR 只渲染公共内容,凭证校验压到中间件。

面试官追问的那一句,才是考点

SSR 适不适合处理登录态?为什么?

大多数人答到"服务端拿不到 localStorage"就停了。这话没错,但只对一层。面试官跟一句"Cookie 不是能传到服务端吗",就接不住了。

难点从来不是传。是传过去之后,服务端和客户端怎么保持一致。

六条高频答案,第 2 条最要命

常见回答 实际情况
SSR 完全做不了登录态 能做,但通道极窄:只能靠 Cookie,且不能缓存
Cookie 没法把凭证传到服务端 同源请求自动携带,通道本身没问题
登录态不一致只是视觉闪烁 会直接触发水合报错,框架丢弃整段 DOM 重建
localStorage 服务端也能读 Node 里根本没有这个全局对象
刷新页面就能解决 服务端缓存不清,刷十次还是串号
登录态错乱跟水合没关系 水合只是矛盾的出口,根因是两套状态源

第 2 条最要命:Cookie 能传,所以"拿不到"不是答案。答案是"拿到了也难对齐"。

差别不在能不能拿到,在拿到的对不对得上

SSR 页面在 Node 侧提前跑一遍组件树、吐出 HTML,客户端 JS 到位后再 hydrate 一次接管。

两套环境、两套输入、两次渲染。登录态只要参与其中一次,就有对不齐的可能。

存储不在同一个世界

这是最直观的一刀,也是所有适配问题的起点。

存储方式 浏览器可读 Node 服务端可读 能否承载登录凭证
localStorage 是 否 否
sessionStorage 是 否 否
IndexedDB 是 否 否
普通 Cookie 是 需解析 req.headers.cookie 可以
HttpOnly Cookie 否 需解析请求头 推荐

SSR 阶段唯一可信的凭证来源是请求头里的 Cookie。 把 token 塞进 localStorage 的那套方案,在 SSR 页面里等于不存在。

Next.js 里拿到它的方式是 cookies().get('token'),Express 里是 req.headers.cookie 手动解析------都绕不开"客户端存、服务端读不到"这条鸿沟。

水合不一致不是概率问题,是必然

服务端按 Cookie 渲染出带用户信息的页面,客户端 hydrate 时发现本地状态对不上,事情就炸了。

sequenceDiagram participant B as 浏览器 participant S as Node服务端 participant A as 鉴权服务 B->>S: GET user 携带 Cookie S->>A: 校验 token A-->>S: 有效 返回用户信息 S-->>B: SSR HTML 已含用户名头像 Note over B: 用户信息已经可见 B->>B: hydrate 读取 localStorage Note over B: localStorage 无 token 判定未登录 B-->>B: 重渲染为未登录 UI Note over B: 用户信息闪一下消失 触发水合报错

React 18 的报错原文是 Hydration failed because the initial UI does not match what was rendered on the server.,Vue 3 会抛 Hydration children mismatch / Hydration node mismatch。

关键在于框架的处理方式:它不会"帮你补一下",而是把这段子树丢掉重新渲染。 用户看到的就是头像昵称闪一下消失。

反向也一样成立:token 只存在 Cookie 里,服务端认为已登录,客户端某个 useEffect 里读 localStorage 判定未登录,照样炸。

服务端没有记忆

每次请求都是一次全新的渲染流程。Node 进程不持有任何用户的会话状态,它没法"监听"客户端点了退出登录。

后果很直接:

  • 用户在客户端退出登录,服务端缓存里那份 HTML 还是登录态
  • token 过期了,客户端已经登出,SSR 缓存还在吐带用户名的页面
  • 服务端只能靠每次请求重新校验,一旦缓存介入,校验就被绕过了

线上真会出什么事

事故一:SSR 缓存串号

用户 A 的 HTML 被 CDN 或进程内缓存命中,返回给了用户 B。

flowchart LR U1[用户A请求] --> N[Node服务端] U2[用户B请求] --> N N --> C[SSR页面缓存 key只用路径] C --> R[同一份HTML返回给两个人] R --> X[登录态串号]

B 打开页面看到的是 A 的昵称、A 的订单数,严重的时候是 A 的手机号。这类缓存通常挂在 renderToString 结果或 ISR 产物上,key 只用了路径,没带用户维度。

事故二:半登录页面

头部显示已登录,点进详情接口全是 401。用户不知道该重新登录还是刷新页面------因为页面自己也不知道。

事故三:切换账号时的反复闪烁

A 退出、B 登录,同一路由下服务端缓存和客户端状态来回打架,页面反复重渲染,控制台一路水合警告。

四条落地规矩

该不该让这个页面参与 SSR,其实是个决策问题:

flowchart TD A[页面请求] --> B{强依赖登录态?} B -->|是| C[CSR 客户端渲染] B -->|否| D[SSR 服务端渲染] D --> E[中间件校验 token] E -->|未登录或已过期| F[302 重定向到登录页] E -->|校验通过| G[渲染页面骨架与公共内容] G --> H[客户端 hydrate 后二次校验] C --> I[骨架屏 + 客户端拉取用户信息]

强登录页别碰 SSR

个人中心、订单、会员权益这类页面,登录态高频变更,直接从根上走 CSR。

tsx 复制代码
'use client'
export default function ProfilePage() {
  const { data: user, isLoading } = useSWR('/api/me', fetcher)
  if (isLoading) return <ProfileSkeleton />
  if (!user) return <LoginPrompt />
  return <Profile user={user} />
}

代价是首屏多一个骨架屏,换来的是零水合风险和零缓存风险。

校验必须在渲染之前

ts 复制代码
// middleware.ts
export function middleware(req: NextRequest) {
  const token = req.cookies.get('token')?.value
  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url))
  }
  try {
    verifyJwt(token) // 过期会抛错
    return NextResponse.next()
  } catch {
    const res = NextResponse.redirect(new URL('/login', req.url))
    res.cookies.delete('token')
    return res
  }
}

export const config = { matcher: ['/profile/:path*', '/orders/:path*'] }

不要渲染完无效页面再让客户端跳转,重定向必须发生在渲染之前。

带登录态的响应一律不许缓存

http 复制代码
Cache-Control: private, no-store
Vary: Cookie

private 禁止 CDN 缓存,no-store 禁止浏览器和中间层落盘,Vary: Cookie 让缓存 key 带上 Cookie 维度。

真要缓存,就只缓存不含用户信息的外壳,用户区留给客户端填。

水合之后再对齐状态

更彻底的做法是让登录态相关的 UI 在 mount 之后再渲染,SSR 输出的永远是骨架,从源头上消灭不一致:

tsx 复制代码
function UserBadge() {
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])
  if (!mounted) return <UserBadgeSkeleton />
  return <UserBadgeWithData />
}

服务端和客户端首帧输出完全一致,剩下的状态同步走接口,不再依赖 DOM 层面的"猜"。

SSR 还是 CSR,按页面分

维度 SSR 方案 CSR 方案
凭证来源 请求头 Cookie Cookie 或 localStorage
首屏内容 直接带用户信息 骨架屏
水合风险 高,两套状态源 无
缓存串号风险 高 低
适合页面 营销页、内容页、公共列表 个人中心、订单、会员

SSR 负责让公共内容变快,登录态交给客户端和中间件。

追问链

Q:Cookie 明明能传到服务端,难在哪? 服务端拿到的是一次性快照,客户端持有的是一份会变的状态。快照和状态对不齐,就是水合报错。

Q:那 SSR 页面里怎么显示用户名? 中间件校验 token 后把用户信息注入 props 或服务端上下文,SSR 一次性输出;hydrate 后不再二次判断登录态。

Q:客户端怎么感知 token 过期? 401 拦截器统一处理,清 Cookie 跳登录页;关键页面再叠一层中间件,请求进来就判掉。

一句话版本:SSR 能处理登录态的渲染,但处理不了登录态的变更。

写在最后

你们项目里的登录态是怎么处理的------老老实实全站 CSR,还是 SSR 加客户端二次校验?踩过 SSR 缓存串号的坑没有,评论区聊聊。

有用的话点个赞。

相关推荐
计算机魔术师24 分钟前
出题人自己算错了,AI成绩从88%直接跳到98%
前端
j7~36 分钟前
IT 面试指南:从准备到 Offer 的完整路线图(校招 / 实习通用
面试·职场和发展·秋招·春招·实习面试·it后端
Amos_Web42 分钟前
Rspack 源码解析(三十一):HotModuleReplacementPlugin 如何用 hash 比对生成增量更新
前端
尤水就下1 小时前
🔥说一说在线文档的 Block 架构是如何实现的
前端·javascript·架构
小淮AI1 小时前
面试前 48 小时,怎样练表达和复盘:一份个人准备记录
面试·职场和发展
是江迪呀1 小时前
介绍一个开源项目 —— 烂笔头
前端·后端
恋猫de小郭1 小时前
Flutter 紧急发布 3.47.8 安全升级版本,建议都升级
android·前端·flutter
紧跟先前的步伐1 小时前
Go AI Agent 实战 18|Tool Calling 的基本原理与完整执行流程
前端·数据库·人工智能·ai·golang·1024程序员节
考研保研资料分享1 小时前
双非转专业保研海军军医大学心理学:四校考核、学年论文与择校复盘
面试
captain3762 小时前
Spring Web MVC(2)
java·前端·spring boot·后端·spring·mvc