Tips & Tricks (更新: 2026/6/2)

用 Claude Code 安全实现 Cookie 管理:Next.js 会话、CSRF 与同意边界

用 Claude Code 实现安全 Cookie:HttpOnly、Secure、SameSite、CSRF、登出与同意边界。

用 Claude Code 安全实现 Cookie 管理:Next.js 会话、CSRF 与同意边界

Cookie 管理看起来只是“保存登录状态”,但它其实是认证安全的入口。浏览器会自动把 Cookie 带到请求里,所以一个小小的属性错误,就可能让账号被劫持、登出失效、CSRF 防护失效,或者把分析追踪和登录安全混在一起。

Claude Code 可以很快生成代码,但如果提示词只有“设置一个登录 Cookie”,结果常常不够安全。常见问题包括:没有 HttpOnly,SameSite=None 没有配 Secure,删除 Cookie 时 Path 不一致,或者把需要同意的分析 Cookie 和认证 Cookie 放进同一套逻辑。

这篇文章用 Next.js App Router 做例子,给出可复制运行的 Route Handler、登出处理、服务端读取、CSRF token、session fixation(会话固定)防护、浏览器行为、同意边界和验证命令。HttpOnly 可以理解为“禁止前端 JavaScript 读取的标记”,SameSite 是“跨站请求时浏览器是否自动带上 Cookie 的规则”。

不要一开始就写 cookies().set()。先写清楚每个 Cookie 的目的:认证 Cookie 是凭证;偏好 Cookie 是主题、语言等 UI 状态;分析和广告 Cookie 是追踪基础设施。它们的同意要求和安全要求不同。

用途示例建议属性同意边界
认证会话__Host-sessionHttpOnly, Secure, SameSite=Lax, Path=/, 短 Max-Age通常属于服务必要 Cookie,但仍需按地区确认
CSRF tokencsrf-tokenSecure, SameSite=Lax, 短 Max-Age只用于安全校验,不当作分析 ID
UI 偏好theme, localeSecure, SameSite=Lax, 有限期限视地区和用途解释
分析或广告_ga, campaign ID需要同意时,必须在同意后设置与登录、结账 Cookie 分开

MDN 的安全 Cookie 配置建议用 Secure、HttpOnly、SameSite 和 Cookie prefix 收窄范围。MDN 的 Set-Cookie 还说明,SameSite=None 必须配 Secure,同时出现 Max-Age 和 Expires 时,Max-Age 优先。

认证 Cookie 推荐使用 __Host- prefix。支持该规则的浏览器只接受同时满足 Secure、没有 Domain、Path=/ 的 __Host- Cookie。这样可以降低子域名伪造或覆盖会话 Cookie 的风险。

Next.js 官方 cookies API 说明,cookies() 是异步函数,并支持 httpOnly、secure、sameSite、maxAge、path、domain 等选项。Server Component 可以读取 Cookie;设置和删除 Cookie 应放在 Route Handler 或 Server Action 中。

下面的代码可以放在 app/api/login/route.ts。它用内存 Map 保存 session,方便本地验证。生产环境请替换成 Redis、PostgreSQL、DynamoDB 等真正的 session store。

import { createHmac, randomBytes } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

export const runtime = "nodejs";

const env = z
  .object({
    NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
    SESSION_SECRET: z.string().min(32),
  })
  .parse(process.env);

const SESSION_COOKIE = "__Host-session";
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 8;

type SessionRecord = {
  userId: string;
  expiresAt: number;
};

declare global {
  var demoSessions: Map<string, SessionRecord> | undefined;
}

const sessions = globalThis.demoSessions ?? new Map<string, SessionRecord>();
globalThis.demoSessions = sessions;

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(12),
});

function createSessionToken() {
  const id = randomBytes(32).toString("base64url");
  const signature = createHmac("sha256", env.SESSION_SECRET)
    .update(id)
    .digest("base64url");

  return `${id}.${signature}`;
}

async function authenticate(email: string, password: string) {
  if (email === "masa@example.com" && password === "correct-horse-battery-staple") {
    return { id: "user_123" };
  }

  return null;
}

