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

有用的话点个赞。

相关推荐
晚安日记wanna1 小时前
压测 TPS 卡在 800CPU 只跑四成六步定位法
运维·面试·测试
aixingpan1 小时前
aixingpan.cn API开发文档:api_docs_bichart_natalvssolararc2接口指南
前端·php
无限压榨切图仔1 小时前
一个优惠券需求改了三端,我才明白全栈不是多学一门语言
前端·后端
晚安日记wanna1 小时前
批量请求失败只弹一个 Toast面试官想听五层
前端·面试·架构
晚安日记wanna1 小时前
TS const 类型参数一道面试题的四层追问
前端·面试·typescript
晚安日记wanna1 小时前
大表 DDL 面试翻车现场Online DDL 为什么还会锁死业务
数据库·面试·架构
用户921080262861 小时前
前端 Vue 专栏 09:Vue Router 与前端路由
前端
晚安日记wanna1 小时前
MySQL 主从延迟别只答并行复制
数据库·面试·架构
SamDeepThinking1 小时前
Java 17内存屏障:为什么需要,怎么用
java·后端·面试