用 Claude Code 安全实践 Vercel Edge Functions
用 Claude Code 实践 Vercel Edge Runtime:Middleware、签名验证、A/B测试、缓存前处理与常见坑。
不要只因为“Edge听起来更快”就使用它
Vercel Edge Functions 运行在 Edge Runtime 中。它不是普通的 Node.js 进程,而是一个以 Web API 为中心的轻量运行环境,可以使用 fetch、Request、Response、URL、TextEncoder 和 Web Crypto。换成更直白的话说,Edge Runtime 像是请求进入应用前的一道小门卫:它适合读取 URL、Header、Cookie 和很小的请求体,然后快速做出判断。
这也是 Claude Code 有价值的地方。真实项目中的 Edge 改动通常不是一个文件就结束。国家或地区跳转会碰到 middleware.ts、Vercel 请求 Header 和本地开发差异。A/B 测试会碰到 Cookie、请求 Header、统计事件和回滚。Webhook 会碰到原始请求体、签名、环境变量、请求体大小限制和后端队列。让 Claude Code 只“生成一个 Edge Function”并不够,应该让它同时审查运行时边界、日志、测试和生产环境差异。
截至 2026 年 6 月,Vercel 的 Edge Runtime 官方文档并没有把 Edge 描述成万能加速器。文档说明了可用 API、限制、区域、执行时间,也提醒部分工作负载更适合 Node.js。Next.js 文档中的 Middleware 和 Route Handlers 也围绕 Web Request 与 Response API 组织。实务结论是:把小而明确的入口判断放到 Edge,把持久化、重试、复杂业务逻辑留给后端服务。
如果你还在整理 Webhook 的重试和幂等性,可以继续看 Claude Code Webhook 实现指南。如果重点是整体速度和缓存策略,可以配合 Claude Code 性能优化指南 阅读。
五个适合 Edge 的实际场景
适合 Edge 的任务通常可以从请求元数据或很小的签名正文中得到答案。需要大量依赖、数据库长事务、私有网络连接、大文件上传或长时间 LLM 流式输出的任务,不应放在 Edge 中。
| 场景 | 为什么适合 Edge | 应保留在 Node.js 或后端服务中 |
|---|---|---|
| 国家或地区跳转 | 可根据 x-vercel-ip-country 等 Header 在入口处快速分流 | 用户偏好保存、价格规则、账号策略 |
| A/B 测试 | 可用 Cookie 固定实验分组,并在渲染前写入请求 Header | 数据统计、显著性判断、实验停止决策 |
| 轻量认证或签名检查 | 可提前拒绝非法预览请求或 Webhook | 会话签发、角色权限、审计日志 |
| 缓存前处理 | 可规范化 URL 和查询参数,稳定缓存键 | 重新生成缓存、库存更新、复杂计算 |
| Webhook 接收 | 可校验小正文签名并转发给内部服务 | 支付确认、邮件发送、重试和 CRM 更新 |
这张表也适合直接放进 Claude Code 的提示词。你要明确告诉它哪些逻辑属于 Edge,哪些必须留在后端。这样可以减少生成代码误用 Node-only API、直接连接数据库、或者把 secret 打进日志的风险。
flowchart LR
A["User request"] --> B["Next.js Middleware"]
B --> C{"Small decision"}
C --> D["Country redirect"]
C --> E["A/B bucket"]
C --> F["Light auth"]
B --> G["Edge Route Handler"]
G --> H["HMAC signature check"]
H --> I["Internal API or queue"]
图中的 Edge 不是完整后端,而是入口控制层。Middleware 负责分类请求,Route Handler 负责验证小型 Webhook,真正需要持久化和重试的工作交给内部 API、队列或工作流服务。
可复制的 Next.js Middleware
下面的 middleware.ts 同时处理地区跳转、A/B 分组、预览页的轻量认证和安全 Header。示例使用 Vercel Header,而不是依赖 request.geo,这样更容易在不同 Next.js 版本中保持稳定。注意,本地开发通常没有 x-vercel-ip-country,地区跳转需要在 Vercel Preview Deployment 上确认。
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
const PUBLIC_FILE = /\.(?:png|jpg|jpeg|gif|svg|webp|ico|css|js|map|txt)$/i;
const SECRET_HEADER = "x-edge-shared-secret";
export const config = {
matcher: ["/((?!api/webhooks|_next/static|_next/image|favicon.ico).*)"],
};
function chooseBucket(request: NextRequest): "a" | "b" {
const current = request.cookies.get("ab_bucket")?.value;
if (current === "a" || current === "b") return current;
const random = new Uint8Array(1);
crypto.getRandomValues(random);
return random[0] < 128 ? "a" : "b";
}
function localeFromCountry(country: string | null): string | null {
switch (country?.toUpperCase()) {
case "JP":
return "ja";
case "KR":
return "ko";
case "CN":
case "TW":
case "HK":
return "zh";
case "BR":
return "pt";
case "ES":
case "MX":
return "es";
default:
return null;
}
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (PUBLIC_FILE.test(pathname)) {
return NextResponse.next();
}
if (pathname === "/") {
const country = request.headers.get("x-vercel-ip-country");
const locale = localeFromCountry(country);
if (locale) {
return NextResponse.redirect(new URL(`/${locale}/`, request.url), 307);
}
}
if (pathname.startsWith("/beta")) {
const bucket = chooseBucket(request);
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-ab-bucket", bucket);
const response = NextResponse.next({
request: { headers: requestHeaders },
});
if (!request.cookies.has("ab_bucket")) {
response.cookies.set("ab_bucket", bucket, {
maxAge: 60 * 60 * 24 * 30,
path: "/",
sameSite: "lax",
secure: request.nextUrl.protocol === "https:",
});
}
return response;
}
if (pathname.startsWith("/preview")) {
const expected = process.env.EDGE_SHARED_SECRET;
const actual = request.headers.get(SECRET_HEADER);
if (!expected || actual !== expected) {
return NextResponse.redirect(new URL("/login", request.url), 307);
}
}
const response = NextResponse.next();
response.headers.set("x-content-type-options", "nosniff");
response.headers.set("referrer-policy", "strict-origin-when-cross-origin");
return response;
}
这个例子故意保持克制。A/B 测试只负责分组,不负责统计。预览认证只做入口拦截,不替代完整登录系统。地区跳转只在首页执行,避免把所有路径都卷进重定向循环。Middleware 可能影响大量请求,因此越小越容易审查。
Edge Route Handler 中验证 Webhook 签名
下面是 app/api/webhooks/provider/route.ts 示例。HMAC 可以理解为“发送方和接收方共享一个 secret,用它和原始正文算出签名,以确认正文没有被篡改”。Edge Runtime 中不使用 Node.js 的 crypto.createHmac 或 Buffer,而是用 Web Crypto 和 TextEncoder。
// app/api/webhooks/provider/route.ts
export const runtime = "edge";
export const preferredRegion = ["iad1", "hnd1"];
const MAX_BODY_BYTES = 256_000;
function hexToBytes(hex: string): Uint8Array {
const clean = hex.replace(/^sha256=/, "").trim();
if (!/^[0-9a-f]+$/i.test(clean) || clean.length % 2 !== 0) {
return new Uint8Array();
}
const bytes = new Uint8Array(clean.length / 2);
for (let index = 0; index < clean.length; index += 2) {
bytes[index / 2] = Number.parseInt(clean.slice(index, index + 2), 16);
}
return bytes;
}
async function hmacSha256(secret: string, payload: string): Promise<Uint8Array> {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
return new Uint8Array(signature);
}
function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let index = 0; index < a.length; index += 1) {
diff |= a[index] ^ b[index];
}
return diff === 0;
}
export async function POST(request: Request) {
const secret = process.env.WEBHOOK_SECRET;
const internalOrigin = process.env.INTERNAL_API_ORIGIN;
const internalToken = process.env.INTERNAL_API_TOKEN;
if (!secret || !internalOrigin || !internalToken) {
return Response.json({ error: "server is not configured" }, { status: 500 });
}
const contentLength = Number(request.headers.get("content-length") ?? "0");
if (contentLength > MAX_BODY_BYTES) {
return Response.json({ error: "payload too large" }, { status: 413 });
}
const rawBody = await request.text();
const rawBodyBytes = new TextEncoder().encode(rawBody);
if (rawBodyBytes.byteLength > MAX_BODY_BYTES) {
return Response.json({ error: "payload too large" }, { status: 413 });
}
const provided = hexToBytes(request.headers.get("x-signature-sha256") ?? "");
const expected = await hmacSha256(secret, rawBody);
if (!constantTimeEqual(provided, expected)) {
return Response.json({ error: "invalid signature" }, { status: 401 });
}
const event = JSON.parse(rawBody) as { id?: string; type?: string };
if (!event.id || !event.type) {
return Response.json({ error: "invalid event" }, { status: 400 });
}
await fetch(`${internalOrigin}/api/webhook-events`, {
method: "POST",
headers: {
authorization: `Bearer ${internalToken}`,
"content-type": "application/json",
},
body: JSON.stringify({
id: event.id,
type: event.type,
receivedAt: new Date().toISOString(),
}),
});
return Response.json({ ok: true });
}
顺序很重要:先限制大小,再读取原始正文,再验证签名,最后才解析 JSON。验证后也不要把支付、发邮件、CRM 更新都放在 Edge 里;把事件转发给内部系统,让后端负责幂等、重试和审计。
给 Claude Code 的审查指令和最小测试
可以把下面的提示词直接交给 Claude Code。重点不是让它多写代码,而是让它按 Edge Runtime 的限制进行代码审查。
Review this Next.js Edge implementation.
Scope:
- middleware.ts
- app/api/webhooks/provider/route.ts
- related tests and environment variable names
Check:
- no Node-only APIs such as fs, net, tls, Buffer, or node:crypto in Edge files
- no direct database connection from Edge Runtime
- country redirect does not loop
- A/B bucket is stable by cookie and not written on every request
- webhook verifies the raw body before JSON parsing
- secrets, signatures, cookies, and authorization headers are not logged
- body size and production-only Vercel headers are documented
Return blockers first, then suggested tests.
本地测试中可以用 Node.js 生成 HMAC 签名,因为那只是测试辅助代码,不在 Edge Runtime 中运行。
npm run lint
npm run build
vercel dev
BODY='{"id":"evt_123","type":"checkout.completed"}'
SIG=$(node -e "const crypto=require('crypto'); const body=process.argv[1]; console.log('sha256='+crypto.createHmac('sha256', process.env.WEBHOOK_SECRET).update(body).digest('hex'))" "$BODY")
curl -i http://localhost:3000/api/webhooks/provider \
-X POST \
-H "content-type: application/json" \
-H "x-signature-sha256: $SIG" \
--data "$BODY"
curl -I http://localhost:3000/beta
curl -I http://localhost:3000/preview
生产前还要在 Preview Deployment 中确认地区 Header、HTTPS Cookie、重定向、日志和区域假设。本地测试能发现语法和大部分路由问题,但不能完全模拟 Vercel 的边缘环境。
常见坑
第一个坑是误用 Node.js API。fs、Buffer、crypto.createHmac、原生模块和基于 TCP 的数据库客户端都不适合 Edge 文件。有时不是你直接导入,而是某个工具库间接导入,所以要让 Claude Code 检查依赖路径。
第二个坑是从 Edge 直接连接数据库。即使代码在用户附近运行,如果每次都要回到同一个数据库区域,延迟仍然存在,还可能增加连接压力。需要持久状态时,优先走 HTTP API、队列或靠近数据库的 Node.js Function。
第三个坑是误解 cold start 和 region。Edge 可以降低入口判断的延迟,但不能把远端数据库变成本地数据库。preferredRegion 有帮助,但必须用日志和指标确认实际路径。
第四个坑是 secret 和日志泄露。Webhook 正文、签名、Cookie、Authorization Header、预览 secret 都不应该原样打印。调试日志可以存在,但要先脱敏。
第五个坑是请求体大小和 streaming。Edge 适合小请求和快速判断,不适合大文件上传、CSV 导入、图片处理或长时间 LLM 流式输出。读取 request.text() 时一定要设大小上限。
第六个坑是本地与生产差异。vercel dev 很有用,但它不能完整模拟地区 Header、实际运行区域、Preview 日志和 secure Cookie 行为。测试应分为本地、构建、Preview 三层。
ClaudeCodeLab 的团队落地方式
个人项目可以从上面的两个文件开始。团队项目更难的是规则:哪些文件允许使用 Edge Runtime,哪些 API 禁止,环境变量如何命名,Preview Deployment 由谁确认,Claude Code 生成的代码如何审查。
ClaudeCodeLab 可以帮助团队整理 Claude Code 规则、CLAUDE.md、Edge Runtime 审查清单、Webhook 验证记录和 Vercel 部署检查。如果你希望把这些模式应用到真实仓库,可以从 Claude Code 培训与咨询 开始。目标不是增加流程,而是避免一个小小的 middleware 改动影响整个站点。
实际尝试后的结果
按照本文的结构实践后,最大的收益不是“代码变得更快”,而是边界更清楚。Middleware 只负责跳转、A/B 分组、Header 和轻量拦截;Edge Route Handler 只验证小型签名 Webhook 并转发事件。给 Claude Code 明确审查指令后,它更容易发现 Buffer 混入、签名前先解析 JSON、Vercel 专有 Header 本地不可用、日志过度暴露等问题。Edge Functions 不是魔法加速器,但把入口判断保持小而可测时,它们是非常实用的生产工具。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。