Ningú no tria un proveïdor de captcha pensant en el dia que el voldrà canviar, i tanmateix aquest dia arriba: puja de preu, empitjora la taxa de falsos positius o el client exigeix que les seves dades no passin per segons quin servidor. El que segueix és com integrar-lo des del principi perquè aquest canvi sigui una línia de configuració i no un rastreig per tot el projecte, i on col·locar la verificació perquè serveixi d'alguna cosa. Els exemples van en Laravel perquè és una de les nostres especialitats a AndorraDev, i comparem amb el que ja existeix al seu ecosistema, però l'abstracció que proposem és la mateixa en qualsevol framework.
Posar un captcha és fàcil, canviar-lo és el que fa mal
Integrar Turnstile són quinze minuts. Copies el div, carregues l'script, valides el token contra l'endpoint del proveïdor i llest.
El problema apareix un any després, quan vols canviar. I no és un problema d'esforç, és de superfície de contacte: el proveïdor no apareix en un lloc, apareix en quatre.
El nom del camp canvia. Turnstile envia cf-turnstile-response, reCAPTCHA envia g-recaptcha-response. Aquesta cadena està escrita al teu formulari, a la teva regla de validació i al teu JavaScript.
La URL de l'script canvia, i va al layout o a la vista.
El marcatge canvia. Una classe cf-turnstile no és una classe g-recaptcha.
I el flux canvia entre versions: la versió invisible de reCAPTCHA no pinta cap requadre, s'executa en enviar el formulari i injecta el token, amb la qual cosa necessita un JavaScript que l'anterior no necessitava.
Multiplica això pels formularis que tinguis i ja no és un canvi de proveïdor, és una tarda.
Cinc mètodes basten per abstreure un captcha
El que cal aïllar és exactament el que varia, i resulta ser molt poc:
interface CaptchaDriver
{
public function name(): string;
public function verify(string $token, ?string $ip = null): VerificationResult;
public function siteKey(): string;
public function scriptUrl(): string;
public function responseField(): string;
}
Amb això, canviar de proveïdor és canviar una variable d'entorn, sempre que a la teva aplicació no hi hagi quedat cap cadena literal.
I aquí hi ha la clau pràctica: l'abstracció només funciona si la fas servir sencera. Si pintes el widget a mà i escrius el nom del camp al controlador, tens la interfície posada i l'acoblament intacte. Val la pena revisar-ho abans de donar la integració per tancada: n'hi ha prou amb buscar al projecte les cadenes del proveïdor i comprovar que no en queda cap.
El token es pot reutilitzar si no ho impedeixes
La documentació dels proveïdors acaba en "el token és vàlid", i és on s'atura gairebé tothom. El problema és que vàlid no vol dir nou, i un captcha que accepta el mateix token dues vegades no atura ningú que es prengui la molèstia de mirar.
Un token de captcha és vàlid durant uns minuts. Si el teu servidor només comprova que sigui vàlid, un script pot resoldre el captcha una vegada i reenviar el mateix token moltes vegades dins d'aquesta finestra. Has verificat que hi va haver un humà, no que hi ha un humà a cada enviament.
La protecció és senzilla i cal posar-la a propòsit: guardar una empremta del token en validar-lo i rebutjar-lo si torna a aparèixer.
$cacheKey = 'captcha|' . md5($token);
if (Cache::has($cacheKey)) {
$fail('captcha.used');
return;
}
// ... comprovació contra el proveïdor ...
Cache::put($cacheKey, true, now()->addMinutes(5));
El termini s'ha d'assemblar a la validesa del mateix token. Cinc minuts encaixa bé amb Turnstile, els tokens del qual duren uns tres-cents segons.
La protecció antireutilització és el que impedeix que un testimoni resolt un cop serveixi per a molts enviaments. Si el teu formulari es reenvia sense recarregar la pàgina, neteja el camp ocult del testimoni després de cada enviament perquè el JavaScript en generi un de nou. Són dues línies i deixen la protecció fent la seva feina sense que l'usuari ho noti.
Verifica l'acció, no només la puntuació
Això és específic de la versió que puntua el comportament en comptes de mostrar un requadre, i és on més gent es queda curta.
Aquesta versió retorna tres coses: si el token és vàlid, una puntuació de zero a u, i l'acció que vas declarar en generar-lo. Gairebé tothom comprova la puntuació i s'oblida de l'acció.
El problema és que un token generat en qualsevol pàgina del teu lloc és vàlid en qualsevol altra. Si tens un formulari de subscripció a la newsletter amb llindar baix i un formulari de registre amb llindar alt, un atacant pot minar tokens al barat i gastar-los al car. El mateix amb el nom de domini: si no comproves que el token es va generar al teu lloc, serveix un token generat en un altre.
Comprovar-ho és un argument més en dos llocs. El widget marca el testimoni amb l'acció d'aquest formulari, i la regla exigeix aquesta mateixa acció:
<x-laracaptcha::widget action="register" />
'g-recaptcha-response' => ['required', Captcha::make('recaptcha_v3', 'register')],
Un testimoni generat per a qualsevol altra cosa es rebutja amb action-mismatch, i també un que arribi sense acció. Els proveïdors els testimonis dels quals no la porten, Turnstile entre ells, la ignoren sense que hagis de canviar res en alternar d'un a l'altre.
Un captcha protegeix el pas car, no el formulari
Es tendeix a pensar en el captcha com en un porter de porta: el poses a l'entrada i ja està. Però no tots els formularis costen el mateix d'atacar ni de defensar, i posar el filtre al lloc equivocat molesta els usuaris legítims sense aturar els que importen.
La reacció per defecte és posar el captcha al formulari d'accés. I moltes vegades està malament, perquè l'accés ja està protegit per una altra cosa: per entrar cal encertar una contrasenya, i per a això el límit d'intents és una defensa millor i no molesta ningú.
El que de debò vols protegir és l'operació que et costa diners o reputació. En un projecte nostre el captcha no protegeix l'accés: protegeix el pas previ a l'alta, just abans d'enviar el codi de verificació per correu. El motiu està escrit al mateix controlador i és exacte: per no enviar correus a bots. Si el correu ja existeix, és un accés normal i el captcha ni apareix.
Aquesta és la pregunta correcta: no "quin formulari protegeixo", sinó "quina acció es dispara en enviar-lo". Si l'acció és escriure una fila, el límit de peticions sol bastar. Si és enviar un correu, cridar un model de pagament o crear un recurs en un tercer, aleshores sí.
Decideix què passa quan el proveïdor no respon
Afegir un captcha vol dir ficar un tercer al camí crític dels teus formularis. Gairebé ningú no decideix què ha de passar si aquest tercer cau, i el resultat és que la decisió l'acaba prenent el codi per accident, normalment de la pitjor manera possible.
Verificar un captcha és una crida HTTP a un servei extern. Si aquest servei triga, la teva petició es queda esperant, i si no respon, alguna cosa ha de passar. Les dues opcions:
Fallar tancat: sense verificació no es passa. És el segur i significa que una caiguda del proveïdor tomba els teus formularis.
Fallar obert: si no es pot verificar, es deixa passar i es registra. Mantens el servei i acceptes una finestra en què entra tot.
El que no pot passar és que la decisió la prengui un error no controlat i l'usuari rebi un error del servidor. Una verificació que no es pot completar ha d'arribar a la teva regla de validació com a verificació fallida, no com a excepció.
I hi va al mateix lot el temps màxim d'espera. Cinc segons sobren per a un siteverify; els trenta que heretes del client HTTP si no el fixes et retenen un procés PHP massa estona, i això amb trànsit es nota abans que cap altra cosa.
Hi ha a més un cas intermedi que gairebé sempre s'oblida: una resposta que arriba però no és JSON, la pàgina d'error d'un intermediari per exemple. No és una fallada de connexió, així que cal tractar-la a part, i el correcte és que tampoc passi: descodifica a buit, no porta confirmació d'èxit i per tant falla igual.
Les tres decisions vénen preses de sèrie en el nostre cas: falla tancat, amb cinc segons configurables, i una resposta que no sigui JSON tampoc passa. Si el teu formulari necessita el contrari, es canvia; el que no tindràs mai és un cinc-cents perquè Cloudflare trigui a contestar.
Què hi ha al mercat i en què es diferencia
L'ecosistema està ben assortit i les opcions s'ordenen per acoblament.
Els paquets d'un sol proveïdor són la majoria i funcionen molt bé dins del seu àmbit. ryangjchandler/laravel-cloudflare-turnstile cobreix Turnstile amb una ergonomia molt cuidada, especialment per a tests, on el seu simulador permet forçar fallada o caducitat. anhskohbo/no-captcha és el veterà de reCAPTCHA i acumula milions d'instal·lacions. El compromís de tots dos és el mateix i és el punt d'aquest article: canviar de proveïdor és reescriure formularis, regles i vistes, no canviar una variable.
La llibreria oficial de Google exposa la verificació al detall: acció, nom de domini i llindar s'exigeixen a la mateixa crida. El que no porta és res de Laravel: ni regla de validació, ni component, ni facade. La integració l'escrius tu i quedes lligat a reCAPTCHA.
El camp trampa és una altra capa diferent i molt infravalorada: un camp ocult que un humà no omple mai, més una marca temporal. Zero JavaScript de tercers, zero problema amb la normativa de dades i cost operatiu nul. Frena bots genèrics, no frena bots dirigits. No competeix amb un captcha, s'hi suma.
I el límit de peticions ja el tens al framework. No distingeix un humà d'un bot, només limita la freqüència, i per a molts formularis és tot el que calia.
I si no treballes amb Laravel
Els criteris són els mateixos en qualsevol llenguatge:
- Aïlla el que varia, que és el nom del camp, la clau pública, la URL de l'script i el marcatge. Són quatre coses.
- I després fes-ho servir de debò. Una interfície amb cadenes literals repartides per les vistes no et salvarà el dia del canvi.
- Impedeix la reutilització del token. Una empremta a la memòria cau i llest.
- Comprova l'acció i el domini, no només la puntuació.
- Posa el captcha davant de l'operació cara, no davant de tots els formularis.
- I decideix a propòsit què passa quan el proveïdor no respon, amb un temps màxim d'espera fixat per tu.
On és el codi
El paquet és a Packagist, el codi a GitHub i la documentació del paquet recull la configuració dels tres proveïdors. És MIT, sense taules, sense rutes i sense dependències de tercers: fa servir el client HTTP que ja porta Laravel.
Si tens formularis públics que disparen correus o crides de pagament, això és de les coses més barates que pots posar i de les que més soroll treuen. Ho treballem a aplicacions web i disseny web, i si ens expliques què t'està entrant et diem quina capa et falta.