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 缓存串号的坑没有,评论区聊聊。

有用的话点个赞。

相关推荐
小呆呆6667 小时前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
Dovis(誓平步青云)7 小时前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
lsc_blog8 小时前
远程和居家办公的经历,技术简历上怎么摆
面试·职场和发展·重构·pdf·求职招聘
码艺-Alimjan8 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_8 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒8 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技9 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
时间的拾荒人9 小时前
Qt 事件机制全解析:从事件处理到事件过滤器
开发语言·qt·面试
yivifu9 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
默_笙10 小时前
🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)
前端·javascript