Questa guida è per FastAPI su Python 3.11 o successivi, servito da uvicorn, e funziona allo stesso modo in qualsiasi app Starlette. La chiamata usa httpx con un AsyncClient per processo, che tiene aperta la connessione; asyncio.timeout limita l'intera chiamata, connessione compresa, perché il timeout di httpx vale per ogni passaggio separatamente.
Il tag
Metti il tag nel <head> del template che le pagine di registrazione, login e checkout estendono:
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
La chiamata
# 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 è l'ordine in cui sono arrivati gli header della richiesta: uno script che copia i valori di un browser ne copia di rado l'ordine. A differenza di WSGI, ASGI lo tiene, in request.scope["headers"]. Dietro nginx, un load balancer o Cloudflare, è l'ordine in cui quel passaggio li inoltra, non quello del browser.
Nella route che crea l'account (Form richiede python-multipart):
# 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)
L'IP del visitatore
request.client.host è l'indirizzo della connessione, a meno che uvicorn non si fidi del proxy davanti. uvicorn legge X-Forwarded-For dagli indirizzi che --forwarded-allow-ips indica (127.0.0.1 di default) e imposta il client sull'ultima voce che non hanno aggiunto loro: avvialo con l'indirizzo del tuo load balancer, come uvicorn main:app --forwarded-allow-ips 10.0.0.5, o con la rete in cui si trova. Dietro Cloudflare, leggi invece request.headers.get("cf-connecting-ip"), se il tuo server accetta traffico solo da Cloudflare.
Applica l'azione
Leggi solo action. Su block, rispondi con lo stesso errore che l'azione mostra quando fallisce per qualsiasi altro motivo, così il bot non impara niente. Su challenge, usa la verifica che hai già (un CAPTCHA, un codice via email o telefono); se non ne hai, prosegui. L'account parte in monitor mode, dove action è sempre allow: per i visitatori non cambia niente finché non attivi il block mode.
Conserva il request_id
Ogni risposta ha un request_id. Conservalo insieme a ciò che l'azione ha creato, in una colonna dell'account o dell'ordine, o in una riga di log con il loro id. Identifica quella decisione quando ci chiedi informazioni, e ti permette di scoprire in seguito quale risposta ha fatto entrare un account falso.
Provalo sulla tua macchina
Avvia uvicorn main:app --reload e aprilo su localhost. Le pagine su localhost accettano sempre il tag e non compaiono mai tra i domini. In locale l'IP è 127.0.0.1 o ::1, e va bene così. In Docker sulla tua macchina, l'app vede invece l'indirizzo del bridge, come 172.17.0.1: la pagina di challenge ti riporta comunque su localhost, perché il tag ha visto lì la tua pagina.
- Imposta
KORZEN_API_KEYnell'ambiente e apri la pagina di registrazione. Negli strumenti per sviluppatori del browser, il cookie_kzcompare dopo qualche secondo. - Registrati. La decisione compare nella pagina Decisioni della dashboard, sotto il path che hai inviato.
- Verifica il fail-open: imposta una chiave sbagliata e registrati di nuovo. La chiamata risponde
401, e la registrazione va a buon fine.
Errori comuni
--forwarded-allow-ips '*'. Si fida di ogni hop, quindi il client diventa la prima voce diX-Forwarded-For, che scrive il visitatore.- Un nuovo
httpx.AsyncClient()per ogni chiamata. Ognuno apre la propria connessione, con un handshake TLS dentro il tuo timeout. - Solo
timeout=su httpx. Limita ogni passaggio separatamente (connessione, scrittura, lettura), quindi la chiamata può durare più volte tanto;asyncio.timeoutla limita tutta. - Chiamare
/v1/decidedal browser. L'API key arriverebbe a ogni visitatore; la chiamata va fatta dal tuo server. - Nessun timeout, o un errore che blocca la registrazione. Qualsiasi risposta diversa da
200, o nessuna risposta in tempo, significa: lascia passare il visitatore.