Vue.jsのフォームにreCAPTCHAを導入する方法

この記事では、Vue.jsを使ったお問い合わせフォームにGoogle reCAPTCHAを導入し、スパム対策を強化する方法を解説します。このフォームは、入力画面、確認画面、完了画面の3ステップで構成され、メール送信機能も実装されています。また、Google reCAPTCHAのsiteverifyエンドポイントについても詳しく解説します。


目次

  1. プロジェクト概要
  2. Google reCAPTCHA の設定
  3. ソースコード解説
  • フロントエンド (Vue.js)
  • バックエンド (PHP)
  1. siteverifyエンドポイントについて
  2. メリットと注意点
  3. 動作確認

1. プロジェクト概要

このプロジェクトでは、以下の技術を使用しています:

  • Vue.js: 3.x (CDN経由で最新バージョンを使用)
  • Vue Router: 4.x (CDN経由で最新バージョンを使用)
  • Tailwind CSS: 3.x (CDN経由で最新バージョンを使用)
  • Google reCAPTCHA v3
  • PHP: 8.2.22
  • サーバー環境: エックスサーバー

2. Google reCAPTCHA の設定

  • Google reCAPTCHA サイトに登録
    • Google reCAPTCHA管理画面 にアクセスし、サイトを登録。
    • reCAPTCHA v3 を選択し、サイトキーシークレットキーを取得します。
  • JavaScriptでreCAPTCHAを読み込む
    フォームに以下のスクリプトを追加します:
   <script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
  • サーバー側でreCAPTCHAを検証
    トークンの有効性をサーバー側で検証する必要があります。siteverifyエンドポイントを利用します(後述)。

3. ソースコード解説

(1) フロントエンド (Vue.js)

以下は、reCAPTCHAトークンの取得を含むVue.jsフォームのコードです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue.js お問い合わせフォーム</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://unpkg.com/vue@3"></script>
  <script src="https://unpkg.com/vue-router@4"></script>
  <script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
  <div id="app" class="bg-white shadow-md rounded p-6 w-full max-w-md mx-auto">
    <router-view></router-view>
  </div>

  <script>
    const { createRouter, createWebHistory } = VueRouter;
    const { createApp } = Vue;

    // 入力画面
    const InputForm = {
      template: `
        <div>
          <h1 class="text-2xl font-bold mb-4">お問い合わせフォーム</h1>
          <form @submit.prevent="goToConfirm">
            <div class="mb-4">
              <label for="name" class="block text-sm font-medium text-gray-700">名前:</label>
              <input v-model="formData.name" type="text" id="name" required class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
            </div>
            <div class="mb-4">
              <label for="email" class="block text-sm font-medium text-gray-700">メールアドレス:</label>
              <input v-model="formData.email" type="email" id="email" required class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
            </div>
            <div class="mb-4">
              <label for="message" class="block text-sm font-medium text-gray-700">お問い合わせ内容:</label>
              <textarea v-model="formData.message" id="message" required class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"></textarea>
            </div>
            <button type="submit" class="w-full bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">確認画面へ</button>
          </form>
        </div>
      `,
      data() {
        return {
          formData: JSON.parse(localStorage.getItem('formData')) || { name: '', email: '', message: '' },
        };
      },
      methods: {
        goToConfirm() {
          localStorage.setItem('formData', JSON.stringify(this.formData));
          this.$router.push({ name: 'confirm' });
        },
      },
    };

    // 確認画面
    const ConfirmForm = {
      template: `
        <div>
          <h1 class="text-2xl font-bold mb-4">内容の確認</h1>
          <p class="mb-2"><strong>名前:</strong> {{ formData.name }}</p>
          <p class="mb-2"><strong>メールアドレス:</strong> {{ formData.email }}</p>
          <p class="mb-4"><strong>お問い合わせ内容:</strong> {{ formData.message }}</p>
          <div class="flex space-x-4">
            <button @click="goBack" class="w-1/2 bg-gray-600 text-white py-2 px-4 rounded hover:bg-gray-700">戻る</button>
            <button @click="sendForm" class="w-1/2 bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">送信する</button>
          </div>
        </div>
      `,
      data() {
        return {
          formData: JSON.parse(localStorage.getItem('formData')) || { name: '', email: '', message: '' },
        };
      },
      methods: {
        goBack() {
          this.$router.push({ name: 'input' });
        },
        sendForm() {
          grecaptcha.ready(() => {
            grecaptcha.execute('YOUR_SITE_KEY', { action: 'submit' }).then((token) => {
              fetch('backend/contact.php', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ ...this.formData, recaptcha_token: token }),
              })
                .then((response) => response.json())
                .then((data) => {
                  if (data.success) {
                    localStorage.removeItem('formData');
                    this.$router.push({ name: 'complete' });
                  } else {
                    alert('送信に失敗しました: ' + data.error);
                  }
                })
                .catch(() => alert('エラーが発生しました。通信環境を確認してください。'));
            });
          });
        },
      },
    };

    // 完了画面
    const CompleteForm = {
      template: `
        <div>
          <h1 class="text-2xl font-bold mb-4">送信が完了しました</h1>
          <p>お問い合わせいただきありがとうございます。</p>
          <button @click="goToInput" class="mt-4 w-full bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">フォームに戻る</button>
        </div>
      `,
      methods: {
        goToInput() {
          this.$router.push({ name: 'input' });
        },
      },
    };

    const routes = [
      { path: '/', name: 'input', component: InputForm },
      { path: '/confirm', name: 'confirm', component: ConfirmForm },
      { path: '/complete', name: 'complete', component: CompleteForm },
    ];

    const router = createRouter({
      history: createWebHistory(),
      routes,
    });

    const App = {
      template: '<router-view></router-view>',
    };

    const app = createApp(App);
    app.use(router);
    app.mount('#app');
  </script>
