Prevenire lo spam con un'API
Chiunque può inviare un feedback. Anche robot, spammer e altro ancora. Potremmo aggiungere un po' di "captcha" al form per essere in qualche modo protetti dai robot, oppure possiamo usare API di terze parti.
Ho deciso di utilizzare il servizio antispam gratuito Akismet per dimostrare come fare chiamate ad un'API e come fare la chiamata "fuori banda".
Iscrizione ad Akismet
Create un account gratuito su akismet.com e così ottenete la chiave API fornita dal servizio.
Aggiungere il componente HTTPClient di Symfony
Invece di usare una libreria che astrae le API di Akismet, faremo tutte le chiamate API direttamente. Fare da soli le chiamate HTTP è più efficiente (e ci permette di beneficiare di tutti gli strumenti di debug di Symfony, come l'integrazione con il Profiler).
Design di una classe per il controllo dello spam
Create una nuova classe nella cartella src/
, chiamatela SpamChecker
: la classe conterrà la logica di chiamata alle API di Akismet e la logica per interpretarne le risposte:
Il metodo request()
del client HTTP invia una richiesta POST all'URL di Akismet ($this->endpoint
) e passa un array di parametri.
Il metodo getSpamScore()
restituisce tre possibili valori, che dipendono dalla risposta alla chiamata API:
2
: se il commento è uno "spam palese";1
: se il commento potrebbe essere spam;0
: se il commento è sicuro e non è spam (il cosiddetto "ham").
Tip
Usate l'indirizzo speciale akismet-guaranteed-spam@example.com
per forzare il risultato della chiamata a "spam".
Utilizzare le variabili d'ambiente
La classe SpamChecker
si basa sul parametro $akismetKey
. Come per la cartella di caricamento, possiamo iniettarlo tramite un'annotazione dell' Autowire
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
--- a/src/SpamChecker.php
+++ b/src/SpamChecker.php
@@ -3,6 +3,7 @@
namespace App;
use App\Entity\Comment;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class SpamChecker
@@ -11,7 +12,7 @@ class SpamChecker
public function __construct(
private HttpClientInterface $client,
- string $akismetKey,
+ #[Autowire('%env(AKISMET_KEY)%')] string $akismetKey,
) {
$this->endpoint = sprintf('https://%s.rest.akismet.com/1.1/comment-check', $akismetKey);
}
Sicuramente non vogliamo forzare il valore della chiave di Akismet direttamente nel codice, per questo motivo usiamo una variabile d'ambiente (AKISMET_KEY
).
Spetta poi a ogni sviluppatore impostare una variabile d'ambiente "reale" o memorizzare il valore in un file .env.local
:
Per l'ambiente di produzione, si dovrebbe definire una variabile d'ambiente "reale".
Funziona bene, ma la gestione di molte variabili d'ambiente potrebbe diventare complicata. In questo caso, Symfony offre un'alternativa "migliore" quando si tratta di conservare stringhe segrete.
Salvare stringhe segrete
Invece di usare molte variabili d'ambiente, Symfony può gestire un portachiavi dove è possibile memorizzare stringhe segrete. Una caratteristica chiave è la possibilità di effettuare il commit del portachiavi nel repository (ma senza la chiave per aprirlo). Un'altra grande caratteristica è che possiamo gestire un portachiavi per ciascun ambiente.
Le stringhe segrete sono variabili d'ambiente camuffate.
Aggiungete la chiave API Akismet al portachiavi:
1
$ symfony console secrets:set AKISMET_KEY
1 2 3 4
Please type the secret value:
>
[OK] Secret "AKISMET_KEY" encrypted in "config/secrets/dev/"; you can commit it.
Poiché è la prima volta che eseguiamo questo comando, sono state generate due chiavi nella cartella config/secret/dev/
. Il comando ha poi memorizzato la stringa segreta AKISMET_KEY
, nella stessa cartella.
Per le stringhe segrete usate nell'ambiente di sviluppo, si può decidere di fare il commit del portachiavi e delle chiavi generate nella cartella config/secret/dev/
.
Le stringhe segrete possono anche essere sovrascritte, impostando una variabile d'ambiente con lo stesso nome.
Controllo dello spam nei commenti
Un modo semplice per controllare se un commento appena ricevuto sia da marcare come spam consiste nel richiamare la classe SpamChecker, prima di memorizzare i dati nel database:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -7,6 +7,7 @@ use App\Entity\Conference;
use App\Form\CommentType;
use App\Repository\CommentRepository;
use App\Repository\ConferenceRepository;
+use App\SpamChecker;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
@@ -34,6 +35,7 @@ class ConferenceController extends AbstractController
Request $request,
Conference $conference,
CommentRepository $commentRepository,
+ SpamChecker $spamChecker,
#[Autowire('%photo_dir%')] string $photoDir,
): Response {
$comment = new Comment();
@@ -48,6 +50,17 @@ class ConferenceController extends AbstractController
}
$this->entityManager->persist($comment);
+
+ $context = [
+ 'user_ip' => $request->getClientIp(),
+ 'user_agent' => $request->headers->get('user-agent'),
+ 'referrer' => $request->headers->get('referer'),
+ 'permalink' => $request->getUri(),
+ ];
+ if (2 === $spamChecker->getSpamScore($comment, $context)) {
+ throw new \RuntimeException('Blatant spam, go away!');
+ }
+
$this->entityManager->flush();
return $this->redirectToRoute('conference', ['slug' => $conference->getSlug()]);
Controlliamo che funzioni bene.
Gestire stringhe segrete in produzione
Per la produzione, Platform.sh supporta l'impostazione di variabili d'ambiente sensibili:
1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:AKISMET_KEY --value=abcdef
Ma, come abbiamo già visto, usare le stringhe segrete di Symfony potrebbe essere la scelta migliore. Non in termini di sicurezza, ma in termini di gestione dei segreti nel team. Tutte le stringhe segrete sono memorizzate nel repository e l'unica variabile d'ambiente da gestire per la produzione è la chiave di decrittazione. Questo rende possibile a ciascun membro del team l'aggiunta di stringhe segrete, anche se non ha accesso ai server di produzione. Tuttavia il setup risulterà un po' più complesso.
In primo luogo, generare una coppia di chiavi per l'uso in produzione:
1
$ symfony console secrets:generate-keys --env=prod
On Linux and similiar OSes, use
APP_RUNTIME_ENV=prod
instead of--env=prod
as this avoids compiling the application for theprod
environment:1
$ APP_RUNTIME_ENV=prod symfony console secrets:generate-keys
Aggiungere nuovamente la stringa segreta di Akismet nel portachiavi di produzione, ma con il suo valore di produzione:
1
$ symfony console secrets:set AKISMET_KEY --env=prod
L'ultimo passo è quello di inviare la chiave di decrittazione a Platform.sh, impostando una variabile:
1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:SYMFONY_DECRYPTION_SECRET --value=`php -r 'echo base64_encode(include("config/secrets/prod/prod.decrypt.private.php"));'`
Si possono aggiungere tutti i file a git in area di stage, ed eseguire il commit. La chiave di decrittazione è stata aggiunta automaticamente al file .gitignore
, in modo che sia esclusa da qualsiasi commit. Per maggiore sicurezza, è possibile rimuoverla dalla macchina locale, essendo stata inclusa nel deploy:
1
$ rm -f config/secrets/prod/prod.decrypt.private.php