6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHPでGoogle reCAPTCHA Enterpriseを実装してみる

Last updated at Posted at 2025-01-16

はじめに

この記事は以下の人向けの資料となります。

  • 新しくウェブサイトを構築している方
  • Google reCAPTCHAのクラシック版を使っている方

クラシック版管理画面

クラシック版についての説明記事

より

もともと「reCAPTCHA」と呼ばれていたサービスです。v2 / v3のバージョンで機能が違います。
昔から(2020年以前まで?)無料でreCAPTCHAを使っている場合は、たぶんこれです。

のようです。

最近自社サービスに「Google reCAPTCHA Enterprise版」を実装したのでそのやり方を書きます。

この記事で取り上げているのはv2版(難易度中)です。
v3版(難易度高)とはViewの実装が少し異なるかもしれません。
サーバー側のトークン検証に関してはv2とv3で違いはない認識です。

※当記事では、トークンの有効性のチェックのみでリスク検証については紹介をしておりません。

前提

Google Cloud Platformのお支払い方法登録を事前に完了させておく必要があります。

実装手順の紹介

APIキーの設定編

手順1.Google Cloud Platformのコンソールを開く

スクリーンショット 2024-12-05 9.31.38.png

手順2.検索窓から「APIとサービス」を選択

スクリーンショット 2024-12-05 9.32.56.png

手順3.左メニューから「認証情報」を選択します

スクリーンショット 2024-12-05 9.34.07.png

手順4.「認証方法を作成」を選択

スクリーンショット 2024-12-05 9.39.41.png

※赤い四角の部分は自社サービスに関わる部分なのでマスキングしてあります。

手順5.「APIキー」を選択

スクリーンショット 2024-12-05 10.36.27.png

手順6.APIキーの設定をして保存

■必須

  • 名前
  • APIの制限
    • reCAPTCHA Enterprise API

■任意

  • アプリケーションの制限の設定

スクリーンショット 2024-12-05 10.37.24.png

APIキーの設定編は以上で終了です。

続いて、Google reCAPTCHA Enterprise編に移ります。

Goolge reCAPTCHA Enterpriseの設定編

手順1.検索窓から「reCAPTCHA」を選択

スクリーンショット 2024-12-05 10.45.28.png

手順2.「キーを作成」を選択

スクリーンショット 2024-12-05 10.47.22.png

手順3.reCAPTCHAの設定を保存する

■必須

  • 表示名
  • プラットフォームの種類を選択
  • ドメインリスト
  • キーのタイプ
    • チェックボックスによる本人確認を使用する
      • 難易度中 or 難易度高

スクリーンショット 2024-12-05 10.49.50.png

Goolge reCAPTCHA Enterpriseの設定編は以上で終了です。

続いて、実装編に移ります。

実装編

手順1.システム開発を開く

スクリーンショット 2024-12-05 10.56.40.png

手順2.「ウェブサイトに reCAPTCHA を追加する」の指示に従ってHTMLにスクリプトを埋め込む

■下記をhead部に記載します

※QUERY STRINGとして「hl」を指定することができます。
hl=jaとした場合、日本語
hl=enとした場合、英語

サンプル(通常)
<script src="https://www.google.com/recaptcha/enterprise.js" async></script>
サンプル(QUERY STRING付き)
<script src="https://www.google.com/recaptcha/enterprise.js?hl=ja" async></script>

■reCAPTCHAを表示させたいところに、以下を記載します

※サンプルとしては下記が記載してありますが、カスタマイズすることもできます

<サンプル版>

サンプル
<div class="g-recaptcha" data-sitekey="YOUR SITE KEY" data-action="LOGIN"></div>

<カスタマイズ版>

カスタマイズ
<div class="g-recaptcha" data-callback="callbackGoogleRecaptcha" data-expired-callback="callbackGoogleRecaptchaExpired" data-sitekey="YOUR SITE KEY"></div>

