用 Claude Code 安全实现 Cookie 管理:Next.js 会话、CSRF 与同意边界
用 Claude Code 实现安全 Cookie:HttpOnly、Secure、SameSite、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 的规则”。
先做 Cookie 分类
不要一开始就写 cookies().set()。先写清楚每个 Cookie 的目的:认证 Cookie 是凭证;偏好 Cookie 是主题、语言等 UI 状态;分析和广告 Cookie 是追踪基础设施。它们的同意要求和安全要求不同。
| 用途 | 示例 | 建议属性 | 同意边界 |
|---|---|---|---|
| 认证会话 | __Host-session | HttpOnly, Secure, SameSite=Lax, Path=/, 短 Max-Age | 通常属于服务必要 Cookie,但仍需按地区确认 |
| CSRF token | csrf-token | Secure, SameSite=Lax, 短 Max-Age | 只用于安全校验,不当作分析 ID |
| UI 偏好 | theme, locale | Secure, 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 中发放认证 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 和同意逻辑。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code权限安全阶梯:逐步放开访问而不失控
从只读到有限编辑、验证命令和部署检查的 Claude Code 权限升级流程。
Claude Code 小PR证据包:让小改动真正可审查
用差异、验证命令、公开URL、CTA路径和回滚说明,把Claude Code的小PR变得可审查。
Claude Code 提交前 Review Gate:同时检查差异、测试、公开 URL 和 CTA
提交前用 Claude Code 审查差异范围、build、公开 URL、Gumroad 链接、咨询 CTA、缺少测试和无关文件。