Frontend Craft Labフロントエンド開発の実験場
← 記事一覧に戻る
security

reCAPTCHAを導入する方法

reCAPTCHAを導入する方法を紹介します。

reCAPTCHAを導入する方法

そもそもreCAPTCHAとは?

reCAPTCHAは、Googleが提供しているCAPTCHAサービスです。

CAPTCHAは、ユーザーが人間かボット(コンピューター)かを判断するためのサービスです。

reCAPTCHAの種類

reCAPTCHAには、以下の2種類があります。

  • reCAPTCHA v2は、ユーザーがチェックボックスをクリック、画像の判別を行うことで、人間かBotかを判断します。 そのため、ユーザーエクスペリエンスが悪いですね。

  • reCAPTCHA v3は、v2のようなチェックボックスをクリックすることなく、ユーザーの行動データをもとに人間かBotかを判断することができます。

※ reCAPTCHA v1は、すでにサービス終了しています。

reCAPTCHAを導入する方法

大まかな手順は以下の通りです。

  1. reCAPTCHAコンソール(Google Cloud Platform)での設定
  2. サイトにreCAPTCHAを導入

reCAPTCHAコンソール(Google Cloud Platform)での設定

Google Cloud Platformにアクセスします。

  1. 使ってみる」をクリックします。

reCAPTCHAコンソール

  1. ラベルやドメインを設定します。

reCAPTCHAコンソール

  1. サイトキーとシークレットキーを取得します。

reCAPTCHAコンソール

reCAPTCHA Classic 管理コンソール

以前と管理画面の仕様が変わっており戸惑うかもしれませんが、 従来の reCAPTCHA Classic 管理コンソールにアクセスして設定することも可能です。

reCAPTCHAをサイトで使用できるようにコーディング

reCAPTCHA v3 ガイドのサンプルコードを参考に、サイトにreCAPTCHAを導入します。

reCAPTCHA Classic 管理コンソールの左側メニューから、 「以前の管理コンソール」をクリックすると、従来の管理画面にアクセスすることができます。

PHPをバックエンド使用した場合のサンプルコード

大まかな処理の流れとしては、

  1. フロントエンドでreCAPTCHAのトークンを取得
  2. PHPで実装した確認画面でトークンを検証
  3. 検証に成功した場合、その後の処理を実行

フロントエンド

<head>
    <!-- サイトキーを使用して JavaScript API を読み込みます -->
    <script src="https://www.google.com/recaptcha/enterprise.js?render=reCAPTCHA_site_key"></script>
</head>
<div class="contactBox">
    <form action="mail_web.php" method="post" id="contactform">
        <table class="formTable">
            <tr>
                <th>
                    <label for="name">お名前</label>
                </th>
                <td>
                    <input type="text" name="name" id="name" placeholder="お名前を入力してください" required>
                </td>
            </tr>
            <tr>
                <th>
                    <label for="email">メールアドレス</label>
                </th>
                <td>
                    <input type="email" name="email" id="email" placeholder="メールアドレスを入力してください">
                </td>
            </tr>
            <tr>
                <th>
                    <label for="message">お問い合わせ内容</label>
                </th>
                <td>
                    <textarea name="message" id="message" placeholder="お問い合わせ内容を入力してください"></textarea>
                </td>
            </tr>
        </table>
        <!-- /.formTable -->
        <input type="hidden" name="recaptchaToken" id="recaptchaToken">
        <input type="submit" value="送信">
    </form>
</div>
<!-- /.contactBox -->
 
<!-- 送信ボタンをクリックした際に、reCAPTCHAのトークンを取得して送信 -->
<script>
    document.getElementById('contactform').addEventListener('submit', onClick);
 
    function onClick(e) {
        e.preventDefault();
        grecaptcha.enterprise.ready(function() {
            // トークンを取得
            grecaptcha.enterprise.execute('6Le7MhYqAAAAAKs13l-SpAbsvceKruD2-CqY7swM', {
                action: 'submit'
            }).then(function(token) {
                // トークンを送信
                var recaptchaToken = document.getElementById('recaptchaToken');
                recaptchaToken.value = token;
                document.getElementById('contactform').submit();
                console.log('✅ 送信完了、トークン:' + token);
            });
        });
    }
</script>

送信先、確認画面をPHPで作成

<?php
require_once '../include/recaptcha_logger.php';
$name = $_POST['name'];
$email = $_POST['email'];
$message = $_POST['message'];
$recaptchaToken = $_POST['recaptchaToken'];
 
/**
 * @var int $sendmail 送信フラグ
 */
$sendmail = 0;
 
$sendmail = isset($_POST['mail_set']) ? 1 : 0;
?>
 
