Next.js App Router 将用户数据页面误判为静态页面并缓存,导致不同用户看到彼此的私有数据;即使 getCurrentUser 正确使用了 cookies(),若后续数据获取路径不被识别为动态信号,仍可能触发缓存。
这是我在 Next.js 中遇到的最可怕的一类 bug,不是因为它有多特殊,而是因为它真的很容易在不知不觉中引入。一旦发生,症状就是一个用户看到了另一个用户的真实数据。不是崩溃,不是报错,而是一个真正的隐私泄露——从外表看,应用似乎一切正常。
Next.js 会在可能的情况下积极地将页面静态渲染,生成一次 HTML,然后向每个访问者提供相同的缓存 HTML,这对于真正的静态内容来说对性能非常好。App Router 根据是否检测到使页面变为动态的因素(读取 cookie、读取 header、以某些方式使用 searchParams)来决定页面是否可以静态渲染。
危险在于,当一个页面通过 Next.js 不识别为动态信号的路径读取用户特定数据时。
// app/dashboard/page.tsx
export default async function DashboardPage() {
const user = await getCurrentUserFromSomewhere(); // how is this actually getting the user?
return <h1>Welcome back, {user.name}</h1>;
}
如果 getCurrentUserFromSomewhere 通过 cookies() 正确读取了 session,Next.js 会检测到这一点,并正确地将该路由标记为动态,每次请求都会重新渲染,从不缓存。但如果该函数或其调用的任何内容从 Next.js 不识别为动态信号的地方读取用户身份——比如在其他地方设置的全局变量、从上一个请求缓存的值、一个配置错误的 fetch 调用对应该是用户特定的数据使用了 cache: 'force-cache'——那么该页面会被静态生成一次,将一个特定用户的数据直接烘焙到 HTML 中,然后原封不动地提供给每个后续访问者。
// lib/queries/user.ts
export async function getDashboardData(userId: string) {
// Explicitly opted into caching, on data that's actually user-specific
const res = await fetch(`https://api.example.com/dashboard/${userId}`, {
cache: 'force-cache',
});
return res.json();
}
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
export default async function DashboardPage() {
const cookieStore = await cookies();
const userId = cookieStore.get('userId')?.value;
const data = await getDashboardData(userId as string);
return <Dashboard data={data} />;
}
在这里读取 cookies() 确实正确地将页面标记为动态。但 getDashboardData 内部的 fetch 调用用 cache: 'force-cache' 明确覆盖了缓存行为,而且这个特定的 fetch 结果可能会被缓存并在不同用户对同一路由的请求之间重复使用——如果 URL 恰好相同,或者缓存层的作用域没有开发者假设的那么严格。确切的机制取决于部署方式,但根本错误是相同的:对返回用户特定数据的 fetch 明确强制使用缓存。
在本地以自己的身份测试时,一切看起来完全正确。每次都能看到自己的数据,因为你是唯一的测试用户,缓存填充的时机与你自己请求的时机很少会暴露重叠。这个 bug 通常在生产环境中暴露,在真实并发流量下,当用户 A 首先加载 dashboard 时获得新鲜数据,而用户 B 片刻后加载时却获得用户 A 的缓存响应而不是自己的。没有人看到错误。页面只是安静地显示了另一个人的信息。
任何真正与用户相关的数据都不应该应用 cache: 'force-cache',无论是显式应用还是从共享的 fetch wrapper 继承默认值。如果一个 fetch 调用的结果取决于请求者是谁,缓存行为需要反映这一点。
// ❌ Force-caching something that varies per user
const res = await fetch(url, { cache: 'force-cache' });
// ✅ Explicitly no caching for user-specific data
const res = await fetch(url, { cache: 'no-store' });
// ✅ Or, if using a direct database query instead of fetch, use React's cache()
// for per-request deduplication only, never a persistent cache like unstable_cache
import { cache } from 'react';
export const getUserData = cache(async (userId: string) => { ... });
no-store 明确告诉 Next.js 这个 fetch 永远不应该被缓存,句号,每次都新鲜获取,不管周围发生什么。对于直接数据库查询,这正是为什么请求作用域的 cache()(在每个新请求时重置)是用户特定内容的 safe default,而 unstable_cache(跨请求持久化)应该专门保留给对每个人都真正相同的数据,绝不是任何针对个人用户的数据。
任何读取用户身份的地方都应该显式地从 cookies()、headers() 或经过身份验证的 session 中读取,而不是从隐式的或共享的东西中读取。这正是让 Next.js 最初正确地将路由检测为动态的原因。
任何返回用户特定数据的 fetch 调用都应该使用 cache: 'no-store' 或 next: { revalidate: 0 },永远不要 force-cache,也不要在未检查的情况下从共享 wrapper 继承默认缓存配置。
任何返回用户特定数据的直接数据库查询最多使用 cache() 进行按请求去重,绝不要使用 unstable_cache,后者明确用于跨不同请求和不同用户持久化。
用两个不同的账号测试,而不仅仅是你自己的,特别检查切换账号是否曾经显示过前一个账号的陈旧数据。这是在实践中发现这个问题的真正方法,因为作为单个用户单独测试几乎永远不会暴露它。
如果你正在运行一个带有任何个性化 dashboard 的 Next.js 应用,真的用两个不同的账号登录,快速连续地测试它,并密切观察是否有任何数据似乎滞后或短暂显示错误账号的信息。如果你在生产环境中遇到过这个问题,我真的想听听它是如何暴露的,可以在评论区留言。
Get the templates: https://pixelanas.gumroad.com
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751