ステップ 16: API でスパム対策をする
API でスパム対策をする¶
ロボットやスパマーなど誰でもフィードバックを投稿すること可能な状態ですので、 "CAPTCHA" を追加したり、サードパーティのAPI を使用して、ロボットからの投稿から保護することを考えます。
Akismet を使用することにしましょう。ここでは、アウトオブバンドに Akismet の API を呼ぶ方法を説明します。
Symfony HTTPClient コンポーネントに依存させる¶
Akismet API を抽象化したライブラリを使用するのではなく、まず直接API を呼んでみましょう。HTTP 呼び出しがより効率的です(Symfonyプロファイラが使用できるのでSymfonyデバッグツールの恩恵が得られます)。
API 呼び出しをするために、 Symfony の HTTPClient コンポーネントを使用します:
1 | $ symfony composer req http-client
|
スパムチェッカークラスを設計する¶
src/
以下に、新しいクラス SpamChecker
を追加し、 Akismet API の呼び出しロジックをラップしてレスポンスを解釈させます:
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;
}
}
|
HTTP クライアントの request()
メソッドは、 Akismet URL($this->endpoint
) に POST リクエストを行い、パラメーターの配列を渡します。
getSpamScore()
メソッドは API 呼び出しのレスポンスに応じて 3つの値を返します:
2
: コメントが "露骨なスパム";1
コメントがスパムの可能性がある;0
コメントがスパムでない。
ちなみに
特別なメールアドレスの akismet-guaranteed-spam@example.com
を使用すると強制的にスパムと判定させることができます。
環境変数を使用する¶
SpamChecker
クラスは $akismetKey
引数が必要です。ファイルアップロードのディレクトリのときと同じようにコンテナの設定に バインド
させてインジェクトします:
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:
$photoDir: "%kernel.project_dir%/public/uploads/photos"
+ $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
|
services.yaml
にハードコードで Akismet のキーを書くことは避けたいですので、環境変数の (AKISMET_KEY
) を使用することにします。
"本当の" 環境変数をセットするか .env.local
ファイルに値をセットするかは各エンジニアの判断に任せます:
1 | AKISMET_KEY=abcdef
|
本番においては、 "本当の" 環境変数を定義するべきです。
多くの環境変数を管理するのは大変ですので、 Symfony は、シークレット情報を格納するのに "ベター" な方法があります。
シークレット情報を格納する¶
たくさんの環境変数を使用する代わりに、Symfony では、シークレット情報を格納することができる ヴォールト で管理することができます。例えば、メリットの一つとしてリポジトリにヴォールトをコミットすることができます(キーは入れないでください)。さらに、環境毎にヴォールトを管理することも可能です。
シークレット情報は、実際の値を覆った環境変数になります。
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
ディレクトリにキーが2つ生成されます。そして、 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;
@@ -39,7 +40,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);
@@ -57,6 +58,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()]);
|
正しく動作するかチェックする
本番でシークレットを管理する¶
本番では、 SymfonyCloud は 注意が必要な環境変数 の設定をサポートしています:
1 | $ symfony var:set --sensitive AKISMET_KEY=abcdef
|
しかし、上記で議論したように、セキュリティーの面からではなくプロジェクトチームのシークレット管理の面から、Symfony のシークレット管理を使う方がベターです。全てのシークレットがリポジトリに格納されるので、唯一の管理するべき本番の環境変数は復号キーのみとなります。こうすることで、少しセットアップが面倒ですが、チームの誰もが本番のサーバーへのアクセス権がなくても、本番のシークレットを追加することができます。
まず、本番用のキーのペアを生成してください:
1 | $ APP_ENV=prod symfony console secrets:generate-keys
|
本番用の Akismet のシークレットを本番のヴォールトに再追加してください:
1 | $ APP_ENV=prod symfony console secrets:set AKISMET_KEY
|
最後に、SymfonyCloud に、注意が必要な値をセットした際の復号キーを送ってください:
1 | $ symfony var:set --sensitive SYMFONY_DECRYPTION_SECRET=`php -r 'echo base64_encode(include("config/secrets/prod/prod.decrypt.private.php"));'`
|
復号キーは、 .gitignore
に自動的に追加されているので、コミットされることはありませんので、全てのファイルを追加することができます。デプロイが終わったので、安全のために、ローカルマシンから削除しておいてください:
1 | $ rm -f config/secrets/prod/prod.decrypt.private.php
|
- « Previous ステップ 15: 管理者のバックエンドをセキュアにする
- Next » ステップ 17: テストをする
This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.