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.
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
:
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 theprod
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