Empêcher le spam avec une API
N'importe qui peut soumettre un commentaire, même des robots ou des spammeurs. Nous pourrions ajouter un "captcha" au formulaire pour nous protéger des robots, ou nous pouvons utiliser des API tierces.
J'ai décidé d'utiliser le service gratuit Akismet pour montrer comment appeler une API et comment faire un appel "vers l'extérieur".
S'inscrire sur Akismet
Créez un compte gratuit sur akismet.com et récupérez la clé de l'API Akismet.
Ajouter une dépendance au composant Symfony HTTPClient
Au lieu d'utiliser une bibliothèque qui abstrait l'API d'Akismet, nous ferons directement tous les appels API. Faire nous-mêmes les appels HTTP est plus efficace (et nous permet de bénéficier de tous les outils de débogage de Symfony comme l'intégration avec le Symfony Profiler).
Concevoir une classe de vérification de spam
Créez une nouvelle classe dans src/
nommée SpamChecker
pour contenir la logique d'appel à l'API d'Akismet et l'interprétation de ses réponses :
La méthode request()
du client HTTP soumet une requête POST à l'URL d'Akismet ($this->endpoint
) et passe un tableau de paramètres.
La méthode getSpamScore()
retourne 3 valeurs en fonction de la réponse de l'appel à l'API :
2
: si le commentaire est un "spam flagrant" ;1
: si le commentaire pourrait être du spam ;0
: si le commentaire n'est pas du spam (ham).
Tip
Utilisez l'adresse email spéciale akismet-guaranteed-spam@example.com
pour forcer le résultat de l'appel à être du spam.
Utiliser des variables d'environnement
La classe SpamChecker
utilise un argument $akismetKey
. Comme pour le répertoire d'upload, nous pouvons l'injecter grâce au paramètre bind
du conteneur :
1 2 3 4 5 6 7 8 9 10
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -12,6 +12,7 @@ services:
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
string $photoDir: "%kernel.project_dir%/public/uploads/photos"
+ string $akismetKey: "%env(AKISMET_KEY)%"
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
Nous ne voulons certainement pas coder en dur la valeur de la clé d'Akismet dans le fichier de configuration services.yaml
, nous utilisons donc plutôt une variable d'environnement (AKISMET_KEY
).
Il appartient alors à chacun de définir une variable d'environnement "réelle" ou d'en stocker la valeur dans un fichier .env.local
:
Pour la production, une variable d'environnement "réelle" doit être définie.
Ça fonctionne bien, mais la gestion de nombreuses variables d'environnement peut devenir lourde. Dans un tel cas, Symfony a une "meilleure" alternative pour le stockage des chaînes secrètes.
Stocker des chaînes secrètes
Au lieu d'utiliser plusieurs variables d'environnement, Symfony peut gérer un coffre-fort où vous pouvez stocker plusieurs chaînes secrètes. L'une de ses caractéristiques les plus intéressantes est la possibilité de committer l'espace de stockage dans le dépôt (mais sans la clé pour l'ouvrir). Une autre fonctionnalité intéressante est qu'il peut gérer un coffre-fort par environnement.
Les chaînes secrètes sont des variables d'environnement déguisées.
Ajoutez la clé d'Akismet dans le coffre-fort :
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.
Comme c'est la première fois que nous exécutons cette commande, elle a généré deux clés dans le répertoire config/secret/dev/
. Elle a ensuite stocké la chaîne secrète AKISMET_KEY
dans ce même répertoire.
Pour les chaînes secrètes de développement, vous pouvez décider de committer l'espace de stockage et les clés qui ont été générées dans le répertoire config/secret/dev/
.
Les chaînes secrètes peuvent également être écrasées en définissant une variable d'environnement du même nom.
Identifier le spam dans les commentaires
Une façon simple de vérifier la présence de spam lorsqu'un nouveau commentaire est soumis est d'appeler le vérificateur de spam avant de stocker les données dans la base de données :
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 37
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -7,6 +7,7 @@ use App\Entity\Conference;
use App\Form\CommentFormType;
use App\Repository\CommentRepository;
use App\Repository\ConferenceRepository;
+use App\SpamChecker;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
@@ -35,7 +36,7 @@ class ConferenceController extends AbstractController
}
#[Route('/conference/{slug}', name: 'conference')]
- public function show(Request $request, Conference $conference, CommentRepository $commentRepository, string $photoDir): Response
+ public function show(Request $request, Conference $conference, CommentRepository $commentRepository, SpamChecker $spamChecker, string $photoDir): Response
{
$comment = new Comment();
$form = $this->createForm(CommentFormType::class, $comment);
@@ -53,6 +54,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()]);
Vérifiez qu'il fonctionne bien.
Gérer les chaînes secrètes en production
En production, Platform.sh prend en charge le paramétrage des variables d'environnement sensibles :
1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:AKISMET_KEY --value=abcdef
Mais comme nous l'avons vu plus haut, l'utilisation des chaînes secrètes de Symfony pourrait être une meilleure manière de procéder. Pas en termes de sécurité, mais en termes de gestion des chaînes secrètes pour l'équipe du projet. Toutes les chaînes secrètes sont stockées dans le dépôt et la seule variable d'environnement que vous devez gérer pour la production est la clé de déchiffrement. Cela permet à tous les membres de l'équipe d'ajouter des chaînes secrètes en production même s'ils n'ont pas accès aux serveurs de production. L'installation est un peu plus compliquée cependant.
Tout d'abord, créez une paire de clés pour l'utilisation en production :
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
Rajoutez la chaîne secrète d'Akismet dans le coffre-fort en production, mais avec sa valeur de production :
1
$ symfony console secrets:set AKISMET_KEY --env=prod
La dernière étape consiste à envoyer la clé de déchiffrement à Platform.sh en définissant une variable sensible :
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"));'`
Vous pouvez ajouter et commiter tous les fichiers ; la clé de déchiffrement a été ajoutée dans le .gitignore
automatiquement, donc elle ne sera jamais enregistrée. Pour plus de sécurité, vous pouvez la retirer de votre machine locale puisqu'elle a été déployée :
1
$ rm -f config/secrets/prod/prod.decrypt.private.php