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”,结果常常不够安全。常见问题包括:没有 HttpOnlySameSite=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 配置建议用 SecureHttpOnlySameSite 和 Cookie prefix 收窄范围。MDN 的 Set-Cookie 还说明,SameSite=None 必须配 Secure,同时出现 Max-AgeExpires 时,Max-Age 优先。

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

Next.js 官方 cookies API 说明,cookies() 是异步函数,并支持 httpOnlysecuresameSitemaxAgepathdomain 等选项。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 的意义。

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

第三,把 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() 检查 httpOnlysecuresameSite 和过期时间。

相关链接、CTA 与实测结果

认证整体设计可以继续看Claude Code 认证实现指南JWT 认证对比安全审计指南。官方资料包括 MDN Set-CookieNext.js cookiesOWASP Session ManagementOWASP 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,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

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