Защита от спама с помощью API
Отзыв может оставить кто угодно, включая роботов и спамеров. Поэтому, чтобы снизить поток спама, мы можем либо добавить в форму капчу, либо использовать сторонние API.
Я решил использовать бесплатный сервис Akismet, чтобы показать, как можно работать с API и как выполнять внешние запросы.
Регистрация в Akismet
Зарегистрируйте бесплатный аккаунт на akismet.com и получите ключ Akismet API.
Добавление компонента Symfony HTTPClient
Мы будем обращаться к API Akismet напрямую вместо использования соответствующей библиотеки для этого. Выполнение HTTP-запросов самостоятельно более эффективно (кроме этого даёт использовать инструменты отладки Symfony, включая профилировщик Symfony).
Создание класса для проверки на спам
В директории src/
создадим новый класс SpamChecker
, который будет содержать логику отправки запроса к API Akismet и обработку его ответа:
Метод HTTP-клиента request()
отправляет POST-запрос на URL-адрес Akismet ($this->endpoint
) и передаёт массив параметров.
Метод getSpamScore()
возвращает 3 значения в зависимости от ответа на API-вызов:
2
: если комментарий является явным спамом;1
: если комментарий может быть спамом;0
: если комментарий не спам (так называемый ham).
Tip
Используйте специальный адрес электронной почты akismet-guaranteed-spam@example.com
, чтобы антиспам-сервис пометил такое сообщение как спам и вы таким образом смогли проверить его работу.
Использование переменных окружения
Класс SpamChecker
зависит от параметра $akismetKey
. Как и в случае с директорией для загруженных файлов, мы можем переместить ключ в параметр контейнера, используя свойство bind
:
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
Разумеется, мы не будем хранить значение ключа Akismet в конфигурационном файле services.yaml
, поэтому используем переменную окружения (AKISMET_KEY
).
Затем каждый разработчик может сам определить переменную окружения в терминале или хранить ключ в файле .env.local
:
Однако в продакшене должна быть определена только "реальная" переменная окружения в терминале.
Это неплохой рабочий вариант, хотя управление множеством переменных окружения может стать обременительным. В таком случае у Symfony есть "лучшая" альтернатива, когда речь заходит о хранении конфиденциальных данных.
Хранение конфиденциальных данных
Вместо использования множества переменных окружения, в Symfony есть хранилище, в котором можно поместить много конфиденциальных данных. Среди одной из ключевых особенностей — можно сохранить хранилище в репозитории (но без ключа, чтобы его открыть). Другое замечательное преимущество состоит в том, что для каждого окружения может быть создано собственное хранилище.
На деле такие конфиденциальные данные являются неявными переменными окружения.
Добавьте ключ Akismet в хранилище:
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.
Поскольку мы запускаем эту команду впервые, она сгенерировала два ключа в директорию config/secret/dev/
. Затем эта команда сохранила секретную строку AKISMET_KEY
в этой же директории.
Для хранения конфиденциальных данных в процессе разработки вы можете сохранить в репозитории хранилище вместе с ключами в директории config/secret/dev/
.
Все конфиденциальные данные также можно переопределить путём определения одноимённой переменной окружения.
Проверка комментариев на спам
При отправке нового комментария одним из простых способов проверить его на спам — вызвать антиспам-сервис перед сохранением данных в базе данных:
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()]);
Убедитесь, что всё работает правильно.
Управление конфиденциальными данными в продакшене
Для продакшена Platform.sh поддерживает установку конфиденциальных переменных окружения:
1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:AKISMET_KEY --value=abcdef
Однако, как отмечалось выше, использование механизма Symfony для управления конфиденциальными данными может быть более предпочтительным вариантом. Но не с точки зрения безопасности, а в плане управления секретными данными в команде проекта. Поскольку все конфиденциальные данные хранятся в репозитории, то чтобы использовать их в продакшене нужна только специальная переменная окружения с ключом дешифрования. Благодаря такому подходу каждый участник команды может добавить новые защищённые переменные окружения для использования в продакшене, даже если у него нет к нему доступа. Хотя для настройки этого процесса нужно кое-что сделать.
Прежде всего, сгенерируйте пару ключей для использования в продакшене:
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
Повторно добавьте ключ Akismet в хранилище продакшена, но теперь уже с его действительным значением:
1
$ symfony console secrets:set AKISMET_KEY --env=prod
Последний шаг — отправьте ключ дешифрования в Platform.sh, установив специальную для этого переменную:
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"));'`
Можно добавить все файлы в репозиторий, так как файл с ключом дешифрования уже игнорируется в .gitignore
, поэтому он никогда не будет зафиксирован. Для большей безопасности лучше удалите его с вашего компьютера, потому что он уже есть в продакшене и больше не понадобится:
1
$ rm -f config/secrets/prod/prod.decrypt.private.php