在 Next.js / Node.js 的请求生命周期中,中间件就像是守卫在城堡大门口的哨兵。当一个请求从客户端发出,它会先经过中间件,然后才会到达具体的路由处理器(Route Handlers)或服务器组件(Server Components)。
如果等到请求进入了业务代码再去限制体积,Node.js 此时可能已经把这个大文本读入内存了,这无法从根本上防御 DoS 攻击。
在中间件这一层,我们可以利用 HTTP 请求头中的 Content-Length 来做文章。这个请求头记录了整个请求体的字节大小(Byte)。我们可以直接在这里设置一条"硬红线"。
通常,一个正常的导航菜单配置 JSON 数据不会超过几百 KB。如果我们把上限设为 1MB 或者是 2MB,就能挡掉绝大多数恶意的大文件轰炸。
让我们来看看在 Next.js 的 middleware.ts 中可以如何实现这个拦截逻辑:
import
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 1. 🔍 只针对修改菜单的 POST / PUT 请求进行体积检查
if (request.nextUrl.pathname.startsWith('/api/admin/menu')) {
// 2. 📏 获取请求头中的数据体积大小 (单位: 字节)
const contentLength = request.headers.get('content-length');
if (contentLength) {
const sizeInBytes = parseInt(contentLength, 10);
const ONE_MEGABYTE = 1024 * 1024; // 1MB
// 3. 🚨 超过红线,直接弹回,不给它进入服务器解析的机会
if (sizeInBytes > ONE_MEGABYTE) {
return new NextResponse(
JSON.stringify({ error: '请求体体积过大,拒绝服务' }),
{ status: 413, headers: { 'content-type': 'application/json' } }
);
}
}
}
return NextResponse.next();
}
黑客也可能会非常狡猾。如果黑客故意伪造了 HTTP 请求头,把 Content-Length 故意写得很小(比如写了 100 字节),但实际上底层却通过 HTTP 分块传输(Chunked Transfer Encoding)源源不断地发送了几百兆的数据流,你觉得我们的中间件还能防住它吗?我们还需要配合什么机制来彻底锁死它呢 黑客确实可以通过分块传输来伪造 Content-Length,哨兵在这个时候就会被欺骗。只检查头部是不够的,我们必须对请求体(Request Body)的实际流入量进行实时监控。
在 Node.js 和 Next.js 的路由处理器(Route Handlers)中,标准的做法是流式读取(Streaming)请求体,并在读取过程中计算已接收的字节数。一旦实际读取的字节数超过阈值,立刻掐断连接。
我们可以使用原生的 TransformStream 或者结合 safe-json-parse 来实现这一层防御。
🛡️ 3. 实战:限制实际流入的 Body 体积
在我们的路由处理器中,我们可以写一个通用的解析助手函数:
import
import { scan } from 'secure-json-parse';
// 限制实际读取的最大体积为 1MB
const MAX_ALLOWED_SIZE = 1024 * 1024;
async function parseAndValidateSafeBody(request: NextRequest) {
const reader = request.body?.getReader();
if (!reader) throw new Error('请求体为空');
let totalBytes = 0;
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
totalBytes += value.length;
// 🚨 核心防御:在读取流的过程中,一旦发现实际体积超标,立刻熔断!
if (totalBytes > MAX_ALLOWED_SIZE) {
throw new Error('413_PAYLOAD_TOO_LARGE');
}
chunks.push(value);
}
// 将所有数据块合并并转为字符串
const concatBuffer = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
concatBuffer.set(chunk, offset);
offset += chunk.length;
}
const rawText = new TextDecoder().decode(concatBuffer);
// 🔒 结合我们第一维学的 secure-json-parse 杜绝原型链污染
return scan(rawText, { protoAction: 'error', constructorAction: 'error' });
}