Spam voorkomen middels een API
Iedereen kan feedback geven. Ook scripts zoals robots en spammers. We zouden een "captcha" kunnen toevoegen aan het formulier waardoor het een zeker niveau van bescherming krijgt tegen dit soort scripts. Of, we kunnen gebruik maken van een aantal externe API's.
Ik heb besloten om de gratis Akismet service te gebruiken, om te laten zien hoe je een API kunt aanroepen en hoe je de calls "out of band" kunt maken.
Aanmelden bij Akismet
Meld je aan voor een gratis account op akismet.com en ontvang de Akismet API key.
Gebruik maken van het Symfony HTTPClient-component
In plaats van een specifieke library te gebruiken voor de Akismet API, zullen we alle API-calls direct uitvoeren. Het zelf uitvoeren van de HTTP-calls is efficiënter (en stelt ons in staat om te profiteren van alle Symfony debugging tools, zoals de integratie met de Symfony Profiler).
Een spam-checker-class bouwen
Creëer een nieuwe class onder de src/
map met de naam SpamChecker
. Hierin zullen we de logica schrijven om de Akismet API aan te roepen en de antwoorden te verwerken:
De request()
methode van de HTTP client verstuurt een POST request naar de Akismet URL ( $this->endpoint
) en geeft hier een reeks parameters aan mee.
De getSpamScore()
method geeft, afhankelijk van de API-response één van deze 3 waarden terug:
2
: als de reactie "overduidelijk spam" is;1
: als de reactie "mogelijk spam" kan zijn;0
: als de reactie "geen spam" (ham) is.
Tip
Gebruik het speciale akismet-guaranteed-spam@example.com
e-mailadres om het resultaat van een call als spam te forceren.
Omgevingsvariabelen gebruiken
De SpamChecker
class is afhankelijk van een $akismetKey
argument. Net als bij de upload directory, kunnen we deze injecteren via een bind
container instelling:
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
We willen de waarde van de Akismet key niet hard in het services.yaml
configuratiebestand coderen. In plaats daarvan gebruiken we de omgevingsvariabele ( AKISMET_KEY
).
Het is dan aan de individuele ontwikkelaar om een "echte" omgevingsvariabele in te stellen of de waarde op te slaan in een .env.local
bestand:
Voor productie moet een "echte" environment variable gedefiniëerd worden.
Dat werkt goed, maar het beheer van veel environment variables kan omslachtig worden. Symfony heeft een "beter" alternatief als het gaat om het bewaren van secrets.
Secrets bewaren
In plaats van veel omgevingsvariabelen te gebruiken, kan Symfony een vault beheren waar je secrets kan opslaan. Een belangrijke feature hiervan is de mogelijkheid om de vault aan de repository toe te voegen (maar zonder de decryptiesleutel om de inhoud te lezen). Een andere interessante feature is dat je één vault per omgeving kan beheren.
Secrets zijn verkapte environment variables.
Voeg de Akismet key toe aan de vault:
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.
Omdat dit de eerste keer is dat we het commando uitvoeren zijn er twee keys in de config/secret/dev/
map gegenereerd. De AKISMET_KEY
secret werd vervolgens in diezelfde map opgeslagen.
Voor development-secrets kan je er voor kiezen om de vault en de sleutels die in de config/secret/dev/
directory zijn gegenereerd te committen.
Secrets kunnen ook worden overschreven door een environment variable met dezelfde naam in te stellen.
Reacties controleren op spam
Een eenvoudige manier om nieuwe reacties te controleren op spam, is de spam checker aanroepen voordat de gegevens in de database worden opgeslagen:
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()]);
Controleer of het goed werkt.
Secrets beheren in productie
In productie ondersteunt Platform.sh het instellen van sensitive environment variables:
1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:AKISMET_KEY --value=abcdef
Zoals hierboven besproken is het gebruik van Symfony secrets mogelijk beter. Niet voor de veiligheid, maar om het beheer van secrets eenvoudiger te maken voor het projectteam. Alle secrets worden opgeslagen in de repository en de enige omgevingsvariabele die je moet beheren voor de productieomgeving is de decryptiesleutel. Dat maakt het voor iedereen in het team mogelijk om productie-secrets toe te voegen, zelfs als ze geen toegang hebben tot productieservers. De setup is wel iets complexer.
Genereer eerst een keypair voor gebruik in productie:
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
Voeg het Akismet secret opnieuw toe in de productie vault, maar nu met de productie waarde:
1
$ symfony console secrets:set AKISMET_KEY --env=prod
Als laatste stap configureren we de decryptiesleutel op Platform.sh door het instellen van een sensitive variable:
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"));'`
Je kan alle bestanden toevoegen en committen; de decryptionkey werd automatisch aan .gitignore
toegevoegd, dus deze zal nooit gecommit worden. Voor meer veiligheid kan je deze van je lokale machine verwijderen, omdat deze al gedeployd is:
1
$ rm -f config/secrets/prod/prod.decrypt.private.php