Перейти к содержимому
Integration

Блокируйте одноразовые электронные письма в Next.js с помощью одного промежуточного файла

| 6 min read

Промежуточное программное обеспечение Next.js из 40 строк, которое вызывает API-интерфейс botoi для отклонения регистрации с временных адресов электронной почты. Скопируйте, вставьте, разверните.

Email inbox interface on a laptop
Photo by Stephen Phillips on Unsplash

Пользователь регистрируется с помощью test92847@mailinator.com, сгорает ваша бесплатная пробная версия и исчезает. Они вернутся завтра с test92848@mailinator.com и сделай это снова. Ваша очередь поддержки заполняется фантомными учетными записями. Ваша аналитика показывает завышенное количество пользователей, которое ничего не значит. Обнаружение злоупотреблений срабатывает слишком поздно, поскольку учетная запись уже израсходовала ресурсы.

Исправление: заблокируйте одноразовые электронные письма у дверей до того, как запрос на регистрацию достигнет вашей базы данных. В этом руководстве показано, как это сделать в Next.js с одним файлом промежуточного программного обеспечения и нулевыми зависимостями, кроме вызова выборки.

Что ты построишь

Промежуточное программное обеспечение Next.js, которое перехватывает запросы POST к вашей конечной точке регистрации. извлекает электронное письмо из тела запроса, сверяет его с API одноразовой электронной почты botoi, и возвращает ответ 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'],
};

Как это работает

Сопоставление маршрутов

The 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"
  }
}

The is_disposable флаг — ворота. Когда это true, промежуточное программное обеспечение возвращает 422 с четким сообщением. Когда это false, NextResponse.next() позволяет запросу продолжить работу с вашим обработчиком регистрации.

Открытая конструкция

The catch Блокировка вызова выборки означает, что сбои сети, таймауты или простои API не нарушают регистрацию. Промежуточное программное обеспечение регистрирует предупреждение и пропускает запрос. Ваши пользователи никогда не увидят ошибок, вызванных сторонним сбоем.

Обработка пограничных случаев

Таймауты

API botoi отвечает на большинство запросов менее чем за 50 мс, поскольку он использует список доменов в памяти. Если вам нужен жесткий тайм-аут, заверните выборку в 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
}

Промежуточное программное обеспечение по-прежнему действует как авторитетный шлюз. Проверка на стороне клиента — это улучшение UX, а не мера безопасности.

FAQ

Работает ли это с маршрутизатором приложений Next.js?
Да. Промежуточное программное обеспечение Next.js запускается перед любым обработчиком маршрута, независимо от того, используете ли вы App Router или Pages Router. Файл middleware.ts находится в корне проекта (или внутри src/, если вы используете каталог src) и перехватывает запросы по совпадающим путям до того, как они достигнут ваших маршрутов API или действий сервера.
Нужен ли мне ключ API для одноразовой проверки электронной почты botoi?
Нет. Уровень бесплатного пользования допускает 5 запросов в минуту без ключа API. Чтобы рабочие приложения обрабатывали больше регистраций, получите ключ на странице документации API botoi, чтобы разблокировать более высокие ограничения скорости.
Что произойдет, если API ботои не работает?
Промежуточное программное обеспечение улавливает сетевые ошибки и пропускает запрос. Такой отказоустойчивый подход означает, что временное отключение API никогда не блокирует регистрацию законных пользователей. Вы можете добавить ведение журнала, чтобы отслеживать, когда срабатывает резервное поведение.
Могу ли я использовать это с другими фреймворками, такими как Remix или SvelteKit?
Сам вызов API работает из любой серверной среды. Показанный здесь шаблон промежуточного программного обеспечения специфичен для Next.js, но основная логика (POST до конечной точки, проверка is_disposable в ответе) транслируется непосредственно в загрузчики Remix, перехватчики SvelteKit или промежуточное программное обеспечение Express.
Насколько точно обнаружение одноразовой электронной почты?
Конечная точка сверяется со списком из более чем 700 известных одноразовых доменов и использует сопоставление с образцом для выявления вариантов. Он также идентифицирует бесплатных поставщиков электронной почты (Gmail, Outlook, Yahoo) отдельно от одноразовых, поэтому вы можете отличить личный Gmail от одноразового адреса Mailinator.

Начните разработку с botoi

150+ API-эндпоинтов для поиска, обработки текста, генерации изображений и утилит для разработчиков. Бесплатный тариф, без банковской карты.