
成为全栈·Next.js 网站前台篇·会员中心资料、密码与个人数据为什么不能进公共缓存会员中心不是在公开页面外面套一个登录判断。它还要等待会话恢复、隔离每个账号的查询缓存、禁止搜索索引并让修改资料与修改密码产生不同的会话后果。前言公开文章允许短暂旧值会员资料却不能“缓存 60 秒再说”。一旦 A 用户的响应被公共缓存复用给 B问题就从内容新鲜度升级成隐私事故。当前项目把会员中心放在/member/*页面外壳由 App Router 提供但数据仍在浏览器会话恢复后通过私有 API 获取。这个结构并不是因为 CSR 天然安全而是为了让内存 access token、当前账号和交互查询处在同一客户端生命周期里。会员区有三层边界层次解决的问题不能替代什么MemberShell客户端守卫等待恢复、未登录跳转、避免内容闪现后端鉴权私有请求与查询键不走公开缓存、按用户隔离状态资源归属校验后端权限检查验证 token、角色和数据所有者前端体验状态客户端守卫可以被绕过所以“看不到页面”不等于“拿不到数据”。后端仍必须对/me/*和稿件接口做最终裁决。先等启动恢复完成再决定是否跳登录页面刷新后内存 access token 必然丢失但 HttpOnly Cookie 可能还有效。如果一看到user null就跳登录合法会员会先被踢走刷新请求随后才成功。export function MemberShell({ children }: { children: React.ReactNode }) { const { user, bootStatus } useAuthStore() const pathname usePathname() const router useRouter() useEffect(() { if (bootStatus ready !user) { router.replace(/login?redirect${encodeURIComponent(pathname)}) } }, [user, bootStatus, pathname, router]) if (bootStatus ! ready || !user) return Skeleton / return section classNamemembercontent{children}/section }这里有三个状态尚未恢复、已恢复会员、已确认游客。骨架只覆盖第一种过渡与跳转前的瞬间不会把“游客”误当成长期加载中。私有页面同时拒绝搜索索引export const metadata { title: 会员中心, robots: { index: false, follow: false }, }会员页面不进入 sitemaplayout 还输出 noindex。它们减少误抓取却不是安全控制。搜索引擎可以遵守指令恶意请求不需要遵守真正的数据保护仍是 API 鉴权与private, no-store。查询键必须包含用户身份const query useQuery({ queryKey: [profile, user?.id], queryFn: getMeProfile, enabled: !!user, })如果 key 只有[profile]A 账号退出后 B 账号登录组件可能先读到 A 的缓存。把 user id 放进 key再在身份变化时移除私有查询才能表达数据所有权。列表也遵循同一原则useQuery({ queryKey: [member, user?.id, kind, page, status], queryFn: () fetchCollection(kind, page, status), enabled: !!user, })用户、列表类型、页码和筛选状态共同决定一份结果。少一个维度都可能出现错误复用。资料更新要同步服务端状态与会话显示昵称既出现在资料页也出现在会员侧栏。更新成功后只刷新表单缓存侧栏仍会显示旧名称const update useMutation({ mutationFn: updateMeProfile, onSuccess: (data) { useAuthStore.getState().setUser(data) queryClient.setQueryData([profile, user?.id], data) setMessage(资料已更新) }, onError: (error) setMessage(error.message), })这里同时更新轻量会话状态和 React Query 中的资料实体。两者职责不同却需要在一次成功结果后保持一致。表单只提交契约允许修改的字段update.mutate({nickname:String(data.get(nickname)).trim(),...(String(data.get(email)).trim()?{email:String(data.get(email)).trim()}:{}),})用户名在界面中禁用但不能因此省略后端字段白名单攻击者可以绕过表单直接构造请求。修改密码成功后必须结束旧会话await changePassword({ oldPassword: String(data.get(oldPassword)), newPassword: String(data.get(newPassword)), }) form.reset() useAuthStore.getState().clear() queryClient.clear() window.location.assign(/login?expired1)修改昵称可以保留登录修改密码则需要清空内存令牌和所有私有查询重新登录。这里使用完整页面导航让会话恢复与页面状态从干净环境重启。客户端会先检查两次新密码一致if(data.get(newPassword)!data.get(confirm)){setMessage(两次输入的新密码不一致)return}但密码强度、旧密码正确性和会话撤销仍由后端决定。前端校验只提供即时反馈。私有请求需要双向 no-store客户端请求层统一使用awaitfetch(/api/v1/me/profile,{headers:{Authorization:Bearer${accessToken}},cache:no-store,})同源 BFF 对浏览器响应再写入headers.set(cache-control,private, no-store)第一处阻止 Next.js 请求缓存复用上游数据第二处约束浏览器与中间缓存。它们仍不是鉴权只是避免合法响应被错误保存和共享。删除最后一项后要处理空页会员收藏、历史和点赞共用列表控制器。若用户在第 3 页删除唯一一项当前页将不存在useEffect(() { if (query.data !query.data.items.length page 1) { setPage(page - 1) } }, [query.data, page])操作成功还要失效相关状态awaitqueryClient.invalidateQueries({queryKey:[member]})awaitqueryClient.invalidateQueries({queryKey:[favorite-ids]})awaitqueryClient.invalidateQueries({queryKey:[like]})这样详情页按钮和会员列表不会长期互相矛盾。错误、空数据和未登录是三种界面状态界面行为恢复动作会话仍在恢复骨架等待一次启动恢复已确认未登录跳转登录并保留 redirect登录后返回私有请求失败错误提示重试当前请求请求成功但列表为空空状态浏览文章或开始投稿密码修改成功清会话使用新密码登录验收要使用两个账号1. A 登录后打开资料、收藏和历史 2. 退出 A登录 B不刷新页面继续访问会员中心 3. 检查 B 看不到 A 的昵称、列表和请求缓存 4. 刷新页面确认 HttpOnly Cookie 可恢复 B 会话 5. 修改密码确认旧会话清空并回到登录页 6. 直接请求 A 的资源编号确认后端拒绝越权 7. 检查所有 /me 响应含 private, no-store 8. 检查会员页面不在 sitemap 且带 noindex只用一个账号无法证明隔离只点页面也无法证明 API 权限。会员中心必须把界面、缓存、响应头和后端授权一起验收。适用边界当前会员区采用客户端取数适合交互密集且依赖内存令牌的页面。如果需要服务端直接渲染私有内容可以让 BFF 代持会话但缓存键、Cookie 和每请求用户隔离会更复杂。对于金融、医疗等高敏感信息还需要更严格的重新认证、操作审计、设备管理与内容遮罩本文的普通内容会员模型不够。小结会员中心的关键不是做一套侧栏导航而是确保每份数据都属于当前账号会话恢复完成后再请求查询键带用户 id身份切换清私有缓存响应始终 no-store后端继续检查资源归属。公开内容可以用缓存换速度个人数据必须先保证身份和边界正确。这是两类页面最根本的工程差异。延伸阅读C 端认证内存令牌、HttpOnly Cookie 与会话代次如果这篇文章对你有帮助欢迎订阅我的 CSDN 专栏「成为全栈」 专栏地址https://blog.csdn.net/fungleo/category_13204651.html 本系列配套代码仓库https://github.com/fengcms/become-a-full-stack-developer