Spam mit Hilfe einer API verhindern
Jede*r kann Feedback geben. Sogar Roboter, Spammer und mehr. Wir könnten dem Formular ein "Captcha" hinzufügen, um irgendwie vor Robots geschützt zu sein, oder wir nutzen die API eines Drittanbieters.
Ich habe mich entschieden, den kostenlosen Akismet-Dienst zu nutzen, um zu demonstrieren, wie man eine API aufruft und wie man diesen Aufruf "out of band" macht.
Bei Akismet anmelden
Melde dich kostenlos bei akismet.com an. Anschließend erhältst Du einen Akismet-API-Schlüssel.
Die Symfony HTTPClient-Komponente verwenden
Anstatt eine Bibliothek zu verwenden, die die Akismet-API abstrahiert, werden wir alle API-Aufrufe direkt ausführen. Die HTTP-Aufrufe selbst auszuführen ist effizienter (und ermöglicht es uns, von allen Symfony-Debugging-Tools wie der Integration mit dem Symfony Profiler zu profitieren).
Eine Spam-Checker-Klasse erstellen
Erstelle eine neue Klasse unter src/
mit dem Namen SpamChecker
, um die Logik des Aufrufs der Akismet-API und der Interpretation ihrer Responses zu bündeln:
Die HTTP-Client-Methode request()
sendet einen POST-Request an die Akismet-URL ($this->endpoint
) und übergibt ein Array mit Parametern.
Die getSpamScore()
-Methode gibt je nach API-Response 3 Werte zurück:
2
wenn der Kommentar "offenkundiger Spam" ist;1
wenn der Kommentar Spam sein könnte;0
wenn der Kommentar kein Spam (Ham) ist.
Tip
Verwende die besondere E-Mail-Adresse akismet-guaranteed-spam@example.com
, um das Ergebnis des API-Calls als Spam zu erzwingen.
Environment-Variablen verwenden
Die SpamChecker
-Klasse stützt sich auf ein $akismetKey
-Argument. Wie beim Upload-Verzeichnis können wir es über eine bind
-Containereinstellung injizieren:
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
Wir wollen den Wert des Akismet-Schlüssels sicherlich nicht fest in die services.yaml
-Konfigurationsdatei schreiben, daher verwenden wir stattdessen eine Environment-Variable (AKISMET_KEY
).
Eine "echte" Environment-Variable zu setzen oder den Wert in einer .env.local
-Datei zu speichern, ist Aufgabe der Entwickler*innen:
Für den Produktivbetrieb sollte eine "echte" Environment-Variable definiert werden.
Das funktioniert gut, aber die Verwaltung vieler Environment-Variablen kann umständlich werden. In einem solchen Fall hat Symfony eine "bessere" Alternative, wenn es um die Speicherung solcher Secrets geht.
Secrets speichern
Anstatt viele Environment-Variablen zu verwenden, kann Symfony einen Vault (Tresor) verwalten, in dem Du viele Secrets speichern kannst. Ein wichtiges Merkmal ist die Möglichkeit, den Vault im Repository zu committen (aber ohne den Schlüssel, um ihn zu öffnen). Ein weiteres großartiges Merkmal ist, dass es einen Vault pro Environment verwalten kann.
Secrets sind verschleierte Environment-Variablen.
Füge dem Vault den Akismet-Schlüssel hinzu:
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.
Da wir diesen Befehl das erste mal ausgeführt haben, hat er zwei Schlüssel im config/secret/dev/
-Verzeichnis erzeugt. Anschließend wurde das AKISMET_KEY
-Secret im selben Verzeichnis gespeichert.
Für die Secrets in der Dev-Environment kannst Du selber entscheiden, ob Du den Vault und die Schlüssel, die im config/secret/dev/
-Verzeichnis erzeugt wurden, committen möchtest.
Secrets können auch überschrieben werden, indem eine gleichnamige Einvironment-Variable gesetzt wird.
Kommentare auf Spam überprüfen
Eine einfache Möglichkeit, nach Spam zu suchen, sobald ein neuer Kommentar abgegeben wird, besteht darin, den Spam-Checker aufzurufen, bevor die Daten in der Datenbank gespeichert werden:
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()]);
Überprüfe, ob es einwandfrei funktioniert.
Secrets im Produktivbetrieb verwalten
Für den Produktivbetrieb unterstützt Platform.sh das Setzen sensibler Environment-Variablen:
1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:AKISMET_KEY --value=abcdef
Wie bereits erwähnt, könnte die Verwendung von Symfony-Secrets jedoch besser sein. Nicht in Bezug auf die Sicherheit, sondern in Bezug auf das Secret-Management für das Projektteam. Alle Secrets werden im Repository gespeichert, und die einzige Environment-Variable, die Du für den Produktivbetrieb verwalten musst, ist der Entschlüsselungscode. Das ermöglicht es allen im Team, Secrets zum Produktivsystem hinzuzufügen, auch wenn sie keinen Zugriff auf das Produktivsystem haben. Das Setup ist jedoch etwas aufwändiger.
Erzeuge zunächst ein Schlüsselpaar für den Produktivbetrieb:
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
Füge das Akismet-Secret für den Produktivbetrieb nun dem Produktiv-Vault hinzu:
1
$ symfony console secrets:set AKISMET_KEY --env=prod
Der letzte Schritt besteht darin, den Entschlüsselungscode an Platform.sh zu senden, indem Du eine sensible Variable setzt:
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"));'`
Du kannst alle Dateien zu Git hinzufügen und committen; der Entschlüsselungscode wurde automatisch zu .gitignore
hinzugefügt, so dass er nie commitet wird. Für mehr Sicherheit kannst Du ihn von Deinem lokalen Computer entfernen weil er ja nun im Produktivsystem verfügbar ist:
1
$ rm -f config/secrets/prod/prod.decrypt.private.php