Esta guía usa solo la biblioteca estándar: net/http y un http.Client para el proceso, que mantiene las conexiones abiertas. Su Timeout limita la llamada entera, desde la conexión hasta la lectura del cuerpo.
El tag
Pon el tag en el <head> del layout que renderiza tus páginas de registro, inicio de sesión y checkout:
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
En la página, antes de enviar el formulario, llama a await window.korzen?.ready(): informa lo que el visitante hizo desde el último informe del tag y se resuelve en pocos segundos pase lo que pase. El tag
La llamada
El cliente es un solo archivo con la biblioteca estándar y nada más, korzen.go: pregunta a /v1/decide, se rinde al llegar a tu timeout y deja pasar al visitante ante cualquier error. Descárgalo en un paquete de tu módulo y léelo antes de hacer commit:
mkdir -p internal/korzen curl -o internal/korzen/korzen.go https://korzen.dev/bot-protection/sdk/korzen.go
Un Client para el proceso: mantiene abiertas sus conexiones, y su Timeout limita la llamada entera. Protect pregunta antes del handler que crea la cuenta y deja la respuesta en el contexto de la solicitud:
var bot = &korzen.Client{APIKey: os.Getenv("KORZEN_API_KEY"), URL: "https://korzen.dev", Timeout: 200 * time.Millisecond} mux.Handle("POST /signup", bot.Protect(http.HandlerFunc(signup), korzen.Options{ IP: korzen.RemoteIP, // see "The visitor's IP" OnBlock: func(w http.ResponseWriter, r *http.Request, d korzen.Decision) bool { http.Error(w, "We could not create your account.", http.StatusBadRequest) return true }, // Your own check for challenge runs in signup: return false to go on to it. Without // OnChallenge, a challenged post is refused (403). OnChallenge: func(http.ResponseWriter, *http.Request, korzen.Decision) bool { return false }, })) func signup(w http.ResponseWriter, r *http.Request) { d, _ := korzen.FromContext(r.Context()) // no answer: the zero Decision if d.Action == korzen.Challenge { // your CAPTCHA, or an email or phone check, before going on } user, err := createAccount(r) // your own code // ... saveRequestID(user.ID, d.RequestID) // see "Keep the request_id" }
Para preguntar dentro de un handler: d, err := bot.Decide(r.Context(), korzen.FromHTTP(r, korzen.RemoteIP(r))). Ante cualquier error, d.Action es allow, y err dice por qué, para tu log. Una respuesta cuyo action no es allow, challenge o block también cuenta como error; el *korzen.Problem de una respuesta de error está en err (errors.As). Para registrar cada llamada, define el OnAnswer del cliente: recibe el estado, Korzen-Request-Id y los headers RateLimit.
La IP del visitante
r.RemoteAddr es la dirección de la conexión: la de tu proxy, si tienes uno. korzen.RemoteIP la lee: úsalo cuando nada esté delante de tu servidor. korzen.CloudflareIP lee CF-Connecting-IP, lo que solo es correcto si tu servidor recibe tráfico únicamente de Cloudflare; si no, un visitante podría escribirla. Detrás de tu propio load balancer, korzen.LastForwardedIP toma la última entrada de X-Forwarded-For, la que él añadió.
Actúa según la action
Lee solo action. Con block, responde con el mismo error que la acción muestra cuando falla por cualquier otro motivo, para que un bot no aprenda nada. Con challenge, usa la verificación que ya tienes (un CAPTCHA, un código por email o teléfono); si no tienes ninguna, sigue adelante. La cuenta empieza en monitor mode, donde action es siempre allow: nada cambia para los visitantes hasta que actives el block mode. O envía al visitante a nuestra página de desafío, en challenge.url: consulta la página de desafío.
Sin OnChallenge, el middleware no deja pasar ningún desafío: una carga de página (GET) va a nuestra página de desafío, y el envío de un formulario o una llamada a la API se rechaza con 403. Protege también la página que muestra el formulario, para que la persona encuentre el desafío allí y su pase deje pasar el envío. Con una verificación propia, pasa OnChallenge y ejecútala allí. El middleware devuelve a la persona a la dirección que pidió, leída de X-Forwarded-Host y X-Forwarded-Proto cuando tu proxy los pone; detrás de un proxy que reenvía Host: localhost y ninguno de los dos (lo que nginx hace por defecto), da PublicURL: "https://shop.example".
Guarda el request_id
Cada respuesta trae un request_id. Guárdalo con lo que la acción creó, en una columna de la cuenta o del pedido, o en una línea de log con su id. Sirve para identificar esa decisión cuando nos preguntes por ella y para averiguar después qué respuesta dejó entrar una cuenta falsa. Cuando la cuenta resulte buena o mala, avísanos: bot.Feedback(ctx, []korzen.Feedback{{RequestID: id, Outcome: "chargeback"}}, korzen.Batch{}) (consulta feedback).
Pruébalo en tu máquina
Ejecuta go run . y abre tu servidor en localhost. Las páginas en localhost siempre aceptan el tag y nunca aparecen como dominio. En local la IP es 127.0.0.1 o ::1, y es normal. En Docker en tu máquina, la app ve en su lugar la dirección del bridge, como 172.17.0.1: la página de desafío te devuelve igual a localhost, porque el tag vio tu página allí.
- Define
KORZEN_API_KEYen el entorno y abre la página de registro. En las herramientas de desarrollo del navegador, la cookie_kzaparece tras unos segundos. - Regístrate. La decisión aparece en la página Decisiones del panel, con el path que enviaste.
- Comprueba el fail open: pon una clave incorrecta y regístrate de nuevo. La llamada responde
401, y el registro pasa.
Errores comunes
- Un
HTTPClientcon unhttp.Transportnuevo por llamada, o el de la biblioteca estándar por defecto, que mantiene dos conexiones abiertas. Cada nueva es una conexión TLS nueva: dejaHTTPClientvacío, o dale al tuyokorzen.NewTransport(), una vez para el proceso. - Leer
CF-Connecting-IPen un servidor que Cloudflare no tiene delante. Un bot escribe la cabecera y elige su propia IP. - Llamar a
/v1/decidedesde el navegador. La API key quedaría expuesta a cada visitante; la llamada se hace desde tu servidor. - No poner timeout, o dejar que un error detenga el registro. Cualquier respuesta distinta de
200, o la falta de respuesta a tiempo, significa: deja pasar al visitante. - Una línea en tu log que empieza por
korzen: the visitor's IP sent to Bot Protection. El cliente la escribe una vez, en la primera decisión cuya IP es de tu propia red (la de un balanceador de carga, un proxy, un contenedor), que Bot Protection juzga como sin IP: pon enOptions.IPla dirección del visitante (la IP del visitante). En Docker en tu máquina es la dirección del bridge, y ahí no hace daño.