</body>
</html>

(2) バックエンド (PHP)

以下は、reCAPTCHAトークンを検証し、メールを送信するPHPコードです。

<?php
header('Content-Type: application/json');

// リクエストデータの取得
$data = json_decode(file_get_contents('php://input'), true);

// reCAPTCHA の検証
$recaptcha_url = 'https://www.google.com/recaptcha/api/siteverify';
$recaptcha_secret = 'YOUR_SECRET_KEY'; // シークレットキーを設定
$recaptcha_response = $data['recaptcha_token'];

$response = file_get_contents($recaptcha_url . '?secret=' . $recaptcha_secret . '&response=' . $recaptcha_response);
$recaptcha = json_decode($response, true);

if (!$recaptcha['success']) {
    echo json_encode(['success' => false, 'error' => 'reCAPTCHA 検証に失敗しました。']);
    exit;
}

// 管理者への通知メール
$toAdmin = 'admin@example.com';
$subjectAdmin = 'お問い合わせフォームからのメッセージ';
$messageAdmin = "名前: {$data['name']}\nメールアドレス: {$data['email']}\nメッセージ: {$data['message']}";
$headersAdmin = 'From: noreply@example.com';

$mailToAdmin = mail($toAdmin, $subjectAdmin, $messageAdmin, $headersAdmin);

// 問い合わせ者への自動返信メール
$toUser = $data['email'];
$subjectUser = 'お問い合わせありがとうございます';
$messageUser = "{$data['name']} 様\n\nお問い合わせありがとうございます。\n以下の内容で受け付けました。\n----------------------\nお名前: {$data['name']}\nメールアドレス: {$data['email']}\nお問い合わせ内容:\n{$data['message']}\n----------------------\n\n担当者より折り返しご連絡いたします。";
$headersUser = 'From: noreply@example.com';

$mailToUser = mail($toUser, $subjectUser, $messageUser, $headersUser);

if ($mailToAdmin && $mailToUser) {
    echo json_encode(['success' => true]);
} else {
    echo json_encode(['success' => false, 'error' => 'メール送信に失敗しました。']);
}

4. siteverifyエンドポイントについて

Google reCAPTCHA の siteverify エンドポイント は、reCAPTCHA トークンをサーバー側で検証するためのAPIです。これを利用することで、reCAPTCHAによって生成されたトークンが有効かどうかを確認します。

siteverifyの役割

  • クライアントサイドで生成されたreCAPTCHAトークンを検証。
  • サーバー側でGoogleの検証結果を受け取り、信頼性を判断。

リクエスト構成

  • エンドポイント URL:
    https://www.google.com/recaptcha/api/siteverify
  • 必要なパラメータ:
  • secret: サーバー側で管理するシークレットキー
  • response: クライアントサイドから送信されたトークン

例: PHPによる検証コード

以下はPHPでsiteverifyエンドポイントを使ってreCAPTCHAトークンを検証するコードです。

$recaptcha_url = 'https://www.google.com/recaptcha/api/siteverify';
$recaptcha_secret = 'YOUR_SECRET_KEY';
$recaptcha_response = $data['recaptcha_token'];

$response = file_get_contents($recaptcha_url . '?secret=' . $recaptcha_secret . '&response=' . $recaptcha_response);
$recaptcha = json_decode($response, true);

if (!$recaptcha['success']) {
    echo json_encode(['success' => false, 'error' => 'reCAPTCHA 検証に失敗しました。']);
    exit;
}

レスポンス例

{
  "success": true,
  "challenge_ts": "2024-11-22T12:34:56Z",
  "hostname": "example.com",
  "score": 0.9,
  "action": "submit"
}

レスポンスのフィールド:

フィールド名説明
success検証が成功したかどうか(true または false
challenge_tsユーザーがreCAPTCHAを解決したタイムスタンプ
hostnameトークンが発行されたドメイン
score信頼スコア (v3のみ)。0.0〜1.0で値が高いほど信頼できる
actionフロントエンドで指定したアクション名
error-codes検証が失敗した場合のエラーコード

5. メリットと注意点

メリット

  1. スパム対策
    reCAPTCHAを導入することで、自動化されたスパム送信を防ぐことができます。
  2. 非侵入型保護 (v3)
    reCAPTCHA v3ではユーザー体験を妨げることなく、スコアによる判断が可能です。
  3. 簡単な統合
    Vue.jsおよびPHPとの統合が容易で、柔軟なフォーム設計が可能です。

注意点

  1. HTTPS必須
    reCAPTCHAはHTTPS環境でのみ動作します。
  2. シークレットキーの管理
    シークレットキーは絶対にサーバー外に漏れないようにしてください。
  3. スコアの確認
    reCAPTCHA v3では、scoreを確認し、一定の閾値を設定して適切に動作を制御してください。

6. 動作確認

以下の手順で動作を確認できます。

  1. フォーム入力
  • 名前, メールアドレス, お問い合わせ内容を入力。
  • 「確認画面へ」をクリック。
  1. 確認画面
  • 入力内容が正しいかを確認。
  • 「送信する」をクリックすると、reCAPTCHAトークンが生成されます。
  1. メール送信および完了画面
  • バックエンドでreCAPTCHAトークンが検証され、メールが送信されます。
  • 「送信が完了しました」と表示されます。

スクリーンショット例

入力画面

確認画面

完了画面


この記事では、Vue.jsとreCAPTCHAを利用したお問い合わせフォームの作成方法を解説しました。この手順を活用して、安全性の高いフォームを作成してください!

コメント

タイトルとURLをコピーしました