先记住这个答案
在 Route Handlers 中,先从请求头(如 X-API-Key)取出密钥,再与服务器端环境变量中存储的预期值比对。若相同则放行,否则返回 401。密钥绝不能硬编码在代码里,而要放入 .env.local 并通过 process.env.API_KEY 访问。比对时建议使用常数时间比较函数,避免因长度差异泄漏信息。
- 密钥须存环境变量,不入代码库
- 用安全比较函数防时序攻击
- 验证失败应返回 401 并记录日志
实现机制:从请求头提取到密钥比对
在 Route Handlers 中,每个处理函数接收 Request 对象。首先通过 request.headers.get('x-api-key') 取得用户携带的密钥字符串。注意 HTTP 头名称不区分大小写,但惯例使用 X-API-Key。若缺失,可立即返回 Response.json({ error: 'Missing API Key' }, { status: 401 })。
随后将取得的密钥与 process.env.API_KEY 中预存的值进行比较。切勿使用普通 ===,因为它会在长度不同时立即返回 false,导致攻击者可观察响应时间差异逐步推断密钥。推荐使用 Node.js 内置的 crypto.timingSafeEqual,但该函数要求两个 Buffer 长度相等,因此先比较长度:若长度不同直接判定失败,长度相同则用安全函数。
import { NextResponse } from 'next/server';
import crypto from 'crypto';
export async function GET(request: Request) {
const provided = request.headers.get('x-api-key');
if (!provided) {
return NextResponse.json({ error: 'Missing API Key' }, { status: 401 });
}
const expected = process.env.API_KEY ?? '';
const a = Buffer.from(provided);
const b = Buffer.from(expected);
const isValid = a.length === b.length && crypto.timingSafeEqual(a, b);
if (!isValid) {
return NextResponse.json({ error: 'Invalid API Key' }, { status: 401 });
}
// 验证通过,继续处理请求
return NextResponse.json({ data: 'protected resource' });
}此代码在 GET 处理器中验证 API Key,使用 crypto.timingSafeEqual 防时序攻击。注意先比较长度,且环境变量未设置时默认空字符串,避免 undefined 出错。
具体场景:为第三方提供只读数据接口
假设我们构建一个天气服务,需要向付费客户端开放 API。客户端在请求头中发送 X-API-Key: abc123,服务端在 .env.local 中保存 API_KEY=abc123。Route Handler 验证后,若成功则返回最新天气数据,否则返回 401。每次请求我们都记录 request.headers 中的客户端 IP 和路径,便于追踪异常调用。
实际处理中,我们考虑到如果客户端发送的密钥包含空格或大小写错误,比较会失败。为此,我们将环境变量中的密钥设计为不区分大小写较不现实,但可增加错误日志,记录格式不匹配的请求,帮助识别恶意试探。该场景下密钥由服务器生成并安全分发,不依赖第三方自定义。
适用边界:验证的脆弱点与应对
API Key 验证只适用于服务端与已知客户端之间的简单认证,它不提供身份粒度。如果多个客户端共享同一密钥,无法区分具体调者。另外,密钥通过 HTTPS 传输,若站点未强制 HTTPS,攻击者可从明文流量中截获密钥。Next.js 部署在服务端,但用户仍需确保反向代理等不泄露密钥。
同时,每次发起请求时客户端必须携带密钥,这导致浏览器端请求暴露在调试工具中,因此不适合存放在 localStorage。建议仅由服务端或非浏览器环境调用。若密钥过期或泄露,需要轮换,但环境变量更新后需重启应用,本实现不支持热更新。
容易答错的地方
- 直接使用字符串比较
- 很多人用
provided === expected判断,这会在长度不一致时立即返回,导致可测量时间差,攻击者可逐步推断密钥。应使用常数时间比较,如crypto.timingSafeEqual,且先比较长度。 - 密钥硬编码在源码中
- 把 API Key 写死在代码里,提交到仓库后泄露风险极高。正确做法是存入环境变量(如
.env.local),并在.gitignore中排除该文件,同时为不同环境设置独立值。
面试官还会怎么问?
如果多个调用方需要不同的 API Key,如何设计?
可以在数据库或配置表中保存多组密钥,验证时遍历查询。注意使用常数时间比较每个候选,或通过哈希索引实现 O(1) 查找后再安全比较。
在 Next.js 中间件中也能做类似验证,与 Route Handlers 有何区别?
中间件运行在 Edge 运行时,可能无法使用 Node 的 crypto。Route Handlers 默认 Node 运行时,可使用完整 API。若需要统一保护多个路由,可放入中间件,但要注意环境差异。
如何防止 API Key 被浏览器缓存?
这属于客户端行为。服务端可设置 Cache-Control: no-store 响应头,但根本问题是避免在浏览器端调用此类接口,应将请求代理到自己的后端。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。