Korzen
Bot Protection

Next.js

Bot Protection in einem Next.js-Projekt mit App Router: der Tag im Root-Layout, der Aufruf in einer Server Action, die IP des Besuchers auf Vercel und hinter einem Proxy.

Diese Anleitung gilt für den App Router, Next.js 15 und neuer. Sie fügen den Tag dem Root-Layout hinzu und rufen Bot Protection aus der Server Action oder dem Route Handler auf, der das Konto anlegt.

Der Tag

Im Root-Layout, damit jede Seite ihn hat:

// app/layout.js
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
      </head>
      <body>{children}</body>
    </html>
  );
}

Der Aufruf

Der Client ist eine einzige Datei ohne Abhängigkeiten: Er fragt /v1/decide, gibt bei Ihrem Timeout auf und lässt den Besucher bei jedem Fehler durch. Laden Sie ihn nach lib/ und lesen Sie ihn, bevor Sie ihn committen:

curl -o lib/bot-protection.js https://korzen.dev/bot-protection/sdk/korzen.js
curl -o lib/bot-protection.d.ts https://korzen.dev/bot-protection/sdk/korzen.d.ts # types, for TypeScript

Dann eine Funktion für jede geschützte Aktion:

// lib/korzen.js
import { headers } from "next/headers";
import { createClient, fromHeaders, ipFrom } from "./bot-protection.js";

export const client = createClient({ apiKey: process.env.KORZEN_API_KEY, url: "https://korzen.dev", timeoutMs: 200 });

// decide asks Bot Protection about the request being handled. It returns the answer,
// or null on any error: the caller then goes on as on allow (fail open).
export async function decide(path) {
  // ipFrom.vercel on Vercel; see "The visitor's IP"
  return client.decide(fromHeaders(await headers(), { path, ip: ipFrom.vercel }));
}

Dann in der Server Action, die das Konto anlegt:

// app/signup/actions.js
"use server";
import { decide } from "@/lib/korzen";

export async function signUp(prevState, formData) {
  const d = await decide("/signup");
  if (d?.action === "block") return { error: "We could not create your account." };
  if (d?.action === "challenge") {
    // your CAPTCHA, or an email or phone check, before going on
  }
  const user = await createAccount(formData); // your own code
  await saveRequestID(user.id, d?.request_id); // see "Keep the request_id"
  // ...
}

In einem Route Handler umschließt withKorzen ihn: Es antwortet bei block mit 403 und gibt dem Handler die Antwort nach seinen eigenen Argumenten:

// app/api/signup/route.js
import { ipFrom, withKorzen } from "@/lib/bot-protection";
import { client } from "@/lib/korzen";

export const POST = withKorzen(
  async (request, ctx, d) => {
    if (d?.action === "challenge") {
      // your CAPTCHA, or an email or phone check, before going on
    }
    const user = await createAccount(await request.formData()); // your own code
    await saveRequestID(user.id, d?.request_id);
    return Response.json({ ok: true });
  },
  // onChallenge answers nothing, so the handler runs its own check; without it, a
  // challenged post is refused (403)
  { client, ip: ipFrom.vercel, onChallenge: () => undefined },
);

Die IP des Besuchers

Auf Vercel enthält x-forwarded-for die IP des Besuchers: Vercel überschreibt ihn, sodass ein Besucher keinen eigenen schreiben kann. Anderswo lesen Sie den Header, den Ihr eigener Edge setzt: cf-connecting-ip hinter Cloudflare, x-real-ip, wenn Ihr nginx ihn setzt. Hinter einem Proxy, der an x-forwarded-for anhängt, statt ihn zu überschreiben, ist der erste Eintrag das, was der Besucher gesendet hat: Nehmen Sie den letzten, den Ihr Proxy angehängt hat.

Die Aktion anwenden

Lesen Sie nur action. Bei block antworten Sie mit demselben Fehler, den die Aktion zeigt, wenn sie aus einem anderen Grund scheitert, damit ein Bot nichts lernt. Bei challenge nutzen Sie die Prüfung, die Sie schon haben (ein CAPTCHA, ein Code per E-Mail oder Telefon); ohne eine machen Sie weiter. Das Konto beginnt im Monitor-Modus, in dem action immer allow ist: Für Besucher ändert sich nichts, bis Sie den Block-Modus einschalten. Oder schicken Sie den Besucher zu unserer Challenge-Seite unter challenge.url: siehe die Challenge-Seite.

