Skip to content
  • About
    • What is Symfony?
    • Community
    • News
    • Contributing
    • Support
  • Documentation
    • Symfony Docs
    • Symfony Book
    • Screencasts
    • Symfony Bundles
    • Symfony Cloud
    • Training
  • Services
    • Platform.sh for Symfony Best platform to deploy Symfony apps
    • SymfonyInsight Automatic quality checks for your apps
    • Symfony Certification Prove your knowledge and boost your career
    • SensioLabs Professional services to help you with Symfony
    • Blackfire Profile and monitor performance of your apps
  • Other
  • Blog
  • Download
sponsored by
  1. Home
  2. Documentation
  3. Symfony: The Fast Track
  4. German
  5. Spam mit Hilfe einer API verhindern

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:

src/SpamChecker.php
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
38
39
40
41
42
43
44
45
46
47
48
49
50
namespace App;

use App\Entity\Comment;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class SpamChecker
{
    private $endpoint;

    public function __construct(
        private HttpClientInterface $client,
        string $akismetKey,
    ) {
        $this->endpoint = sprintf('https://%s.rest.akismet.com/1.1/comment-check', $akismetKey);
    }

    /**
     * @return int Spam score: 0: not spam, 1: maybe spam, 2: blatant spam
     *
     * @throws \RuntimeException if the call did not work
     */
    public function getSpamScore(Comment $comment, array $context): int
    {
        $response = $this->client->request('POST', $this->endpoint, [
            'body' => array_merge($context, [
                'blog' => 'https://guestbook.example.com',
                'comment_type' => 'comment',
                'comment_author' => $comment->getAuthor(),
                'comment_author_email' => $comment->getEmail(),
                'comment_content' => $comment->getText(),
                'comment_date_gmt' => $comment->getCreatedAt()->format('c'),
                'blog_lang' => 'en',
                'blog_charset' => 'UTF-8',
                'is_test' => true,
            ]),
        ]);

        $headers = $response->getHeaders();
        if ('discard' === ($headers['x-akismet-pro-tip'][0] ?? '')) {
            return 2;
        }

        $content = $response->getContent();
        if (isset($headers['x-akismet-debug-help'][0])) {
            throw new \RuntimeException(sprintf('Unable to check for spam: %s (%s).', $content, $headers['x-akismet-debug-help'][0]));
        }

        return 'true' === $content ? 1 : 0;
    }
}

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 Autowire-Annotation injizieren:

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);
     }

Wir wollen den Wert des Akismet-Schlüssels sicherlich nicht fest im Code hinterlegen, 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:

.env.local
1
AKISMET_KEY=abcdef

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
--- 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()]);

Ü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 the prod 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

Weiterführendes

  • Die HttpClient-Komponente Dokumentation;
  • Die Dokumentation zur Verarbeitung von Environment-Variablen;
  • Das Symfony HttpClient Cheat Sheet.
Previous page Das Admin-Backend absichern
Next page Testen
This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.
TOC
    Version

    Symfony 6.4 is backed by

    Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).

    Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).

    Be safe against critical risks to your projects and businesses

    Be safe against critical risks to your projects and businesses

    Version:
    Locale:
    ebook

    This book is backed by:

    see all backers

    Symfony footer

    Avatar of Michael Dwyer, a Symfony contributor

    Thanks Michael Dwyer (@kalifg) for being a Symfony contributor

    1 commit • 9 lines changed

    View all contributors that help us make Symfony

    Become a Symfony contributor

    Be an active part of the community and contribute ideas, code and bug fixes. Both experts and newcomers are welcome.

    Learn how to contribute

    Symfony™ is a trademark of Symfony SAS. All rights reserved.

    • What is Symfony?

      • What is Symfony?
      • Symfony at a Glance
      • Symfony Components
      • Symfony Releases
      • Security Policy
      • Logo & Screenshots
      • Trademark & Licenses
      • symfony1 Legacy
    • Learn Symfony

      • Symfony Docs
      • Symfony Book
      • Reference
      • Bundles
      • Best Practices
      • Training
      • eLearning Platform
      • Certification
    • Screencasts

      • Learn Symfony
      • Learn PHP
      • Learn JavaScript
      • Learn Drupal
      • Learn RESTful APIs
    • Community

      • Symfony Community
      • SymfonyConnect
      • Events & Meetups
      • Projects using Symfony
      • Contributors
      • Symfony Jobs
      • Backers
      • Code of Conduct
      • Downloads Stats
      • Support
    • Blog

      • All Blog Posts
      • A Week of Symfony
      • Case Studies
      • Cloud
      • Community
      • Conferences
      • Diversity
      • Living on the edge
      • Releases
      • Security Advisories
      • Symfony Insight
      • Twig
      • SensioLabs Blog
    • Services

      • SensioLabs services
      • Train developers
      • Manage your project quality
      • Improve your project performance
      • Host Symfony projects

      Powered by

    Follow Symfony