<input type="hidden" id="google_recaptcha_code" value="" />

手順3.callbackを定義(任意)

Google reCAPTCHAの認証が成功するまで送信するボタンを押させたくないので、callbackを定義しています。

カスタマイズ
$(document).ready(function(){
    // TODO 送信するボタンを押せないように制御します
});

/**
 * Google reCAPTCHAでチェックがついた場合のコールバックです
 *
 * @param code
 */
function callbackGoogleRecaptcha(code) {
    if (code !== '') {
        // TODO トークンを取得できたら、送信するボタンを押せるようにします。
        
        // トークンをhiddenでおいておく。これはフラグでもいい。
        $('#google_recaptcha_code').val(code);
    }
}

/**
 * Google reCAPTCHAでタイムアウトした場合のコールバックです
 */
function callbackGoogleRecaptchaExpired() {
    // TODO 送信するボタンを押せないように制御します

    // トークンを削除する
    $('#google_recaptcha_code').val('');
}

/**
 * Google reCAPTCHAで次へボタンをクリックしたときの処理です
 * onClickに使用します
 */
function clickNextButton() {
    // トークンがない場合は送信をしない
    if ($('#google_recaptcha_code').val() === '') return;

    // 送信を発火させます
    form.submit();
}

上記JavaScriptによる制御のサンプル

■未認証時

スクリーンショット 2024-12-05 17.51.05.png

■認証完了後

スクリーンショット 2024-12-05 17.51.13.png

■時間経過後

スクリーンショット 2024-12-05 17.52.15.png

手順4.サーバーで検証をする

スクリーンショット 2024-12-05 11.20.16.png

今回は「REST API」による検証となります。

■検証プログラム

Google_recaptcha

class Google_recaptcha
{
    private const ENTERPRISE_BASE_URL = 'https://recaptchaenterprise.googleapis.com/v1/projects';

    public function __construct(private array $options)
    {
    }

    /**
     * @return string
     */
    private function get_enterprise_project_url(): string
    {
        return sprintf('%s/%s', self::ENTERPRISE_BASE_URL, $this->options['google_recaptcha_project_name']);
    }

    /**
     * 利用方法が共通な可能性があるので、メソッドとして切り出す
     *
     * @param string $request
     * @param string $method
     *
     * @return array
     */
    private function call_google_recaptcha_enterprise(string $request, string $method): array
    {
        $enterprise_project_url = $this->get_enterprise_project_url();
        $request_url = sprintf('%s/%s', $enterprise_project_url, $method);

        $query = [
            'key' => $this->options['google_recaptcha_api_key'],
        ];

        $url = sprintf('%s?%s', $request_url, http_build_query($query));

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            'Content-Length: ' . strlen($request),
        ]);

        // 処理タイムアウト
        curl_setopt($ch, CURLOPT_TIMEOUT, 15);

        $output = curl_exec($ch);
        curl_close($ch);

        // レスポンスが返ってこないときは空配列とする
        if (empty($output)) {
            return [];
        }

        return json_decode($output, true);
    }

    /**
     * @param string $token
     *
     * @return array
     */
    public function assessments(string $token): array
    {
        $body = [
            'event' => [
                'token' => $token,
                'expectedAction' => '',
                'siteKey' => $this->options['google_recaptcha_site_key'],
            ]
        ];

        $jsonBody = json_encode($body);

        return $this->call_google_recaptcha_enterprise($jsonBody, 'assessments');
    }

    /**
     * @param string $token
     *
     * @return bool
     */
    public function validate(string $token): bool
    {
        $response = $this->assessments($token);

        // レスポンスが返ってこないときはエラー
        if (empty($response)) {
            return false;
        }

        // トークンの妥当性はこのプロパティで判断
        return $response['tokenProperties']['valid'] ?? false;

    }
}

検証するときは

// フレームワークごとにリクエストの取得方法は変更する
$token = $_POST['g-recaptcha-response'] ?? '';