Ohne onChallenge lässt die Middleware keine Challenge durch: Ein Seitenaufruf (GET) geht zu unserer Challenge-Seite, ein abgeschicktes Formular oder ein API-Aufruf wird mit 403 abgelehnt. Schützen Sie auch die Seite, die das Formular zeigt: Dort trifft eine Person auf die Challenge, und ihr Pass lässt das Formular durch. Mit einer eigenen Prüfung geben Sie onChallenge an und führen sie dort aus. Die Middleware schickt die Person an die aufgerufene Adresse zurück, gelesen aus X-Forwarded-Host und X-Forwarded-Proto, wenn Ihr Proxy sie setzt; hinter einem Proxy, der Host: localhost und keinen der beiden weiterreicht (nginx standardmäßig), geben Sie publicURL: "https://shop.example" an.

Die request_id aufbewahren

Jede Antwort hat eine request_id. Bewahren Sie sie bei dem auf, was die Aktion erstellt hat, in einer Spalte des Kontos oder der Bestellung, oder in einer Logzeile mit deren ID. Sie benennt diese Entscheidung, wenn Sie uns dazu etwas fragen, und lässt Sie später herausfinden, welche Antwort ein Fake-Konto durchgelassen hat.

Auf Ihrem Rechner testen

Führen Sie next dev aus und öffnen Sie http://localhost:3000. Seiten auf localhost akzeptieren den Tag immer und erscheinen nie als Domain. Lokal ist die IP 127.0.0.1 oder ::1, und das ist in Ordnung. Läuft die App auf Ihrem Rechner in Docker, sieht sie stattdessen die Adresse der Bridge, etwa 172.17.0.1: Die Challenge-Seite bringt Sie trotzdem zu localhost zurück, denn der Tag hat Ihre Seite dort gesehen.

  1. Setzen Sie KORZEN_API_KEY in .env.local und öffnen Sie die Registrierungsseite. In den Entwicklertools des Browsers erscheint nach ein paar Sekunden das _kz-Cookie.
  2. Registrieren Sie sich. Die Entscheidung erscheint auf der Seite Entscheidungen des Dashboards, unter dem Pfad, den Sie gesendet haben.
  3. Prüfen Sie fail-open: Setzen Sie einen falschen Schlüssel und registrieren Sie sich erneut. Der Aufruf antwortet 401, und die Registrierung geht durch.

Häufige Fehler

  • Den ersten Eintrag von x-forwarded-for hinter einem Proxy lesen, der daran anhängt. Ein Bot wählt dann seine eigene IP.
  • Ein Formular, das JavaScript sendet, sobald die Seite lädt, bevor der Tag berichtet hat. Rufen Sie await window.korzen?.ready() auf, bevor Sie es senden; das Promise wird innerhalb weniger Sekunden aufgelöst, selbst wenn Bot Protection langsam ist.
  • Der Aufruf in einer Middleware. Sie läuft bei jeder Anfrage, und jeder Aufruf zählt als Entscheidung: Rufen Sie ihn nur in der Aktion auf, die Sie schützen.
  • /v1/decide aus dem Browser aufrufen. Der API key würde jeden Besucher erreichen; der Aufruf gehört auf Ihren Server.
  • Kein Timeout, oder ein Fehler, der die Registrierung stoppt. Jede andere Antwort als 200, und keine Antwort rechtzeitig, bedeutet: den Besucher durchlassen.
  • Eine Zeile in Ihrem Log, die mit korzen: the visitor's IP sent to Bot Protection beginnt. Der Client schreibt sie einmal, bei der ersten Entscheidung, deren IP aus Ihrem eigenen Netz stammt (die eines Load Balancers, eines Proxys, eines Containers), was Bot Protection wie keine IP wertet: Setzen Sie ip auf die Adresse des Besuchers (die IP des Besuchers). In Docker auf Ihrem Rechner ist es die Adresse der Bridge, und dort harmlos.