<!-- 入力内容の確認 -->
<?php if ($sendmail == 0) { ?>
    <?php
    $recaptchaLogger = new ReCaptchaLogger();
    if (isset($_POST['recaptchaToken'])) {
        $verifyResult = $recaptchaLogger->verifyToken($recaptchaToken, $_POST);
        if (!$verifyResult->success) {
    ?>
            <div class="input-confirm">
                <h2>入力内容の確認</h2>
                <p>名前:<?php echo $name; ?></p>
                <p>メールアドレス:<?php echo $email; ?></p>
                <p>お問い合わせ内容:<?php echo $message; ?></p>
            </div>
            <p>reCAPTCHAの検証に失敗しました。</p>
            <a href="index.php">戻る</a>
        <?php
        } else {
        ?>
            <div class="input-confirm">
                <form action="mail_web.php" method="post">
                    <h2>入力内容の確認</h2>
                    <p>名前:<?php echo $name; ?></p>
                    <p>メールアドレス:<?php echo $email; ?></p>
                    <p>お問い合わせ内容:<?php echo $message; ?></p>
                    <input type="hidden" name="name" value="<?php echo $name; ?>">
                    <input type="hidden" name="email" value="<?php echo $email; ?>">
                    <input type="hidden" name="message" value="<?php echo $message; ?>">
                    <input type="hidden" name="recaptchaToken" value="<?php echo $recaptchaToken; ?>">
                    <input type="hidden" name="mail_set" value="confirm_submit">
                    <input type="submit" value="送信">
                </form>
            </div>
<?php }
    }
}
if ($sendmail == 1) {
    header('Location: thanks.php');
}

reCAPTCHAの検証についての処理をクラスで管理

<?php
 
class ReCaptchaLogger
{
    private $log_dir;
    private $secret_key;
 
    public function __construct($secret_key = 'reCAPTCHA_secret_key')
    {
        $this->log_dir = __DIR__ . '/../logs/recaptcha/';
        if (!file_exists($this->log_dir)) {
            mkdir($this->log_dir, 0777, true);
        }
        $this->secret_key = $secret_key;
    }
 
    /**
     * ログを保存する
     * @param array $data ログデータ
     */
    private function saveJsonLog($data)
    {
        try {
            $json_file = $this->log_dir . 'recaptcha_' . date('Y-m-d_H-i-s') . '.json';
            if (file_put_contents($json_file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) === false) {
                error_log('ファイルに書き込めませんでした: ' . $json_file);
            }
        } catch (Exception $e) {
            error_log('エラーが発生しました: ' . $e->getMessage());
        }
    }
 
    /**
     * CURLエラーをログに記録する
     * @param string $error エラーメッセージ
     * @param array $post_data 送信データ
     */
    private function logCurlError($error, $post_data)
    {
 
        $error_data = [
            'timestamp' => date('Y-m-d H:i:s'),
            'type' => 'recaptcha_curl_error',
            'error' => $error,
            'post_data' => $post_data,
        ];
 
        $this->saveJsonLog($error_data);
    }
 
    /**
     * 検証エラーをログに記録する
     * @param array $error_messages エラーメッセージ
     * @param array $verify_result 検証結果
     * @param array $post_data 送信データ
     */
    private function logVerificatonError($error_messages, $verify_result, $post_data)
    {
        $error_data = [
            'timestamp' => date('Y-m-d H:i:s'),
            'type' => 'recaptcha_verification_error',
            'error_messages' => $error_messages,
            'verify_result' => $verify_result,
            'post_data' => $post_data,
        ];
 
        $this->saveJsonLog($error_data);
    }
 
    private function getVerificationErrorMessages($error_codes)
    {
        $error_messages = [];
        foreach ($error_codes as $code) {
            switch ($code) {
                case 'missing-input-secret':
                    $error_messages[] = 'シークレットキーが指定されていません。';
                    break;
                case 'invalid-input-secret':
                    $error_messages[] = 'シークレットキーが無効です。';
                    break;
                case 'missing-input-response':
                    $error_messages[] = 'reCAPTCHAのレスポンスが見つかりません。';
                    break;
                case 'invalid-input-response':
                    $error_messages[] = 'reCAPTCHAのレスポンスが無効です。';
                    break;
                case 'bad-request':
                    $error_messages[] = 'リクエストが無効です。';
                    break;
                case 'timeout-or-duplicate':
                    $error_messages[] = 'レスポンスの有効期限が切れているか、既に使用されています。';
                    break;
                default:
                    $error_messages[] = '不明なエラーが発生しました。';
            }
        }
        return $error_messages;
    }
 
    /**
     * トークンを検証する
     * @param string $token トークン
     * @param array $post_data 送信データ
     * @return object 検証結果
     */
    public function verifyToken($token, $post_data)
    {
        $url = 'https://www.google.com/recaptcha/api/siteverify';
        $data = [
            'secret' => $this->secret_key,
            'response' => $token,
        ];
 
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // localhostで証明書の検証を無視
        $response = curl_exec($ch);
 
        if (curl_errno($ch)) {
            $this->logCurlError(curl_error($ch), $post_data);
            curl_close($ch);
            return (object) ['success' => false];
        }
 
        $verifyResult = json_decode($response);
        curl_close($ch);
 
        if (!$verifyResult->success) {
            $error_messages = $this->getVerificationErrorMessages($verifyResult->{'error-codes'} ?? []);
            $this->logVerificatonError($error_messages, $verifyResult, $post_data);
        }
 
        return $verifyResult;
    }
}