Pular para o conteúdo

Como implementar o Cloudflare Turnstile com Laravel

Brayan Monteiro
2 min de leitura
Como implementar o Cloudflare Turnstile com Laravel
Compartilhar:

O Cloudflare Turnstile é uma alternativa a CAPTCHAs tradicionais: o visitante passa por uma verificação leve (muitas vezes imperceptível), e o servidor confirma essa interação com a API da Cloudflare antes de aceitar ações sensíveis, login, cadastro ou recuperação de senha.

Este artigo descreve um fluxo completo em Laravel, do cadastro das chaves ao widget no Blade e à validação server-side, no estilo de uma aplicação que separa configuração, serviço HTTP, regra de validação customizada, Form Requests e componente Blade reutilizável.

Por que isso importa?

Bots abusam login, cadastro e “esqueci minha senha”. O Turnstile é o CAPTCHA/alternativa da Cloudflare: widget leve, foco em UX e verificação no servidor. Integrar com Laravel de forma limpa significa: chaves em config, uma chamada HTTP à API siteverify, regra de validação reutilizável e um pedaço de front que envia o token no mesmo POST do formulário.

O que você vai precisar?

  • Conta na Cloudflare e acesso ao Turnstile.
  • Um sistema/site com Laravel (no exemplo, padrão Form Request + Blade).
  • Duas chaves: site key (pública, no HTML) e secret key (só no servidor — nunca no front).

1. Criar o site no painel Turnstile

No dashboard da Cloudflare, crie um widget Turnstile para o domínio da aplicação. Anote a site key e a secret key. Domínios de desenvolvimento podem ser adicionados nas configurações do widget para testar em homologação sem expor produção.

2. Variáveis de ambiente e configuração

No .env (valores ilustrativos — use as suas chaves reais):

CLOUDFLARE_TURNSTILE_SITE_KEY=0x4AAAAAAAxxxxxxxxxxxxxxxxxxxxxxxx
CLOUDFLARE_TURNSTILE_SECRET_KEY=0x4AAAAAAAyyyyyyyyyyyyyyyyyyyyyyyy

No config/services.php:

'cloudflare' => [
    'turnstile' => [
        'site_key' => env('CLOUDFLARE_TURNSTILE_SITE_KEY'),
        'secret_key' => env('CLOUDFLARE_TURNSTILE_SECRET_KEY'),
    ],
],

Depois: php artisan config:clear ao mudar .env em desenvolvimento.

Leia também: Autenticação com Laravel Sanctum

3. Serviço que conversa com a Cloudflare

A API oficial espera POST form-urlencoded para https://challenges.cloudflare.com/turnstile/v0/siteverify com secret, response (o token) e, opcionalmente, remoteip.

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class CloudflareTurnstileService
{
    protected string $secretKey;

    protected string $verifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';

    public function __construct()
    {
        $this->secretKey = config('services.cloudflare.turnstile.secret_key');
    }

    public function verify(string $token, ?string $ip = null): bool
    {
        if (empty($token) || empty($this->secretKey)) {
            return false;
        }

        try {
            $response = Http::asForm()->post($this->verifyUrl, [
                'secret' => $this->secretKey,
                'response' => $token,
                'remoteip' => $ip ?? request()->ip(),
            ]);

            $result = $response->json();

            if (! isset($result['success']) || $result['success'] !== true) {
                Log::warning('Cloudflare Turnstile verification failed', [
                    'errors' => $result['error-codes'] ?? [],
                    'ip' => $ip ?? request()->ip(),
                ]);

                return false;
            }

            return true;
        } catch (\Exception $e) {
            Log::error('Cloudflare Turnstile verification error', [
                'message' => $e->getMessage(),
                'ip' => $ip ?? request()->ip(),
            ]);

            return false;
        }
    }
}

Ideia central: o token é descartável e válido por pouco tempo; a fonte da verdade é a resposta JSON da Cloudflare com success: true.

4. Regra de validação reutilizável

Assim você não repete lógica em cada controller e mantém a mensagem padronizada.

<?php

namespace App\Rules;

use App\Services\CloudflareTurnstileService;
use Illuminate\Contracts\Validation\ValidationRule;

class CloudflareTurnstile implements ValidationRule
{
    public function validate(string $attribute, mixed $value, \Closure $fail): void
    {
        if (app()->environment('local')) {
            return;
        }

        $service = app(CloudflareTurnstileService::class);

        if (! $service->verify($value, request()->ip())) {
            $fail('A verificação do Cloudflare Turnstile falhou. Por favor, tente novamente.');
        }
    }
}

Por que pular local: em desenvolvimento você evita dependência de chaves e do widget a cada refresh. Em CI, avalie testing da mesma forma ou use variáveis de ambiente de teste no Turnstile.

5. Form Request

O restante do request (email, senha, etc.) é o seu domínio. O padrão é condicionar regra e campo ao ambiente, alinhado ao componente Blade:

