Symfony Google Recaptcha
Se você ainda não passou pela experiência de criar um formulário no seu sistema e receber 80 mil preenchimentos aleatórios em 24 horas, você não sabe o que é terror. Mas, um dia com certeza vai saber.
Nesse post, vamos aprender como diferenciar uma requisição feita por um ser humano de uma feita por um bot malicioso nos nossos formulários =)
Construindo o cenário
Vamos supor que seu cliente/chefe te pediu pra construir um formulário de contato na sua landing page:
Que envia os dados com método POST pra a action '/envia' no sistema:
E, por sua vez, recebe os dados do formulário e envia tudo por e-mail pra contato@hefesto.io
Se a gente testar esse código, ele vai enviar o e-mail corretamente pro destino.
Portanto, está funcionando! Certo?
Garantindo segurança
No dia seguinte, você chega no trabalho e descobre que aquele formulário que estava funcionando, funciona até demais e recebeu milhares de requisições com dados válidos.
Desses milhares, apenas algumas unidades são de clientes reais, o resto são dados aleatórios.
O que deixa qualquer cliente louco (principalmente se ele usa um serviço de envio de e-mail pago), portanto precisamos impedir as requisições com dados aleatórios!
Como diferenciar a requisição?
Se os dados fossem inválidos seria simples de resolver, afinal é só validar os campos e ser feliz.
Porém, um email como 'asdvcxz@qwerty.org', pode sim ser um e-mail que existe. Portanto, nosso critério de diferenciação não pode ser os dados.
Criando um Token
Aqui, uma abordagem seria adicionar um campo escondido no formulário com um código secreto:
E, verificar no back-end se esse campo foi enviado corretamente:
Porém, se seu adversário for um bot em selenium ele vai carregar a página exatamente como um usuário carregaria. Com seu código secreto e tudo mais.
E agora?
A verdade é que a única diferença entre você e um bot bem feito é o comportamento ao preencher o formulário.
Por exemplo, você precisa clickar nos campos pra digitar. O bot não. Isso significa que o tempo que você leva pra enviar a requisição com certeza será diferente do tempo de um bot.
reCAPTCHA!
Pensando nesses cenários comportamentais, a galera da google criou o famoso reCAPTCHA: uma forma de pontuar as requisições feitas nos seus sistemas a fim de definir quão suspeita ela é.
Mas, como funciona?
O processo funciona enviando uma requisição pra google quando a tela carrega no navegador. Ou quando você clicka no botãozinho: "Eu não sou um robô".
Nesse post, vamos ficar com a primeira abordagem por ser a menos invasiva pro usuário final =)
Depois enviamos outra requisição com uma chave secreta e a resposta da primeira requisição pra google quando o formulário é enviado, no back-end.
Pontuação
Nessa segunda requisição a google pontua a partir dos comportamentos do usuário e devolve pra gente uma nota de 0 a 1.
Sendo 0 a nota mais provável pra um bot e 1 a nota mais provável para um usuário real =)
Mãos na massa
O primeiro passo, segundo a documentação da google, é criar a chave secreta vinculada a um site
No nosso caso, como vamos trabalhar com o formulário em ambiente de desenvolvimento definimos localhost como domínio permitido. Em casos reais esse domínio deve ser o domínio do seu site.
Ao clickar em enviar, já temos nossas chaves prontas para serem usadas!
A chave de site é a que vamos usar quando a página carregar e a chave secreta é a que vamos usar no back-end =)
Primeira requisição
Agora que temos nossas chaves, podemos adicionar no nosso formulário a primeira requisição quando a página carregar. Isso pode ser feito com um script simples fornecido pela própria google
// CARREGA A API PASSANDO A CHAVE DO SITE
<script src="https://www.google.com/recaptcha/api.js?render=reCAPTCHA_site_key"></script>// ENVIA A PRIMEIRA REQUISIÇÃO PRA GOOGLE
<script>
grecaptcha.ready(function() {
grecaptcha.execute('reCAPTCHA_site_key', {action: 'homepage'}).then(function(token) {
...
});
});
</script>
Aqui, precisamos substituir a chave reCAPTCHA_site_key pela nossa chave do site, que no nosso caso é: "6Lc5DKgUAAAAAIejovEpTX6XImCrwRlQawRbbyNL"
Nosso código no formulário, fica algo como:
Pegando a pontuação no back-end
Além disso, ao finalizar a requisição, a google nos retorna um token, que precisará ser enviado pro back-end junto com nosso formulário.
Por isso, vamos adicionar o campo recaptchaToken escondido no nosso formulário
Agora, com um pouquinho de JavaScript ou JQuery conseguimos popular esse campo quando a google devolver o token
<script>
grecaptcha.ready(function() {
grecaptcha.execute('6Lc5DKgUAAAAAIejovEpTX6XImCrwRlQawRbbyNL', {action: 'homepage'}).then(function(token) { // PREENCHENDO O CAMPO recaptchaToken com JavaScript
document.getElementById('recaptchaToken').value = token;
});
});
</script>
Por fim, quando enviarmos nosso formulário, ele virá com o token do reCAPTCHA.
Segunda requisição
Portanto, só precisamos mandar esse token de volta pra google quando o formulário for enviado e verificar nossa pontuação.
Isso pode ser feito enviando uma requisição do tipo POST pra URL:
https://www.google.com/recaptcha/api/siteverify
Com nossa chave secreta e a resposta do token. Algo como:
$url = "https://www.google.com/recaptcha/api/siteverify";$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, array(
"secret" => "CHAVE_SECRETA",
"response" => "RESPOSTA_DA_PRIMEIRA_REQUISICAO")
);
$response = curl_exec($ch);
curl_close($ch);$respostaComNota = json_decode($response);
Porém, a galera da google criou uma biblioteca em PHP já pronta pra isso!
Instalando a biblioteca
Portanto, ao invés disso, podemos simplesmente pedir a biblioteca pro composer
composer require google/recaptcha "^1.2"
Com a instalação do reCAPTCHA, nosso querido Symfony já cria os parâmetros de configuração no arquivo .env
Porém, precisamos preencher os valores com as chaves corretamente
GOOGLE_RECAPTCHA_SITE_KEY=6Lc5DKgUAAAAAIejovEpTX6XImCrwRlQawRbbyNL
GOOGLE_RECAPTCHA_SECRET=6Lc5DKgUAAAAAHVitpwWbHuHrvuxFLquD5WaeRNL
Usando a biblioteca
Tendo as chaves configuradas, basta pedir o ReCaptcha por injeção de dependência
Com isso, é só usar o método "verify" passando o token que conseguimos na primeira requisição.
O retorno é um objeto com os dados:
{
"success": true|false, // Se a requisição é valida ou não
"score": number // A pontuação da requisição de 0~1
"action": string // A ação que foi realizada no front
"challenge_ts": timestamp, // Data em que a requisição foi feita
"hostname": string, // De que dominio/ip veio a requisição
"error-codes": [...] // Vem preenchido quando da erro com o motivo
}
Tendo a resposta na mão, bastaria verificarmos se success é verdadeiro e se o score contém um valor plausível (maior ou igual a 0.5). Algo como:
$naoEhUmBot = $resposta->success && $resposta->score >= 0.5;
Porém, a biblioteca também já essa verificação pra gente com o método isSuccess!
Caso o token seja inválido, o e-mail não é enviado e lançamos uma Exception
E, pronto!
Agora, quem decide se a requisição é valida ou não é a própria google a partir do comportamento do usuário!
Indo além
Daria pra melhorar esse código isolando em serviços os códigos que deixamos no controller e usando os FormTypes do framwork. Além disso, daria pra consumir a chave do site dos parâmetros do Symfony ao invés de digitar direto no twig ;)
E ai, o que você achou do reCAPTCHA com o Symfony? Compartilha com a gente aqui nos comentários!
Ah, o código pronto desse post você encontra lá no meu git =)