Next.js-Formular vor Bots schützen, ohne Rätsel
Setzen Sie das Script-Tag in das Layout, lassen Sie ein POST-Formular das versteckte Feld antisy-token an eine Route Ihrer Anwendung senden, und prüfen Sie es auf dem Server mit POST /v1/siteverify: Die Antwort ist allow oder deny. Eine Funktion ohne Abhängigkeit, ein Secret, das auf dem Server bleibt.
Stand: .
Was Sie brauchen
- Eine Website, im Dashboard von Antisy (auf Französisch) angelegt, mit den Hostnamen Ihrer Seiten.
- Der Site-Key (32 Zeichen): Er ist öffentlich und kommt in die Seite; das Verifizierungs-Secret (64 Zeichen) bleibt auf Ihrem Server und wird nur einmal angezeigt, bei seiner Erstellung.
- Seiten, die über HTTPS ausgeliefert werden.
- Eine Next.js-Anwendung mit dem App Router, deren Routen
route.tsdas Formular empfangen. - Node 22 oder neuer: Das verlangt die Funktion der Dokumentation, die keine Abhängigkeit hat. Ihr Import setzt die Option
allowJsdertsconfig.jsonvoraus, die bei Anwendungen aus create-next-app eingeschaltet ist.
Ist das heute verfügbar?
Der Zugang erfolgt über eine Warteliste. Die API und das Dashboard sind seit dem 27. Oktober 2026 in Betrieb, gehostet bei Infomaniak in Genf; noch ist kein Kunde in Produktion.
Die Beispiele dieser Anleitung folgen der Version 1 der API, wie die Dokumentation sie beschreibt. Um sie auszuprobieren, ohne ein echtes Formular zu schützen, beschreibt die Dokumentation Testschlüssel; ein Testschlüssel kommt nie in die Produktion.
Siehe: Dokumentation: Testschlüssel · Der Pilot · Der Warteliste beitreten
1. Die zwei Werte
Der Site-Key ist öffentlich, das Secret verlässt nie den Server. Keiner der beiden trägt das Präfix NEXT_PUBLIC_: Next.js kopiert solche Variablen in den Code, der an den Browser geht, und genau das soll für das Secret nicht passieren.
# .env.local: nie im Repository, nie mit dem Präfix NEXT_PUBLIC_.
ANTISY_SITE_KEY=VOTRE_CLE_DE_SITE
ANTISY_SECRET=VOTRE_SECRET_DE_VERIFICATION2. Das Script-Tag im Layout
Das Script-Tag wird von einer Server-Komponente geschrieben, mit defer, und liest den Site-Key aus der Umgebung des Servers.
// app/layout.tsx
import type { ReactNode } from "react";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="fr">
<head>
{/* Der Site-Key ist öffentlich: Er kommt in die Seite. Das Secret nie. */}
<script src="https://api.antisy.com/v1/i/l.js" data-site={process.env.ANTISY_SITE_KEY} defer />
</head>
<body>{children}</body>
</html>
);
}Diese Form verwendet antisy.com für die eigenen Formulare. Nur die Seiten mit einem geschützten Formular brauchen das Script-Tag: Sie können es in das Layout des betreffenden Segments setzen statt in das Root-Layout.
3. Das Formular
Ein gewöhnliches HTML-Formular per POST an eine Route Ihrer Anwendung, ohne Zustand und ohne JavaScript auf Ihrer Seite:
// app/contact/page.tsx: ein gewöhnliches HTML-Formular, ohne Zustand und ohne JavaScript auf Ihrer Seite.
export default function ContactPage() {
return (
<form method="post" action="/api/contact">
<label>
Nom <input name="name" autoComplete="name" />
</label>
<label>
E-mail <input name="email" type="email" autoComplete="email" />
</label>
<label>
Message <textarea name="message" />
</label>
<button>Envoyer</button>
</form>
);
}Beim Absenden hält das Script das Formular zurück, bis es ein Token hat, legt das versteckte Feld antisy-token an, füllt es aus und sendet erneut (höchstens 6 Sekunden). Für eine Übermittlung per fetch oder eine Single-Page-Anwendung beschreibt die Seite zum Script antisy.token(): Diese Anleitung behandelt sie nicht. Mit einer Server Action wurde sie nicht ausprobiert.
4. Die Prüfung in einer Route
Speichern Sie die Node-Funktion der Dokumentation unverändert in lib/antisy.mjs: Sie liest das Secret aus process.env.ANTISY_SECRET und gibt allow oder deny zurück.
// antisy.mjs: Node 22 oder neuer, ohne Abhängigkeit.
// Gibt "allow" oder "deny" zurück.
// Antisy antwortet nicht (Netz, Zeitlimit, 5xx): "allow". Alles andere Ungewisse: "deny".
// Der 429 ist Ihre Entscheidung. Standardmässig "allow", wie bei einem Ausfall;
// für ein sensibles Formular setzen Sie "deny" (siehe die Seite zur Verifizierung).
const ON_RATE_LIMIT = "allow";
export async function antisyVerdict(token) {
// 1. Ein vorhandenes Token muss die Form eines Tokens haben: 1 bis 2048 druckbare ASCII-Zeichen.
// Sonst wird Antisy nicht aufgerufen und nichts durchgelassen.
// Ein leeres oder fehlendes Feld (Script blockiert) geht als leere Zeichenkette: Antisy liest es als fehlendes Token.
if (token && !/^[\x20-\x7e]{1,2048}$/.test(token)) {
console.warn("Jeton mal formé, envoi non vérifié");
return "deny";
}
let response;
try {
response = await fetch("https://api.antisy.com/v1/siteverify", {
method: "POST",
// fetch folgt Weiterleitungen standardmässig: Eine 3xx würde den Body samt Secret anderswohin wiederholen.
// Hier ist sie ein Fehler Ihres Aufrufs (weiter unten abgelehnt).
redirect: "manual",
// Ein URLSearchParams-Body geht als application/x-www-form-urlencoded.
body: new URLSearchParams({
secret: process.env.ANTISY_SECRET, // Ihr Secret, nie in der Seite
response: token ?? "", // das Feld antisy-token, wie erhalten
}),
signal: AbortSignal.timeout(3000), // nach 3 s antwortet Antisy nicht
});
} catch (error) {
console.warn("Antisy injoignable, envoi laissé passer :", error.name);
return "allow";
}
if (response.status >= 500) {
console.warn("Antisy a répondu " + response.status + ", envoi laissé passer");
return "allow";
}
if (response.status === 429) {
console.warn("Antisy a répondu 429, décision : " + ON_RATE_LIMIT);
return ON_RATE_LIMIT;
}
if (!response.ok) {
// 400, 401, 413, 415 ...: Der Fehler liegt in Ihrem Aufruf. Zu beheben, nie ein Freipass.
console.error("Antisy a refusé la requête (" + response.status + "), envoi non vérifié");
return "deny";
}
const result = await response.json().catch(() => null);
if (typeof result?.success !== "boolean") {
console.error("Réponse d'Antisy illisible, envoi non vérifié");
return "deny";
}
// Ein fehlendes oder abgelehntes Secret ist kein HTTP-Fehler: Die Antwort ist eine Ablehnung, mit 200.
const codes = result["error-codes"] ?? [];
if (codes.includes("missing-input-secret") || codes.includes("invalid-input-secret")) {
console.error("Antisy refuse le secret : " + codes.join(", "));
}
// success ist die einzige Annahme: Es ist genau dann true, wenn verdict "allow" ist.
return result.success ? "allow" : "deny";
}// app/api/contact/route.ts
import { antisyVerdict } from "../../../lib/antisy.mjs";
export async function POST(request: Request) {
const form = await request.formData();
const token = form.get("antisy-token"); // eine Zeichenkette, eine Datei oder nichts
// Fehlendes Feld (Script blockiert): leere Zeichenkette, Antisy entscheidet. Eine Datei ist kein Token: Ablehnung.
const verdict = token === null || typeof token === "string" ? await antisyVerdict(token ?? "") : "deny";
if (verdict === "deny") {
return new Response("Envoi refusé.", { status: 403 }); // allgemeine Ablehnung, ohne Begründung
}
// … die Nachricht speichern oder senden
return new Response(null, { status: 303, headers: { Location: "/merci" } });
}request.formData()liest ein Formular, das alsapplication/x-www-form-urlencodedoder alsmultipart/form-datagesendet wird.- Das Feld wird mit einem leeren Standardwert gelesen: Ist das Script blockiert, fehlt es, und Antisy liest ein leeres Token als fehlendes Token.
- Die Ablehnung ist eine allgemeine Antwort
403, ohne den Grund.
Speichern Sie eine Funktion, die die Prüfung aufruft, nie in einem Cache (ein selbst gebautes Memo, unstable_cache, die Direktive "use cache"): Ein Ergebnis gilt für ein Token, und nur für dieses.
5. Mit einer CSP-Richtlinie
Sendet Ihre Anwendung eine Richtlinie Content-Security-Policy, fügen Sie https://api.antisy.com zu script-src und zu connect-src hinzu, und 'wasm-unsafe-eval' zu script-src. Mit einem Nonce pro Antwort trägt das Script-Tag ihn:
// app/layout.tsx: mit einer CSP-Richtlinie mit Nonce, gesetzt von proxy.ts (middleware.ts vor Next.js 16)
import { headers } from "next/headers";
import type { ReactNode } from "react";
export default async function RootLayout({ children }: { children: ReactNode }) {
const nonce = (await headers()).get("x-nonce") ?? undefined;
return (
<html lang="fr">
<head>
<script src="https://api.antisy.com/v1/i/l.js" data-site={process.env.ANTISY_SITE_KEY} defer nonce={nonce} />
</head>
<body>{children}</body>
</html>
);
}Next.js verlangt für einen Nonce ein dynamisches Rendering: Das Lesen von headers() macht das Layout dynamisch. Die drei Richtlinien mit Nonce, die funktionieren, ohne 'unsafe-inline', stehen auf der Seite zum Script.
Was antwortet die Prüfung?
Die Antwort ist ein JSON-Objekt. success ist genau dann true, wenn verdict den Wert allow hat. Es gibt nur diese zwei Ausgänge, und die Antwort nennt nie den Grund: Auch die Ablehnung, die Sie dem Besucher zeigen, ist allgemein.
Ein Token ist 60 Sekunden gültig und lässt sich nur einmal prüfen. Ein zweiter Aufruf mit demselben Token wird abgelehnt.
{
"success": true,
"verdict": "allow",
"flags": [],
"error-codes": [],
"challenge_ts": "2026-09-24T10:00:00Z",
"hostname": "shop.example",
"action": "contact",
"event_id": "evt_01J7X4M2Q9K8ZP3R5T6V7W8Y9A"
}Und wenn Antisy nicht antwortet?
Die Funktion lässt die Übermittlung in einem einzigen Fall durch: Antisy antwortet nicht (Netz unterbrochen, Zeitlimit von 3 Sekunden überschritten, Fehler 5xx). Ein Ausfall von Antisy darf Ihr Formular nicht schliessen.
Jeder andere HTTP-Fehler (400, 401, 413, 415) kommt von Ihrem Aufruf: Die Funktion lehnt die Übermittlung ab und protokolliert sie, nie ein Freipass. Ein 429 ist eine Entscheidung, die bei Ihnen liegt: Die Konstante ON_RATE_LIMIT steht standardmässig auf allow; setzen Sie deny für ein sensibles Formular.
Was passiert, wenn das Script blockiert ist?
Eine CSP-Richtlinie, die das Script verweigert, ein Werbeblocker, eine Seite über HTTP oder ein Besucher ohne JavaScript verhindern, dass das Script läuft. Das Formular wird dann trotzdem gesendet, ohne das Feld antisy-token: Lesen Sie es mit einem leeren Standardwert.
Was folgt, hängt von der Einstellung « Collecte du navigateur » (Erfassung im Browser) der Website ab. Ist die Erfassung an, kommt eine Übermittlung ohne Token nicht durch; ist sie aus, wird das fehlende Token nicht angelastet. Eine neue Website beginnt mit ausgeschalteter Erfassung.
Was eine Bot-Prüfung nicht ersetzt
Keine Bot-Prüfung ist perfekt, und Antisy nennt keine Erkennungsrate. Sie kommt zu den anderen Abwehrmassnahmen eines Formulars hinzu, etwa der Begrenzung der Anzahl Übermittlungen; sie ersetzt sie nicht.
Häufige Fragen
Darf das Secret von Antisy in eine Variable NEXT_PUBLIC_?
Nein. Eine Variable NEXT_PUBLIC_ wird in den Code kopiert, der an den Browser geht: Dort wäre das Secret für jeden Besucher lesbar. Nur der Site-Key ist öffentlich; das Secret bleibt in einer Variable des Servers, die die Route liest.
Gibt es ein Antisy-Paket für Next.js?
Das Server-Kit für Node (Express, Fastify, Next.js) wird auf npm veröffentlicht werden; noch ist es nicht veröffentlicht. Für ein einfaches Formular genügt die Funktion der Dokumentation: ein fetch an /v1/siteverify, ohne Abhängigkeit. Das Kit dient vor allem Login und Registrierung.
Lässt sich das Token in einer Server Action prüfen?
Die Prüffunktion hängt nicht an einer Route: Sie nimmt ein Token und gibt allow oder deny zurück, wo immer man sie aufruft. Nicht ausprobiert wurde, wie das Script ein Formular übernimmt, das per Server Action gesendet wird. Im Zweifel fordern Sie das Token selbst mit antisy.token() an.
Siehe auch: PHP-Formular vor Bots schützen, ohne Rätsel · WordPress-Formular vor Bots schützen, ohne Plugin · Alle Anleitungen · Dokumentation: Schritt-für-Schritt-Anleitung