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. Polish
  5. Ochrona przed spamem przy pomocy API

Ochrona przed spamem przy pomocy API

Każdy może przesłać opinię. Nawet roboty, spamerzy itd. Możemy dodać zabezpieczenie CAPTCHA do formularza, aby w jakiś sposób ochronić się przed robotami, możemy też użyć zewnętrznych API.

Postanowiłem skorzystać z darmowej usługi Akismet, aby zademonstrować, jak wywołać zapytanie do API i jak wykonać połączenie „poza widoczną warstwą”.

Rejestracja w Akismet

Zarejestruj bezpłatne konto na akismet.com i uzyskaj klucz API Akismet.

Zależność od komponentu Symfony HTTPClient

Zamiast korzystać z biblioteki, która obsługuje API Akismet, wykonamy wszystkie zapytania do API bezpośrednio. Wykonywanie zapytań HTTP samodzielnie jest bardziej efektywne (i pozwala nam korzystać ze wszystkich narzędzi Symfony do debugowania, takich jak integracja z Symfony Profiler).

Projektowanie klasy Spam Checker

Utwórz nową klasę w katalogu src/ pod nazwą SpamChecker w której zawrzemy schemat działań odpowiadających za wysłanie zapytania do API Akismet i przetworzenie jego odpowiedzi.

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 $client;
    private $endpoint;

    public function __construct(HttpClientInterface $client, string $akismetKey)
    {
        $this->client = $client;
        $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;
    }
}

Metoda request() klienta HTTP wysyła zapytanie POST pod URL Akismet ($this->endpoint) i przekazuje tablicę parametrów.

Metoda getSpamScore() zwraca trzy wartości w zależności od odpowiedzi z API:

  • 2 jeśli komentarz jest „rażącym spamem” (ang. blatant spam);
  • 1 jeśli komentarz może być spamem;
  • 0 jeśli komentarz nie jest spamem.

Tip

Użyj specjalnego adresu e-mail akismet-guaranteed-spam@example.com, aby wynik wywołania potraktować jako spam.

Korzystanie ze zmiennych środowiskowych

Klasa SpamChecker jest zależna od argumentu $akismetKey. Podobnie jak w przypadku katalogu do zapisu plików, możemy wstrzyknąć ten argument za pomocą ustawienia bind kontenera:

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

Z pewnością nie chcemy zapisywać na stałe wartości klucza Akismet w pliku konfiguracyjnym services.yaml więc zamiast tego użyjemy zmiennej środowiskowej (AKISMET_KEY).

Następnie każdy programista ustawia faktyczną zmienną środowiskową lub zapisuje jej wartość w pliku .env.local:

.env.local
1
AKISMET_KEY=abcdef

W przypadku środowiska produkcyjnego, należy zdefiniować faktyczną zmienną środowiskową.

Działa to nieźle, ale zarządzanie wieloma zmiennymi środowiskowymi może stać się uciążliwe. W takim przypadku Symfony pozwala lepiej rozwiązać przechowywanie poufnych danych (ang. secrets).

Przechowywanie poufnych danych (ang. secrets)

Zamiast używać wielu zmiennych środowiskowych, Symfony może zarządzać sejfem, w którym można przechowywać wiele poufnych danych. Jedną z kluczowych funkcji jest możliwość zapisywania sejfu w repozytorium (jednak bez klucza do jego otwarcia). Kolejną świetną cechą tego rozwiązania jest to, że możemy zarządzać jednym sejfem w ramach jednego środowiska.

Poufne dane są zamaskowanymi zmiennymi środowiskowymi.

Dodaj klucz API Akismet do sejfu:

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.

Ponieważ uruchamiamy to polecenie po raz pierwszy, w katalogu config/secret/dev/ pojawiły się dwa klucze. Następnie w tym samym katalogu został zapisany AKISMET_KEY.

Podczas prac w środowisku deweloperskim, możesz zdecydować się na zapisanie w repozytorium sejfu oraz kluczy, które zostały wygenerowane w katalogu config/secret/dev/.

Wartości poufnych danych mogą być nadpisane ustawieniem zmiennej środowiskowej o tej samej nazwie.

Sprawdzanie komentarzy pod kątem spamu

Podczas wysyłania nowego komentarza, prostym sposobem na sprawdzenie, czy nie jest on spamem, jest wykorzystanie obiektu klasy SpamChecker przed zapisaniem danych do bazy danych:

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

Sprawdź, czy dobrze to działa.

Zarządzanie poufnymi danymi w środowisku produkcyjnym

W środowisku produkcyjnym, Platform.sh obsługuje ustawianie poufnych zmiennych środowiskowych:

1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:AKISMET_KEY --value=abcdef

Ale jak wspomniano powyżej, użycie sejfu przechowującego poufne dane może być lepsze. Nie zwiększa bezpieczeństwa, ale ułatwia zarządzanie nimi w zespole projektowym. Wszystkie poufne dane są przechowywane w repozytorium, a jedyną zmienną środowiskową, o którą musisz zadbać w środowisku produkcyjnym, jest klucz odszyfrowujący. Dzięki temu każdy w zespole może dodać poufne dane, nawet jeśli nie ma dostępu do serwerów produkcyjnych. Konfiguracja jest jednak nieco bardziej skomplikowana.

Po pierwsze, wygeneruj parę kluczy do użytku produkcyjnego:

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

Ponownie wprowadź klucz do API Akismet w sejfie produkcyjnym, ale z wartością dla środowiska produkcyjnego:

1
$ symfony console secrets:set AKISMET_KEY --env=prod

Ostatnim krokiem jest wysłanie klucza odszyfrowującego do Platform.sh poprzez ustawienie poufnej zmiennej:

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"));'`

Możesz dodawać i zatwierdzać (ang. commit) w repozytorium wszystkie pliki; klucz odszyfrowujący został dodany do .gitignore automatycznie, więc nigdy nie zostanie w nim zatwierdzony. Dla większego bezpieczeństwa można go usunąć z maszyny lokalnej, ponieważ został już wdrożony do SymfonyCloud:

1
$ rm -f config/secrets/prod/prod.decrypt.private.php

Idąc dalej

  • Dokumentacja komponentu HttpClient;
  • Procesory zmiennych środowiskowych;
  • Ściągawka Symfony HttpClient.
Previous page Zabezpieczenie panelu administracyjnego
Next page Testowanie
This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.
TOC
    Version

    Symfony 5.4 is backed by

    Measure & Improve Symfony Code Performance

    Measure & Improve Symfony Code Performance

    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 Maxime Steinhausser, a Symfony contributor

    Thanks Maxime Steinhausser (@ogizanagi) for being a Symfony contributor

    438 commits • 28.37K 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