public function rules(): array
{
    $rules = [
        // ... suas regras (email, password, etc.)
    ];

    if (! app()->environment('local')) {
        $rules['cf-turnstile-response'] = ['required', new \App\Rules\CloudflareTurnstile()];
    }

    return $rules;
}

O nome do campo cf-turnstile-response é o que a documentação da Cloudflare usa no POST; manter esse nome evita surpresas com exemplos oficiais e snippets.

Leia também: Multi-tenancy no Laravel

6. Componente Blade: widget + token oculto

Renderizar o widget só quando há site_key e não estiver em local evita erros de JS e formulários quebrados em dev.

resources/views/components/common/cloudflare-turnstile.blade.php:

@if(config('services.cloudflare.turnstile.site_key') && !app()->environment('local'))
    <div class="cf-turnstile"
         data-sitekey="{{ config('services.cloudflare.turnstile.site_key') }}"
         data-callback="onTurnstileSuccess"
         data-error-callback="onTurnstileError"
         data-expired-callback="onTurnstileExpired"></div>

    <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

    <script>
        function onTurnstileSuccess(token) {
            const input = document.getElementById('cf-turnstile-response');
            if (input) {
                input.value = token;
            }
        }

        function onTurnstileError() {
            const input = document.getElementById('cf-turnstile-response');
            if (input) {
                input.value = '';
            }
        }

        function onTurnstileExpired() {
            const input = document.getElementById('cf-turnstile-response');
            if (input) {
                input.value = '';
            }
        }
    </script>

    <input type="hidden" id="cf-turnstile-response" name="cf-turnstile-response" value="">
@endif

Callbacks: em sucesso, o token vai para o hidden; em erro ou expiração, limpa o valor para o required do Laravel falhar de propósito e o usuário refazer o desafio.

7. Usar no formulário

Coloque o componente dentro do <form>, idealmente antes do botão de envio, centralize visualmente se quiser, e exiba erros de validação:

<div class="flex justify-center">
    <x-common.cloudflare-turnstile />
</div>
@error('cf-turnstile-response')
    <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror

Repita o mesmo bloco em login, registro, recuperação e reset de senha — sempre que quiser proteger o endpoint com Turnstile.

8. Encaixando o “meio” ao “fim”: checklist mental

  1. Usuário carrega a página → widget aparece (exceto local).
  2. Cloudflare executa o desafio → onTurnstileSuccess preenche o hidden.
  3. Submit → Laravel valida cf-turnstile-response → Rule chama siteverify.
  4. Se success for true, o restante da validação segue; senão, mensagem única e log com error-codes para debug.

Segurança: a secret nunca vai para o Blade; só o site_key é público. Rate limiting no login continua valendo, Turnstile não substitui throttle, complementa.

9. Conclusão

Implementar o Cloudflare Turnstile em um app Laravel não é só “colocar um script na página”: é combinar três camadas que precisam estar alinhadas, configuração (env + config), verificação server-side contra o endpoint oficial siteverify, e experiência no formulário (widget + campo oculto + mensagens de erro). Quando isso está consistente, você reduz bots e abuso em login, cadastro e recuperação de senha sem depender só de rate limiting ou de CAPTCHAs pesados para o usuário.

O desenho que vimos, serviço HTTP dedicado, regra de validação customizada, Form Requests que só exigem o token fora do ambiente local, e componente Blade que só renderiza quando há site_key e não é local, mantém responsabilidades claras: o controller continua fino, a política “desligar no dev” fica repetível e previsível, e o segredo permanece só no servidor.

Do ponto de vista de segurança operacional, vale fixar três ideias: a site key pode aparecer no HTML (é pública); a secret key só em variável de ambiente e referenciada via config(); em logs, prefira códigos de erro da API e contexto mínimo (por exemplo IP), sem registrar o token inteiro. Se houver proxy reverso, garantir IP correto para remoteip evita falsos negativos na verificação.

Para fechar o ciclo em produção, confira: variáveis preenchidas no ambiente de deploy, domínio do site autorizado no painel do Turnstile, widget visível nos fluxos que você protegeu, e um teste manual (ou teste automatizado com HTTP fake) simulando sucesso e falha da API. Em desenvolvimento local, pular widget e validação acelera o time, desde que todos saibam que a proteção real só vale onde o ambiente não é local (ou a regra que vocês adotarem).

Em suma: Turnstile bem integrado ao Laravel é config + serviço + regra + requests + Blade, todos falando a mesma língua sobre ambiente e campo cf-turnstile-response. Com isso, quem lê o post ou o código consegue não só copiar trechos, mas entender por que cada parte existe e como manter isso seguro e sustentável no tempo.

Palavras-chave:

#Cloudflare Turnstile #Laravel #PHP

Sobre o autor

Brayan Monteiro

Bacharel em Sistemas de Informação pela Faculdade Maurício de Nassau e desenvolvedor de software. Produzo conteúdo e gerencio blogs. Sou especialista em desenvolvimento web e SEO de sites.

Continue lendo

Postagens relacionadas

Discussão

Comentários

Nenhum comentário ainda. Seja o primeiro a comentar.