LaravelのアプリケーションにGoogle reCAPTCHA Enterpriseを導入する

2022/09/11に公開

問い合わせフォームからスパムメールなどが毎日届くようになってしまい、Botの問い合わせなどを避けるために、以下のバージョンの環境にGoogle reCAPTCHA Enterpriseを導入しました。

  • Laravel8
  • PHP7.3

備忘録も兼ねて導入方法を書いていきます。

TL;DR

  • GCPでセッティング
  • フロントエンドに導入してフォーム送信時にreCAPTCHAトークン取得してフォームにセット
  • サーバーサイドでreCAPTCHAトークンを元に評価(スコア)を作成
  • その評価を元にフォーム送信を通すか判定

そもそもGoogle reCAPTCHA Enterpriseとは?

不正行為、スパム、悪用からウェブサイトをスムーズに保護するのに役立ちます。
reCAPTCHA Enterprise を使用すると、クルデンシャル スタッフィング、アカウントの乗っ取り、スクレイピングなどの一般的なウェブベースの攻撃からウェブサイトを保護し、悪意のある人間や自動攻撃による多額の被害を出す悪用を防げます。
https://cloud.google.com/recaptcha-enterprise?hl=ja

現行はV3で、これまでの画像を選択やチェックボックスなどのチャレンジを必要としないスコアベースのものになっています。スコアベースになることで、ユーザーに対して余計なアクションなどを求める必要がなく、負担を軽減できます。

評価(サーバーサイド側でトークンを送ってスコアを取得する)は毎月100万回まで無料で、それ以降は費用が発生する課金体系になっています。

APIのセッティング


GCPでreCAPTCHAのAPIを検索して有効化。

サービスアカウントを作成して、権限付与・クレデンシャル情報を取得。

参考↓
https://cloud.google.com/recaptcha-enterprise/docs/set-up-google-cloud?hl=ja#create-a-service-account-for-backend-authentication

※APIキーを使う方法も書いてあるので要件に合うものをお使いください。
https://cloud.google.com/recaptcha-enterprise/docs/set-up-non-google-cloud-environments-api-keys?hl=ja

フロントエンドでスクリプト埋め込み & トークン生成

スクリプトの埋め込みはこんな感じでheadタグに埋め込めばOK。

<head>
  <script src="https://www.google.com/recaptcha/enterprise.js?render=site_key"></script>
  ....
</head>

埋め込んだら次はトークンの生成。

公式のやり方は2パターンかあります。

今回導入するページはフォームで、仕様上この2つのどちらかをそのまま適用するのが難しかったので、JS側でボタンをクリックしたときにトークンを取得して、そのトークンをinputタグのhiddenに埋め込んでフォーム送信をするようにしました。

イメージとしてはこんな感じです。

const buttonElement = document.getElementById('buttonId') as HTMLButtonElement;
buttonElement.addEventListener('click', async (e) => {
    try {
        const recaptchaToken = await getRecaptchaToken();
        // tokenをinput hidden valueにセット
    } catch (err) {
      // エラーハンドリング
    }

    // フォーム送信
})

function getRecaptchaToken(): Promise<string> {
  const siteKey = 'xxx'
  const action = 'xxxFormSubmit'
  return new Promise<string>((resolve, reject) => {
    this.recaptcha.enterprise
      .execute(siteKey, { action })
      .then((token) => {
        resolve(token);
      })
      .catch((error) => {
        reject(error);
      });
  });
}

サーバーサイド側で評価作成

まずはライブラリをインストール。

$ composer require google/cloud-recaptcha-enterprise

フロントエンドで受け取ったトークンを使用して評価を作成します。

評価を作成するとスコア(デフォルトだと0.1、0.3、0.7、0.9)とアクションが返ってくるので、実際の要件に沿ってロジックを実装すればreCAPTHCAの導入は完了です。

評価作成のイメージはこんな感じです。これをLaravelのフォームリクエストのバリデーション部分で行ないます。

use Google\Cloud\RecaptchaEnterprise\V1\RecaptchaEnterpriseServiceClient;
use Google\Cloud\RecaptchaEnterprise\V1\Event;
use Google\Cloud\RecaptchaEnterprise\V1\Assessment;
use Throwable;

class RecaptchaService
{
    protected RecaptchaEnterpriseServiceClient $client;
    protected Event $event;
    protected Assessment $assessment;
    protected float $score;
    protected string $action;

    public function __construct()
    {
        $this->client = new RecaptchaEnterpriseServiceClient(['credentials' => 'xxxx']);
        $this->event = new Event();
        $this->assessment = new Assessment();
    }

    /**
    * Create an assessment to analyze the risk of a UI action.
    * @param string $siteKey The key ID for the reCAPTCHA key (See https://cloud.google.com/recaptcha-enterprise/docs/create-key)
    * @param string $token The user's response token for which you want to receive a reCAPTCHA score. (See https://cloud.google.com/recaptcha-enterprise/docs/create-assessment#retrieve_token)
    * @return void
    */
    public function createAssessment(
        string $siteKey,
        string $token
    ): void {
        $projectName = $this->client->projectName('xxxxx');
        $this->event->setSiteKey($siteKey)->setToken($token);
        $this->assessment->setEvent($this->event);

        try {
            $response = $this->client->createAssessment($projectName, $this->assessment);

            if ((bool)$response->getTokenProperties()->getValid()) {
                // フロントで取得したreCAPTCHAトークンが有効の場合の処理
                $this->score = floor(round($response->getRiskAnalysis()->getScore(), 4) * 10) / 10;
                $this->action = $response->getTokenProperties()->getAction();
            } else {
                // フロントで取得したreCAPTCHAトークンが無効の場合の処理
            }
        } catch (Throwable $e) {
            // エラーハンドリング
        }
    }
}

サーバーサイドの実装で注意していたところ。

  • スコアを格納する変数を事前に定義するときはflotdoubleにする
    • intにしておくと1以下のスコアが0になるので注意
      -(テスト用のキーで1 or 0を返すようにしてintにしてると事故る)
  • 浮動小数点の精度
    • PHPの場合、浮動小数点の誤差が発生する(PHP: 浮動小数点数 - Manual
    • スコアが0.9でもPHPだと0.89999997xxxxxとかになる
    • なので誤差を吸収するために小数点第四位で四捨五入して小数点第二位以下を切り捨てています
      • $this->score = floor(round($response->getRiskAnalysis()->getScore(), 4) * 10) / 10

GCPの管理コンソールでリクエストのスコアが見れる

導入をすると、GCPでスコアが計測されて見れるようになります。

導入後、これを元にスコアの判定基準などの細かい調整をしていくと良さそうです。

Discussion