export async function POST(request: NextRequest) {
  const body = loginSchema.safeParse(await request.json());
  if (!body.success) {
    return NextResponse.json({ error: "Invalid login payload" }, { status: 400 });
  }

  const user = await authenticate(body.data.email, body.data.password);
  if (!user) {
    return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
  }

  const token = createSessionToken();
  sessions.set(token, {
    userId: user.id,
    expiresAt: Date.now() + SESSION_MAX_AGE_SECONDS * 1000,
  });

  const response = NextResponse.json({ ok: true });
  response.cookies.set({
    name: SESSION_COOKIE,
    value: token,
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
    maxAge: SESSION_MAX_AGE_SECONDS,
  });

  return response;
}

这段代码每次登录成功都会生成新的 session token。这样可以防止 session fixation(会话固定):攻击者先准备一个 session ID,再诱导用户用这个 ID 登录,之后攻击者继续使用同一个 ID。OWASP 的Session Fixation 对这个攻击有详细说明。

登出与服务端读取

删除 Cookie 时,名称不够,还要匹配作用域。发放时是 Path=/,删除时也必须是 Path=/。如果曾经设置 Domain,删除时也要一致。__Host- Cookie 不允许 Domain,因此能减少这类生产事故。

app/api/logout/route.ts:

import { NextResponse } from "next/server";

const SESSION_COOKIE = "__Host-session";

export async function POST() {
  const response = NextResponse.json({ ok: true });

  response.cookies.set({
    name: SESSION_COOKIE,
    value: "",
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
    maxAge: 0,
  });

  return response;
}

真正的登出还要让服务端 session store 失效。只清除浏览器 Cookie,无法让已经泄露的 token 立即失效。

服务端读取时使用 await cookies():

import { cookies } from "next/headers";
import { redirect } from "next/navigation";

const SESSION_COOKIE = "__Host-session";

export default async function AccountPage() {
  const cookieStore = await cookies();
  const sessionToken = cookieStore.get(SESSION_COOKIE)?.value;

  if (!sessionToken) {
    redirect("/login");
  }

  return <main>Account dashboard</main>;
}

不要只因为 Cookie 存在就认定用户已登录。服务端必须检查 session 是否存在、是否过期、是否已被撤销,以及用户权限是否仍然有效。

CSRF 不能只靠 HttpOnly

CSRF 是跨站请求伪造:恶意站点让已登录用户的浏览器向你的站点发起状态变更请求。HttpOnly 只是阻止 JavaScript 读取 Cookie,并不会阻止浏览器自动发送 Cookie。OWASP 的CSRF Prevention Cheat Sheet建议为状态变更请求加入 CSRF token。

下面是一个与 session token 绑定的签名 CSRF token helper。可以在表单或 fetch 中通过 X-CSRF-Token 发送。

import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";

const CSRF_SECRET = process.env.SESSION_SECRET;
if (!CSRF_SECRET || CSRF_SECRET.length < 32) {
  throw new Error("SESSION_SECRET must be at least 32 characters");
}

export function createCsrfToken(sessionToken: string) {
  const nonce = randomBytes(16).toString("base64url");
  const signature = createHmac("sha256", CSRF_SECRET)
    .update(`${sessionToken}.${nonce}`)
    .digest("base64url");

  return `${nonce}.${signature}`;
}