// Google_recaptchaをインスタンス化する
$googleRecaptcha = new Google_recaptcha([
    'google_recaptcha_api_key' => getenv('GOOGLE_RECAPTCHA_API_KEY'),
    'google_recaptcha_site_key' => getenv('GOOGLE_RECAPTCHA_SITE_KEY'),
    'google_recaptcha_project_name' => getenv('GOOGLE_RECAPTCHA_PROJECT_NAME'),
]);

// 成功:true 失敗:false
$result = googleRecaptcha->validate($token);

とします。

※何も手を加えていなければ、Google reCAPTCHAのトークンは「g-recaptcha-response」という名前でリクエストがされるかなと思われます。

手順5.Configを定義

envファイルを作成

.env
GOOGLE_RECAPTCHA_API_KEY=YOUR API KEY=fugafuga
GOOGLE_RECAPTCHA_SITE_KEY=YOUR SITE KEY=hogehoge
GOOGLE_RECAPTCHA_PROJECT_NAME=YOUR PROJECT_NAME=hogefuga

完成

2024.12.03現在返ってくるレスポンス

OK時のレスポンス

{
  "name": "projects/xxxxxxxxxxxx/assessments/yyyyyyyyyyyyyy",
  "event": {
    "token": "aaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "siteKey": "hogehoge",
    "userAgent": "",
    "userIpAddress": "",
    "expectedAction": "",
    "hashedAccountId": "",
    "express": false,
    "requestedUri": "",
    "wafTokenAssessment": false,
    "ja3": "",
    "headers": [],
    "firewallPolicyEvaluation": false,
    "fraudPrevention": "FRAUD_PREVENTION_UNSPECIFIED"
  },
  "riskAnalysis": {
    "score": 1,
    "reasons": [],
    "extendedVerdictReasons": [],
    "challenge": "CHALLENGE_UNSPECIFIED"
  },
  "tokenProperties": {
    "valid": true,
    "invalidReason": "INVALID_REASON_UNSPECIFIED",
    "hostname": "localhost",
    "androidPackageName": "",
    "iosBundleId": "",
    "action": "",
    "createTime": "2024-12-03T04:52:23.582Z"
  },
  "accountDefenderAssessment": {
    "labels": []
  }
}

NGレスポンス

{
  "name": "projects/xxxxxxxxxxxx/assessments/yyyyyyyyyyyyyy",
  "event": {
    "token": "bbbbbbbbbbbbbbbbbbbbbbbbbbb",
    "siteKey": "hogehoge",
    "userAgent": "",
    "userIpAddress": "",
    "expectedAction": "",
    "hashedAccountId": "",
    "express": false,
    "requestedUri": "",
    "wafTokenAssessment": false,
    "ja3": "",
    "headers": [],
    "firewallPolicyEvaluation": false,
    "fraudPrevention": "FRAUD_PREVENTION_UNSPECIFIED"
  },
  "riskAnalysis": {
    "score": 0,
    "reasons": [],
    "extendedVerdictReasons": [],
    "challenge": "CHALLENGE_UNSPECIFIED"
  },
  "tokenProperties": {
    "valid": false,
    "invalidReason": "MALFORMED",
    "hostname": "",
    "androidPackageName": "",
    "iosBundleId": "",
    "action": "",
    "createTime": "1970-01-01T00:00:00Z"
  }
}

さいごに

今回は、Goolge reCAPTCHA Enterpriseの実装をしてみました。

トークンの検証方法は
他にも

  • C#
  • Go
  • Java
  • Node.js
  • PHP
  • Python
  • Ruby

の言語が紹介されているのですが、

こちらは以下の理由により個人的におすすめしません。

REST APIはライブラリに依存しないため容易に実装ができます。

■理由

  • どのライブラリのバージョンを使った実装サンプルなのか不明
  • ライブラリ依存関係の解決が大変

以上、参考にしてもらえればと思います。

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?