Jeder Server auf dieser Seite übergibt Ihrem Code den Request der Fetch API, also dient ein Client allen: korzen.js, eine einzige Datei ohne Abhängigkeiten, die /v1/decide fragt, bei Ihrem Timeout aufgibt und den Besucher bei jedem Fehler durchlässt. Nur wo die Anfrage ankommt und wo die IP des Besuchers steht, unterscheidet sich. Für Next.js siehe Next.js; für einen Cloudflare Worker Cloudflare Workers.
Der Tag
Setzen Sie den Tag in den <head> des Layouts, das Ihre Registrierungs-, Anmelde- und Checkout-Seiten rendert:
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
Rufen Sie auf der Seite vor dem Senden des Formulars await window.korzen?.ready() auf: Es meldet, was der Besucher seit der letzten Meldung des Tags getan hat, und wird in jedem Fall innerhalb weniger Sekunden aufgelöst. Der Tag
Der Client
Laden Sie ihn in den Ordner Ihres Server-Codes und lesen Sie ihn, bevor Sie ihn committen:
curl -o bot-protection.js https://korzen.dev/bot-protection/sdk/korzen.js curl -o bot-protection.d.ts https://korzen.dev/bot-protection/sdk/korzen.d.ts # types, for TypeScript
Dann daneben ein Client für den Prozess. Die Beispiele unten importieren ihn aus korzen.js; in React Router nennen Sie ihn app/korzen.server.js, in SvelteKit src/lib/server/korzen.js, damit er nie ins Bundle des Browsers gelangt:
// korzen.js: one client for the process import { createClient } from "./bot-protection.js"; export const korzen = createClient({ apiKey: process.env.KORZEN_API_KEY, url: "https://korzen.dev", timeoutMs: 200 }); export { fromHeaders, fromRequest, ipFrom } from "./bot-protection.js";
korzen.decide(fromRequest(request, { ip })) liest den User-Agent der Anfrage, die Header, die Bot Protection liest, das _kz-Cookie, den Pfad und die Methode, und liefert die Antwort, oder null bei jedem Fehler. Es liest keinen Body, sodass Ihr Code das Formular danach noch liest. Die Headers der Fetch API halten keine Reihenfolge, also wird keine Header-Reihenfolge gesendet.
Hono
Eine Middleware auf der Route, die das Konto anlegt; dieselbe App läuft auf Node, Bun und Deno:
// app.js import { Hono } from "hono"; import { serve } from "@hono/node-server"; // on Bun or Deno, serve app.fetch with their own server import { fromRequest, ipFrom, korzen } from "./korzen.js"; const app = new Hono(); // Asks before the handler, and keeps the answer as c.get("korzen") (null when there is none). async function guard(c, next) { const d = await korzen.decide(fromRequest(c.req.raw, { ip: ipFrom.lastForwarded })); // see "The visitor's IP" if (d?.action === "block") return c.text("We could not create your account.", 400); // "challenge": your CAPTCHA, or an email or phone check, before going on c.set("korzen", d); await next(); } app.post("/signup", guard, async (c) => { const user = await createAccount(await c.req.parseBody()); // your own code await saveRequestID(user.id, c.get("korzen")?.request_id); // see "Keep the request_id" return c.redirect("/welcome", 303); }); serve({ fetch: app.fetch, port: 3000 });
Bun
Mit den Routen von Bun.serve (Bun 1.2.3 und neuer) liefert der Server die Adresse der Verbindung:
// server.js: bun run server.js import { fromRequest, korzen } from "./korzen.js"; Bun.serve({ port: 3000, routes: { "/signup": { POST: async (req, server) => { const d = await korzen.decide(fromRequest(req, { ip: server.requestIP(req)?.address })); // see "The visitor's IP" if (d?.action === "block") return new Response("We could not create your account.", { status: 400 }); // "challenge": your CAPTCHA, or an email or phone check, before going on const user = await createAccount(await req.formData()); // your own code await saveRequestID(user.id, d?.request_id); // see "Keep the request_id" return Response.redirect(new URL("/welcome", req.url), 303); }, }, }, });
Deno
// server.js: deno run --allow-net --allow-env server.js import { fromRequest, korzen } from "./korzen.js"; Deno.serve({ port: 3000 }, async (req, info) => { if (req.method !== "POST" || new URL(req.url).pathname !== "/signup") return new Response("Not found", { status: 404 }); const d = await korzen.decide(fromRequest(req, { ip: info.remoteAddr.hostname })); // see "The visitor's IP" if (d?.action === "block") return new Response("We could not create your account.", { status: 400 }); // "challenge": your CAPTCHA, or an email or phone check, before going on const user = await createAccount(await req.formData()); // your own code await saveRequestID(user.id, d?.request_id); // see "Keep the request_id" return Response.redirect(new URL("/welcome", req.url), 303); });
React Router und Remix
In der Action der Route, die das Konto anlegt (der Framework-Modus von React Router, zu dem Remix wurde):
// app/routes/signup.jsx import { data, redirect } from "react-router"; import { fromRequest, ipFrom, korzen } from "../korzen.server.js"; export async function action({ request }) { const d = await korzen.decide(fromRequest(request, { ip: ipFrom.lastForwarded })); // see "The visitor's IP" if (d?.action === "block") return data({ error: "We could not create your account." }, { status: 400 }); // "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); // see "Keep the request_id" return redirect("/welcome"); }
Astro
Ein Endpoint, der bei Bedarf gerendert wird, mit einem Server-Adapter (etwa @astrojs/node), der clientAddress liefert:
// src/pages/api/signup.js import { fromRequest, korzen } from "../../korzen.js"; export const prerender = false; export async function POST({ request, clientAddress, redirect }) { const d = await korzen.decide(fromRequest(request, { ip: clientAddress, path: "/signup" })); // see "The visitor's IP" if (d?.action === "block") return new Response("We could not create your account.", { status: 400 }); // "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); // see "Keep the request_id" return redirect("/welcome", 303); }
SvelteKit
In der Form Action der Registrierungsseite; getClientAddress() ist die IP, die Ihr Adapter liest:
// src/routes/signup/+page.server.js import { fail, redirect } from "@sveltejs/kit"; import { fromRequest, korzen } from "$lib/server/korzen.js"; export const actions = { default: async ({ request, getClientAddress }) => { const d = await korzen.decide(fromRequest(request, { ip: getClientAddress() })); // see "The visitor's IP" if (d?.action === "block") return fail(400, { error: "We could not create your account." }); // "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); // see "Keep the request_id" redirect(303, "/welcome"); }, };
Nuxt
Eine Server-Route. fromHeaders(event.headers) liest die Header der Anfrage und überlässt ihren Body readFormData; ein zweiter Request, mit toWebRequest(event) gebaut, ließe das Formular ungelesen:
// server/api/signup.post.js import { fromHeaders, ipFrom, korzen } from "../utils/korzen.js"; export default defineEventHandler(async (event) => { const d = await korzen.decide(fromHeaders(event.headers, { ip: ipFrom.lastForwarded, path: "/api/signup" })); // see "The visitor's IP" if (d?.action === "block") { setResponseStatus(event, 400); return "We could not create your account."; } // "challenge": your CAPTCHA, or an email or phone check, before going on const user = await createAccount(await readFormData(event)); // your own code await saveRequestID(user.id, d?.request_id); // see "Keep the request_id" return sendRedirect(event, "/welcome", 303); });
Die IP des Besuchers
Senden Sie die Adresse, die Ihr eigener Proxy gesehen hat, nie eine, die der Besucher geschrieben haben könnte. Steht nichts vor Ihrem Server, ist das die Adresse der Verbindung: server.requestIP(req) in Bun, info.remoteAddr in Deno. Hinter einem Load Balancer oder nginx von Ihnen ist es der letzte Eintrag von X-Forwarded-For, den Ihr Proxy angehängt hat: ipFrom.lastForwarded. Hinter Cloudflare liest ipFrom.cloudflare den Header CF-Connecting-IP, wenn Ihr Server nur Traffic von Cloudflare annimmt.
clientAddress in Astro und getClientAddress() in SvelteKit sind, was der Adapter liest. Mit @sveltejs/adapter-node hinter einem Proxy setzen Sie ADDRESS_HEADER=X-Forwarded-For und XFF_DEPTH=1 (ein Proxy von Ihnen), wenn jede Anfrage über diesen Proxy kommt: Der Adapter lehnt eine Anfrage ohne den Header ab. In Nuxt nimmt getRequestIP(event, { xForwardedFor: true }) von h3 den ersten Eintrag, den der Besucher schreibt: Verwenden Sie ipFrom.lastForwarded, wie oben.
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: challengeRedirect(d, request.url) liefert die Adresse und das Cookie, mit denen Sie eine 303 beantworten (siehe die Challenge-Seite).
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. Wenn sich das Konto als gut oder schlecht herausstellt, melden Sie es zurück: await korzen.feedback([{ request_id, outcome: "chargeback" }]) (siehe Feedback).
Auf Ihrem Rechner testen
Starten Sie Ihren Dev-Server und öffnen Sie ihn auf localhost. 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; mit ipFrom.lastForwarded und keinem Proxy davor gibt es keine, und auch das ist in Ordnung.
- Setzen Sie
KORZEN_API_KEYin der Umgebung des Servers und öffnen Sie die Registrierungsseite. In den Entwicklertools des Browsers erscheint nach ein paar Sekunden das_kz-Cookie. - Registrieren Sie sich. Die Entscheidung erscheint auf der Seite Entscheidungen des Dashboards, unter dem Pfad, den Sie gesendet haben.
- 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
- Der erste Eintrag von
X-Forwarded-For, wieipFrom.verceloderxForwardedForvon h3 ihn lesen, hinter einem Proxy, der an den Header anhängt. Der Besucher schreibt diesen Eintrag, also wählt ein Bot seine eigene IP.ipFrom.vercelist nur auf Vercel richtig, das den Header überschreibt. - Der Client, importiert in Code, der den Browser erreicht. Der API key würde jeden Besucher erreichen; halten Sie ihn in einem Modul, das nur auf dem Server läuft.
- Eine Astro-Seite, die vorgerendert bleibt. Ohne
prerender = false(oderoutput: "server") hat der Endpoint keine Anfrage zu beurteilen, undclientAddresswirft einen Fehler. - Kein Timeout, oder ein Fehler, der die Registrierung stoppt. Jede andere Antwort als
200, und keine Antwort rechtzeitig, bedeutet: den Besucher durchlassen.