Eén module dient ze allebei: die krijgt het IP-adres, de headers van de request en de _kz-cookie mee, zodat Django en Flask elk hun eigen waarden doorgeven. Hij gebruikt requests met één Session per proces, die de verbinding openhoudt; timeout geldt voor het verbinden en voor elke read.
De tag
Zet de tag in de <head> van je basistemplate (base.html), waarop de 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 os import requests korzen = requests.Session() # 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"] def header_order(environ): """The order the request's headers came in, as gunicorn keeps it in the WSGI environ.""" names = [k for k in environ if k.startswith("HTTP_") or k in ("CONTENT_TYPE", "CONTENT_LENGTH")] return [k.removeprefix("HTTP_").lower().replace("_", "-") for k in names[:64]] def decide(ip, headers, environ, token, path): """The answer for this request, or None on any error: then go on as on allow (fail open).""" try: r = korzen.post( "https://korzen.dev/v1/decide", timeout=0.2, headers={"Authorization": f"Bearer {os.environ['KORZEN_API_KEY']}"}, json={ "ip": ip, "user_agent": headers.get("User-Agent"), "headers": {h: headers[h] for h in FORWARDED if h in headers}, "header_order": header_order(environ), "token": token, "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. gunicorn houdt die bij in de HTTP_*-sleutels van de WSGI-environ (request.META in Django, request.environ in Flask), en laat een naam met _ vallen. Achter nginx, een load balancer of Cloudflare is het de volgorde waarin die hop ze doorstuurt, niet die van de browser.
In Django, in de view die het account aanmaakt:
# views.py from django.shortcuts import redirect, render from . import korzen def client_ip(request): # Behind Cloudflare: return request.headers.get("CF-Connecting-IP"). # Behind one proxy of yours that appends to X-Forwarded-For: its last entry. forwarded = request.headers.get("X-Forwarded-For") if forwarded: return forwarded.split(",")[-1].strip() return request.META.get("REMOTE_ADDR") def signup(request): if request.method == "POST": d = korzen.decide(client_ip(request), request.headers, request.META, request.COOKIES.get("_kz"), "/signup") if d and d["action"] == "block": return render(request, "signup.html", {"error": "We could not create your account."}, status=400) # "challenge": your CAPTCHA, or an email or phone check, before going on user = create_account(request.POST) # your own code save_request_id(user, d and d["request_id"]) # see "Keep the request_id" return redirect("welcome") return render(request, "signup.html")
In Flask:
# app.py from flask import Flask, redirect, render_template, request from werkzeug.middleware.proxy_fix import ProxyFix import korzen app = Flask(__name__) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) # one proxy of yours in front @app.post("/signup") def signup(): d = korzen.decide(request.remote_addr, request.headers, request.environ, request.cookies.get("_kz"), "/signup") if d and d["action"] == "block": return render_template("signup.html", error="We could not create your account."), 400 # "challenge": your CAPTCHA, or an email or phone check, before going on user = create_account(request.form) # your own code save_request_id(user, d and d["request_id"]) # see "Keep the request_id" return redirect("/welcome")
Het IP-adres van de bezoeker
Django heeft geen instelling voor proxy's: REMOTE_ADDR is het adres van de verbinding, dat van je proxy als je er een hebt. De client_ip hierboven neemt de laatste entry van X-Forwarded-For, die je proxy heeft toegevoegd; staan er twee eigen proxy's voor, neem dan de entry daarvoor. Achter Cloudflare lees je CF-Connecting-IP. In Flask doet ProxyFix(x_for=1) hetzelfde voor één proxy, en zet het request.remote_addr.
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 python manage.py runserver of flask run 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 (of in je.env, als je er een laadt) 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
- De eerste entry van
X-Forwarded-For. Die schrijft de bezoeker zelf, dus een bot kiest zijn eigen IP-adres. ProxyFixmet een hogerex_fordan het aantal proxy's dat je hebt. Flask vertrouwt dan een entry die de bezoeker heeft geschreven.- Een formulier dat JavaScript verstuurt zodra de pagina geladen is, voordat de tag heeft gerapporteerd: roep
await window.korzen?.ready()aan voordat je het verstuurt. /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.