Next.js 用 cookies()/headers() 读请求信息:动态渲染触发、缓存失效与在 Server Action 里读写 cookie

在 Next.js App Router 里想读一下用户的登录 token,或者拿一下 User-Agent,你会发现 Server Component 里根本没有 req 对象可用。官方给的入口是 next/headers 里的 cookies()headers()。但很多人第一次用就踩坑:要么报「Dynamic server usage」构建失败,要么在 Server Component 里 cookies().set() 直接抛错。这篇把这两个函数的正确用法和背后的渲染机制讲透。

cookies()headers() 只能在服务端运行的代码里调用------Server Components、Server Actions、Route Handlers。注意 Next.js 15 起它们是异步 的,必须 await

tsx 复制代码
// app/dashboard/page.tsx ------ 这是一个 Server Component
import { cookies, headers } from 'next/headers'

export default async function Dashboard() {
  const cookieStore = await cookies()
  const token = cookieStore.get('token')?.value // 读单个 cookie

  const headerList = await headers()
  const ua = headerList.get('user-agent') ?? 'unknown' // 读请求头

  return (
    <div>
      <p>登录态:{token ? '已登录' : '未登录'}</p>
      <p>你的浏览器:{ua}</p>
    </div>
  )
}

如果你还在用 Next.js 14,cookies()/headers() 是同步的,不用 await。升级到 15 之后忘了加 await 会得到一个 Promise 而不是数据,.get is not a function 就是这么来的。

关键机制:调用它们会让整个路由变成动态渲染

这是最容易忽略、又最影响性能的一点。Next.js 默认会尽量把页面静态化 (构建时生成 HTML,请求时直接吐)。但 cookie 和 header 是每个请求都不一样的东西,一旦你在某个页面调用了 cookies()headers(),Next.js 就知道这个页面没法静态化了,会自动切成动态渲染(每次请求实时生成)。

tsx 复制代码
// 这个页面本来可以静态化,但因为读了 cookie,整页变成每次请求都渲染
export default async function Page() {
  const store = await cookies()
  const theme = store.get('theme')?.value
  return <html data-theme={theme}>...</html>
}

这不是 bug,是设计使然------读了随请求变化的数据,当然不能再用一份缓存的 HTML 糊弄所有人。但它意味着:别在本可静态化的页面里无脑调 cookies() ,否则白白丢掉静态化的性能红利。如果只有页面的一小块需要个性化,把读 cookie 的部分抽成子组件,用 <Suspense> 包住,让页面主体仍能静态化。

cookies() 返回的对象有 .set().delete(),但你在 Server Component 里调它们会得到:

复制代码
Error: Cookies can only be modified in a Server Action or Route Handler

原因是 Server Component 渲染时,HTTP 响应头可能已经开始发送了,没法再塞 Set-Cookie读 cookie 哪都行,写 cookie 只能在 Server Action 或 Route Handler 里。 这是硬性边界。

在 Server Action 里读写 cookie:一个登录例子

Server Action 是写 cookie 的正确场所。下面是一个最小的登录流程:表单提交 → 校验 → 种 cookie → 刷新页面。

tsx 复制代码
// app/login/actions.ts
'use server'

import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'

export async function login(formData: FormData) {
  const password = formData.get('password')

  // 这里用假校验示意,真实项目请对接你的鉴权服务
  if (password !== 'secret') {
    return { error: '密码错误' }
  }

  const cookieStore = await cookies()
  cookieStore.set('token', 'signed-jwt-here', {
    httpOnly: true,   // JS 读不到,防 XSS 窃取
    secure: true,     // 只在 HTTPS 下发送
    sameSite: 'lax',  // 防大部分 CSRF
    maxAge: 60 * 60 * 24 * 7, // 7 天
    path: '/',
  })

  redirect('/dashboard') // 种完 cookie 跳转
}
tsx 复制代码
// app/login/page.tsx
import { login } from './actions'

export default function LoginPage() {
  return (
    <form action={login}>
      <input type="password" name="password" />
      <button type="submit">登录</button>
    </form>
  )
}

登出就是 delete:

tsx 复制代码
'use server'
import { cookies } from 'next/headers'

export async function logout() {
  const store = await cookies()
  store.delete('token') // 删除 cookie
}

那几个 cookie 选项别偷懒:httpOnly 挡住前端 JS 读取(防 XSS 把 token 偷走),secure 强制 HTTPS,sameSite 挡住跨站请求伪造。存登录 token 却不加 httpOnly,等于把钥匙插门上。

在 Route Handler(route.ts)里,除了用 cookies(),也可以直接操作 NextResponse,后者在需要精确控制响应时更直观:

ts 复制代码
// app/api/theme/route.ts
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
  const { theme } = await request.json()
  const res = NextResponse.json({ ok: true })
  res.cookies.set('theme', theme, { path: '/', maxAge: 31536000 })
  return res // cookie 通过响应头下发
}

两种方式都行,选一种别混用。

读 header 的一个实用场景:拿真实 IP

反向代理(Nginx、Cloudflare)后面,客户端真实 IP 藏在转发头里,headers() 正好用来读:

tsx 复制代码
import { headers } from 'next/headers'

async function getClientIP() {
  const h = await headers()
  // 优先取代理链最左边的原始 IP,退回 x-real-ip
  const forwarded = h.get('x-forwarded-for')
  return forwarded?.split(',')[0].trim() ?? h.get('x-real-ip') ?? 'unknown'
}

注意 x-forwarded-for 是客户端可伪造的,别拿它做安全判断(比如「只有内网 IP 能访问」),它只适合做日志、地域展示这类非安全用途。

小结

  • cookies()/headers() 来自 next/headers,只能在服务端代码里用;Next.js 15 起是异步的,记得 await
  • 调用它们会让路由从静态渲染切成动态渲染------别在能静态化的页面里滥用,必要时用 <Suspense> 隔离个性化部分。
  • 读 cookie 到处可以,写 cookie 只能在 Server Action 或 Route Handler,在 Server Component 里 set 会直接报错。
  • 种登录 cookie 必带 httpOnly + secure + sameSite,否则 token 容易被偷。
  • x-forwarded-for 可伪造,只用于日志/展示,别做安全判断。

一句话记忆:读随处、写受限;一读就动态,想静态就隔离。

相关推荐
Darling噜啦啦1 小时前
Next.js 为什么是 AI 全栈开发第一选择?从文件路由到 RSC 的完整架构解析
next.js
_codeOH1 小时前
# 手把手复刻 DeepSeek 官网效果:玻璃拟态 + WebGL 流体 + Spring 动画,一篇讲透
前端
今日无bug1 小时前
从「拿来主义」到「亲手造轮子」:2 种 MCP 文件服务器写法对比
前端·node.js·mcp
xcyxiner1 小时前
flutter 运行到模拟器上
android·前端·flutter
用户921080262861 小时前
从 COT 到 ThoughtChain:AI 应用为什么需要展示“思考过程”
前端
天真小巫1 小时前
2026.8.23总结
前端·html
PedroQue991 小时前
RouterLink v2.5.0:H5端原生能力全面回归
前端·uni-app
DsirNg1 小时前
先守住数据,再下沉交互:RSC 组件分层的实战决策法
react·next.js·app router·前端架构·组件设计·react server components·server actions
八角丶1 小时前
Node.js 异步上下文详解(实验驱动)
前端·node.js