使用一个中间件文件阻止 Next.js 中的一次性电子邮件
一个 40 行的 Next.js 中间件,调用 botoi API 来拒绝来自临时电子邮件地址的注册。 复制、粘贴、部署。
用户注册 test92847@mailinator.com,在您的免费试用期结束后消失。
他们明天回来 test92848@mailinator.com 然后再做一次。
您的支持队列充满了虚拟帐户。 你的分析显示用户数量膨胀,毫无意义。
您的滥用检测触发得太晚,因为该帐户已经消耗了资源。
解决方法:在注册请求到达您的数据库之前,在门口阻止一次性电子邮件。 本指南展示了如何在 Next.js 中使用单个中间件文件和除 fetch 调用之外的零依赖项来完成此操作。
你将构建什么
Next.js 中间件,拦截对您的注册端点的 POST 请求, 从请求正文中提取电子邮件,并根据 botoi 一次性电子邮件 API, 如果电子邮件属于一次性服务,则返回 422 响应。 整个文件不到 50 行。
中间件
创造 middleware.ts 在您的项目根目录(或 src/middleware.ts 如果你使用 src 目录):
import { NextRequest, NextResponse } from 'next/server';
const BOTOI_URL = 'https://api.botoi.com/v1/disposable-email/check';
export async function middleware(req: NextRequest) {
// Only intercept POST requests to the signup route
if (req.method !== 'POST') {
return NextResponse.next();
}
let body: { email?: string };
try {
body = await req.json();
} catch {
return NextResponse.json(
{ error: 'Invalid request body' },
{ status: 400 }
);
}
const email = body.email?.trim().toLowerCase();
if (!email) {
return NextResponse.next();
}
try {
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (data.success && data.data.is_disposable) {
return NextResponse.json(
{ error: 'Disposable email addresses are not allowed. Please use a permanent email.' },
{ status: 422 }
);
}
} catch {
// API unreachable; fail open so real users aren't blocked
console.warn('botoi disposable-email check failed, allowing request through');
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/auth/signup', '/api/register'],
};
它是如何运作的
路线匹配
这 config.matcher 数组告诉 Next.js 哪些路由触发此中间件。
更改这些路径以匹配您的注册端点。 在路由处理程序执行之前,中间件在边缘运行,
因此被拒绝的请求永远不会触及您的数据库或身份验证提供商。
电子邮件提取
中间件读取请求正文 req.json() 并拉动 email 场地。
如果解析失败或不存在电子邮件,则请求将不受影响地通过。
这使得中间件对非注册路由不可见。
API 调用
单个 POST 至 https://api.botoi.com/v1/disposable-email/check 正文中包含电子邮件。
响应内容包括:
{
"success": true,
"data": {
"email": "throwaway@mailinator.com",
"domain": "mailinator.com",
"is_disposable": true,
"is_free": false,
"provider": "Mailinator"
}
}
这 is_disposable 旗帜是门。 当它是 true,中间件返回 422 并带有明确的消息。
当它是 false, NextResponse.next() 让请求继续发送到您的注册处理程序。
故障开放设计
这 catch 阻止 fetch 调用意味着网络故障、超时或 API 停机不会中断注册。
中间件记录警告并让请求通过。 您的用户永远不会看到由第三方中断引起的错误。
处理边缘情况
超时
由于 botoi API 使用内存中的域列表,因此它对大多数请求的响应时间都在 50 毫秒以内。
如果你想要一个硬超时,请将 fetch 包裹起来 AbortSignal.timeout():
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000), // 3 second timeout
});
速率限制
免费套餐每分钟允许 5 个请求。 如果您的应用程序处理的注册数量多于此, 获取 API 密钥 并将其作为不记名令牌传递:
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': \`Bearer \${process.env.BOTOI_API_KEY}\`,
},
body: JSON.stringify({ email }),
});
店铺 BOTOI_API_KEY 在你的 .env.local 文件。 切勿将其提交给版本控制。
重复检查
如果同一封电子邮件连续两次到达您的注册端点(双击,重试逻辑), 您将对同一域进行两次 API 调用。 对于大多数应用程序来说这很好。 如果重要的话,添加一个短期缓存(如下所述)。
生产强化
添加内存缓存
将每个域的一次性检查结果缓存 5 分钟。 这减少了 API 调用并加快了对同一域的重复检查:
const cache = new Map<string, { isDisposable: boolean; expires: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function isDisposableEmail(email: string): Promise<boolean> {
const domain = email.split('@')[1];
const cached = cache.get(domain);
if (cached && cached.expires > Date.now()) {
return cached.isDisposable;
}
try {
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const data = await res.json();
const isDisposable = data.success && data.data.is_disposable;
cache.set(domain, {
isDisposable,
expires: Date.now() + CACHE_TTL,
});
return isDisposable;
} catch {
return false; // fail open
}
}
这种基于映射的缓存适用于无服务器和边缘运行时。 对于多实例部署,请将其替换为 Redis 或 Upstash:
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
async function isDisposableEmail(email: string): Promise<boolean> {
const domain = email.split('@')[1];
const cached = await redis.get<boolean>(\`disposable:\${domain}\`);
if (cached !== null) {
return cached;
}
try {
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const data = await res.json();
const isDisposable = data.success && data.data.is_disposable;
await redis.set(\`disposable:\${domain}\`, isDisposable, { ex: 300 });
return isDisposable;
} catch {
return false;
}
}
将公司域列入白名单
有些公司使用您永远不想阻止的自定义域,即使它们与可疑模式匹配。 添加允许名单:
const ALLOWED_DOMAINS = new Set([
'yourcompany.com',
'partner-corp.io',
'bigclient.co',
]);
async function isDisposableEmail(email: string): Promise<boolean> {
const domain = email.split('@')[1];
if (ALLOWED_DOMAINS.has(domain)) {
return false;
}
// ... rest of the check logic
}
记录被阻止的尝试
跟踪哪些域被拒绝,以便您可以监控滥用模式并调整您的策略:
if (data.success && data.data.is_disposable) {
console.log(
JSON.stringify({
event: 'disposable_email_blocked',
domain: data.data.domain,
provider: data.data.provider,
timestamp: new Date().toISOString(),
})
);
return NextResponse.json(
{ error: 'Disposable email addresses are not allowed.' },
{ status: 422 }
);
}
完整的中间件,全部硬化
这是包含缓存、白名单、超时和结构化日志记录的完整文件:
import { NextRequest, NextResponse } from 'next/server';
const BOTOI_URL = 'https://api.botoi.com/v1/disposable-email/check';
const CACHE_TTL = 5 * 60 * 1000;
const ALLOWED_DOMAINS = new Set([
// Add your corporate or partner domains here
]);
const cache = new Map<string, { isDisposable: boolean; expires: number }>();
async function checkDisposable(email: string): Promise<{
isDisposable: boolean;
domain: string;
provider: string | null;
}> {
const domain = email.split('@')[1];
if (ALLOWED_DOMAINS.has(domain)) {
return { isDisposable: false, domain, provider: null };
}
const cached = cache.get(domain);
if (cached && cached.expires > Date.now()) {
return { isDisposable: cached.isDisposable, domain, provider: null };
}
try {
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const data = await res.json();
const isDisposable = data.success && data.data.is_disposable;
const provider = data.data?.provider ?? null;
cache.set(domain, { isDisposable, expires: Date.now() + CACHE_TTL });
return { isDisposable, domain, provider };
} catch {
return { isDisposable: false, domain, provider: null };
}
}
export async function middleware(req: NextRequest) {
if (req.method !== 'POST') {
return NextResponse.next();
}
let body: { email?: string };
try {
body = await req.json();
} catch {
return NextResponse.json(
{ error: 'Invalid request body' },
{ status: 400 }
);
}
const email = body.email?.trim().toLowerCase();
if (!email || !email.includes('@')) {
return NextResponse.next();
}
const result = await checkDisposable(email);
if (result.isDisposable) {
console.log(
JSON.stringify({
event: 'disposable_email_blocked',
domain: result.domain,
provider: result.provider,
timestamp: new Date().toISOString(),
})
);
return NextResponse.json(
{ error: 'Disposable email addresses are not allowed. Please use a permanent email.' },
{ status: 422 }
);
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/auth/signup', '/api/register'],
};
本地测试
启动 Next.js 开发服务器并使用已知的一次性电子邮件发出请求:
curl -X POST http://localhost:3000/api/auth/signup \\
-H "Content-Type: application/json" \\
-d '{"email": "test@mailinator.com", "password": "hunter2"}'
预期回应:
{
"error": "Disposable email addresses are not allowed. Please use a permanent email."
}
尝试发送合法电子邮件以确认其通过:
curl -X POST http://localhost:3000/api/auth/signup \\
-H "Content-Type: application/json" \\
-d '{"email": "dev@acme-corp.com", "password": "hunter2"}'
此请求照常到达您的注册处理程序。
何时也检查客户端
中间件捕获服务器上的一次性电子邮件。 但您也可以从注册表单中调用相同的 API 在用户提交*之前*显示内联错误。 电子邮件字段失去焦点后的快速客户端检查 节省用户的往返时间并提供更快的反馈:
async function validateEmail(email: string): Promise<string | null> {
const res = await fetch('https://api.botoi.com/v1/disposable-email/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (data.success && data.data.is_disposable) {
return 'Please use a permanent email address.';
}
return null; // no error
}
中间件仍然充当权威门户。 客户端检查是一种用户体验改进,而不是一种安全措施。
FAQ
- 这可以与 Next.js App Router 一起使用吗?
- 是的。 Next.js 中间件在任何路由处理程序之前运行,无论您使用的是 App Router 还是 Pages Router。 middleware.ts 文件位于项目根目录(如果使用 src 目录,则位于 src/ 内),它会在到达 API 路由或服务器操作之前拦截对匹配路径的请求。
- 我需要 API 密钥来进行 botoi 一次性电子邮件检查吗?
- 不需要。免费套餐每分钟允许 5 个请求,无需 API 密钥。 对于处理更多注册的生产应用程序,请从 botoi API 文档页面获取密钥以解锁更高的速率限制。
- 如果 botoi API 关闭会发生什么?
- 中间件捕获网络错误并让请求通过。 这种故障开放方法意味着临时 API 中断永远不会阻止合法用户注册。 您可以添加日志记录以跟踪回退行为何时启动。
- 我可以将其与 Remix 或 SvelteKit 等其他框架一起使用吗?
- API 调用本身可以在任何服务器端环境中工作。 这里显示的中间件模式是 Next.js 特定的,但核心逻辑(POST 到端点,检查响应中的 is_disposable)直接转换为 Remix 加载器、SvelteKit 挂钩或 Express 中间件。
- 一次性电子邮件检测的准确度如何?
- 端点检查 700 多个已知一次性域的列表,并使用模式匹配来捕获变化。 它还可以将免费电子邮件提供商(Gmail、Outlook、Yahoo)与一次性电子邮件提供商分开识别,以便您可以区分个人 Gmail 和一次性 Mailinator 地址。
开始使用 botoi 构建
150+ 个 API 端点,涵盖查询、文本处理、图片生成和开发者工具。免费套餐,无需信用卡。