export function verifyCsrfToken(sessionToken: string, token: string) {
  const [nonce, signature] = token.split(".");
  if (!nonce || !signature) return false;

  const expected = createHmac("sha256", CSRF_SECRET)
    .update(`${sessionToken}.${nonce}`)
    .digest("base64url");

  return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

CSRF token 应用于 POST、PUT、PATCH、DELETE。不要让 GET 修改状态。SameSite=Lax 是防御层之一,但不能替代 token;如果站点存在 XSS,CSRF token 也可能被绕过,所以输出转义、CSP 和依赖安全同样重要。

浏览器行为与过期时间

浏览器不会把 Set-Cookie 暴露给前端 JavaScript。你可以在 DevTools 或 curl -i 中看到它,但 fetch() 的 response headers 里读不到。跨域请求还需要正确配置 CORS 和 credentials,否则 Cookie 发送和接收都可能与预期不一致。

Max-Age 表示从现在开始多少秒后过期;Expires 是固定时间点。业务代码通常更适合使用 Max-Age,因为它不依赖客户端和服务器的时钟是否一致。若两者同时存在,浏览器按 Max-Age 优先处理。

SameSite=Lax 允许顶层安全方法导航,例如用户从外部链接点进来。因此,任何修改状态的接口都不应使用 GET。SameSite=Strict 更强,但可能影响从邮件或外部站点进入后的体验。SameSite=None 只在确实需要跨站上下文时使用,并且必须配 Secure。

同意边界与实际用例

同意边界是把“服务必要 Cookie”和“分析、广告、实验 Cookie”分开的线。欧盟委员会的Cookies policy也按同意偏好、认证、分析等用途区分。本文不是法律建议,但工程上必须做到:认证和安全 Cookie 不与广告分析共用目的。

用例一:SaaS 登录。使用 __Host-session、短 Max-Age、服务端撤销、CSRF token,并在登录成功时生成新 session。管理后台和账单操作可以考虑 SameSite=Strict 或二次验证。

用例二:内容站和转化路径。免费 PDF、产品页、咨询表单可能需要计数,但读者拒绝分析 Cookie 时,登录、下载、购买和咨询不应中断。

用例三:语言和主题偏好。它们可能需要被前端 JavaScript 读取,因此不会是 HttpOnly。但这不代表可以在里面放 token、权限、价格或会员等级。

常见失败例

第一,__Host-session 缺少 Secure、设置了 Domain,或者忘了 Path=/。这些都会破坏 prefix 的意义。

第二,登出不生效。多数原因是删除时的 Path 或 Domain 与发放时不一致。

第三,把 SameSite 当作完整 CSRF 方案。它只是防御层,不替代 token、Origin 检查和正确的 HTTP 方法设计。

第四,把 session token 放进 localStorage 或可被 document.cookie 读取的 Cookie。XSS 一旦发生,凭证就会泄露。

第五,同意弹窗误拦认证 Cookie。拒绝分析不应该导致登录、购物车、结账或 CSRF 防护失效。

Prompt 与验证

给 Claude Code 的提示词要写出安全合同:

为 Next.js App Router 实现登录 Cookie。

要求:
- Cookie 名称为 __Host-session
- 显式设置 HttpOnly, Secure, SameSite=Lax, Path=/, Max-Age
- 不设置 Domain
- 每次登录成功都生成新的 session token
- 登出用相同 Path 和 Max-Age=0 清除
- 不把分析同意 Cookie 与认证 Cookie 混在一起
- 为状态变更请求说明 CSRF token
- 按 MDN、Next.js、OWASP 官方文档 review

本地验证:

curl -i -X POST http://localhost:3000/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"masa@example.com","password":"correct-horse-battery-staple"}'

期望看到:

Set-Cookie: __Host-session=...; Path=/; Max-Age=28800; HttpOnly; Secure; SameSite=Lax

登出验证:

curl -i -X POST http://localhost:3000/api/logout

确认响应中仍是同一个 Cookie 名称、Path=/,并且 Max-Age=0。如果用 Playwright,可以通过 context.cookies() 检查 httpOnly、secure、sameSite 和过期时间。

相关链接、CTA 与实测结果

认证整体设计可以继续看Claude Code 认证实现指南、JWT 认证对比和安全审计指南。官方资料包括 MDN Set-Cookie、Next.js cookies、OWASP Session Management 和 OWASP CSRF Prevention。

如果你想把这些检查变成可复用流程,可以从 ClaudeCodeLab 的免费资料开始,再看产品和模板。团队需要把 Cookie、同意、结账和安全 review 放进真实仓库流程时,可以走培训与咨询。

我实际测试这套流程后发现,提示词里明确写出 __Host-、登出作用域、CSRF token 和分析同意边界时,Claude Code 生成的代码更接近可发布状态。只写“把 Cookie 做安全一点”,通常还需要手工修正登出、SameSite 和同意逻辑。

#Claude Code #Cookie #session #security #Next.js #TypeScript
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

让 Claude Code 真正进入可验证的工作流

先用免费 PDF 固定基础,再用 Gumroad 教材复用工作流;如果涉及团队导入、权限或收入路径,可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。