Deze gids is voor FastAPI op Python 3.11 of nieuwer, geserveerd door uvicorn, en werkt hetzelfde in elke Starlette-app. De call gebruikt httpx met één AsyncClient per proces, die de verbinding open houdt; asyncio.timeout begrenst de hele call, de verbinding inbegrepen, omdat de eigen timeout van httpx voor elke stap apart geldt.
De tag
Zet de tag in de <head> van de template waarop je registratie-, inlog- en checkoutpagina's voortbouwen:
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
De call
# korzen.py import asyncio import os import httpx korzen = httpx.AsyncClient() # one per process: it keeps the connection open # The headers Bot Protection reads: those one browser build sends alike, and Accept-Language. FORWARDED = ["accept", "accept-encoding", "accept-language", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform", "sec-fetch-dest", "sec-fetch-mode", "sec-fetch-site", "upgrade-insecure-requests"] async def decide(request, path): """The answer for this request, or None on any error: then go on as on allow (fail open).""" try: async with asyncio.timeout(0.2): # the whole call, the connection included r = await korzen.post( "https://korzen.dev/v1/decide", headers={"Authorization": f"Bearer {os.environ['KORZEN_API_KEY']}", "Korzen-Deadline-Ms": "200"}, json={ "ip": request.client.host if request.client else None, # see "The visitor's IP" "user_agent": request.headers.get("user-agent"), "headers": {h: request.headers[h] for h in FORWARDED if h in request.headers}, # The order the request's headers came in, as the ASGI server keeps it. "header_order": [name.decode("latin-1").lower() for name, _ in request.scope["headers"][:64]], "token": request.cookies.get("_kz"), "path": path, "method": "POST", }, ) return r.json() if r.status_code == 200 else None except Exception: return None
header_order is de volgorde waarin de headers van het verzoek binnenkwamen: een script dat de waarden van een browser kopieert, kopieert zelden hun volgorde. Anders dan WSGI houdt ASGI die bij, in request.scope["headers"]. Achter nginx, een load balancer of Cloudflare is het de volgorde waarin die hop ze doorstuurt, niet die van de browser.
In de route die het account aanmaakt (Form heeft python-multipart nodig):
# main.py from typing import Annotated from fastapi import FastAPI, Form, Request from fastapi.responses import PlainTextResponse, RedirectResponse import korzen app = FastAPI() @app.post("/signup") async def signup(request: Request, email: Annotated[str, Form()]): d = await korzen.decide(request, "/signup") if d and d["action"] == "block": return PlainTextResponse("We could not create your account.", status_code=400) # "challenge": your CAPTCHA, or an email or phone check, before going on user = await create_account(email) # your own code await save_request_id(user, d and d["request_id"]) # see "Keep the request_id" return RedirectResponse("/welcome", status_code=303)
Het IP-adres van de bezoeker
request.client.host is het adres van de verbinding, tenzij uvicorn de proxy ervoor vertrouwt. uvicorn leest X-Forwarded-For van de adressen die --forwarded-allow-ips noemt (standaard 127.0.0.1) en zet de client op de laatste entry die zij niet hebben toegevoegd: start hem met het adres van je load balancer, zoals uvicorn main:app --forwarded-allow-ips 10.0.0.5, of met het netwerk waarop die staat. Achter Cloudflare lees je in plaats daarvan request.headers.get("cf-connecting-ip"), als je server alleen verkeer van Cloudflare accepteert.
Voer de actie uit
Lees alleen action. Bij block antwoord je met dezelfde foutmelding die de actie toont als hij om een andere reden mislukt, zodat een bot niets leert. Bij challenge gebruik je de controle die je al hebt (een CAPTCHA, een code per e-mail of telefoon); heb je er geen, ga dan door. Het account begint in monitor mode, waar action altijd allow is: voor bezoekers verandert er niets tot je block mode aanzet.
Bewaar de request_id
Elk antwoord heeft een request_id. Bewaar die bij wat de actie heeft aangemaakt, in een kolom van het account of de bestelling, of in een logregel met hun id. Zo weet je welke beslissing het was als je ons ernaar vraagt, en kun je later nagaan welk antwoord een nepaccount binnenliet.
Test het op je eigen machine
Draai uvicorn main:app --reload en open het op localhost. Pagina's op localhost accepteren de tag altijd en staan nooit als domein in de lijst. Lokaal is het IP-adres 127.0.0.1 of ::1, en dat is prima. In Docker op je eigen machine ziet de app in plaats daarvan het adres van de bridge, zoals 172.17.0.1: de challengepagina brengt je toch terug naar localhost, want de tag zag je pagina daar.
- Zet
KORZEN_API_KEYin de omgeving en open de registratiepagina. In de developer tools van de browser verschijnt na een paar seconden de_kz-cookie. - Registreer je. De beslissing verschijnt op de pagina Beslissingen van het dashboard, onder het pad dat je stuurde.
- Controleer fail-open: zet een verkeerde key en registreer je opnieuw. De call antwoordt met
401, en de registratie gaat door.
Veelgemaakte fouten
--forwarded-allow-ips '*'. Dat vertrouwt elke hop, dus de client wordt de eerste entry vanX-Forwarded-For, en die schrijft de bezoeker zelf.- Een nieuwe
httpx.AsyncClient()voor elke call. Elke client opent zijn eigen verbinding, met een TLS-handshake binnen je timeout. - Alleen
timeout=op httpx. Die begrenst elke stap apart (verbinden, schrijven, lezen), dus de call kan meerdere keren zo lang duren;asyncio.timeoutbegrenst het geheel. /v1/decideaanroepen vanuit de browser. De API key zou dan bij elke bezoeker terechtkomen; de call hoort op je server.- Geen timeout, of een fout die de registratie tegenhoudt. Elk ander antwoord dan
200, en geen antwoord op tijd, betekent: laat de bezoeker door.