React フォームに reCAPTCHA を導入する方法

この記事では、以前作成した React フォームに Google reCAPTCHA を導入する手順をご紹介します。reCAPTCHA を追加することで、不正な送信やボットからのアクセスを防ぐことができます。


1. プロジェクト構成

以下は、今回のプロジェクト構成例です。

/
├── index.html        # フロントエンドコード
├── backend/
│   └── contact.php   # バックエンド処理

2. 必要なライブラリとスクリプト

以下のスクリプトを利用します。

  • React, ReactDOM
  • React Router DOM (バージョン 5.x)
  • Google reCAPTCHA v3

スクリプトは <head> タグ内で読み込みます。

<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-router-dom@5.3.4/umd/react-router-dom.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>

3. フロントエンドコード

以下は、Reactフォームのコードです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>React お問い合わせフォーム</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/react-router-dom@5.3.4/umd/react-router-dom.min.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></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="root" class="container mx-auto p-4 max-w-screen-md"></div>
  <script type="text/babel">
    const { BrowserRouter, Route, Switch, useHistory } = window.ReactRouterDOM;
    const { useState } = React;

    function InputForm() {
      const history = useHistory();
      const [formData, setFormData] = useState(JSON.parse(localStorage.getItem("formData")) || {
        name: "",
        email: "",
        message: "",
      });

      const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData((prev) => ({ ...prev, [name]: value }));
      };

      const handleSubmit = (e) => {
        e.preventDefault();
        localStorage.setItem("formData", JSON.stringify(formData));
        history.push("/confirm");
      };

      return (
        <div className="bg-white shadow-md rounded p-6 w-full max-w-md mx-auto">
          <h1 className="text-2xl font-bold mb-4">お問い合わせフォーム</h1>
          <form onSubmit={handleSubmit}>
            <div className="mb-4">
              <label htmlFor="name" className="block text-sm font-medium text-gray-700">名前:</label>
              <input
                type="text"
                id="name"
                name="name"
                value={formData.name}
                onChange={handleChange}
                required
                className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
              />
            </div>
            <div className="mb-4">
              <label htmlFor="email" className="block text-sm font-medium text-gray-700">メールアドレス:</label>
              <input
                type="email"
                id="email"
                name="email"
                value={formData.email}
                onChange={handleChange}
                required
                className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
              />
            </div>
            <div className="mb-4">
              <label htmlFor="message" className="block text-sm font-medium text-gray-700">お問い合わせ内容:</label>
              <textarea
                id="message"
                name="message"
                value={formData.message}
                onChange={handleChange}
                required
                className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
              />
            </div>
            <button type="submit" className="w-full bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">
              確認画面へ
            </button>
          </form>
        </div>
      );
    }

    function ConfirmForm() {
      const history = useHistory();
      const formData = JSON.parse(localStorage.getItem("formData")) || { name: "", email: "", message: "" };

      const handleBack = () => {
        history.push("/");
      };

      const handleSubmit = async () => {
        grecaptcha.ready(async () => {
          const token = await grecaptcha.execute("YOUR_SITE_KEY", { action: "submit" });
          fetch("backend/contact.php", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ ...formData, recaptcha_token: token }),
          })
            .then((res) => res.json())
            .then((data) => {
              if (data.success) {
                localStorage.removeItem("formData");
                history.push("/complete");
              } else {
                alert("送信に失敗しました: " + data.error);
              }
            })
            .catch(() => alert("エラーが発生しました。通信環境を確認してください。"));
        });
      };

      return (
        <div className="bg-white shadow-md rounded p-6 w-full max-w-md mx-auto">
          <h1 className="text-2xl font-bold mb-4">内容の確認</h1>
          <p className="mb-2"><strong>名前:</strong> {formData.name}</p>
          <p className="mb-2"><strong>メールアドレス:</strong> {formData.email}</p>
          <p className="mb-4"><strong>お問い合わせ内容:</strong> {formData.message}</p>
          <div className="flex space-x-4">
            <button onClick={handleBack} className="w-1/2 bg-gray-600 text-white py-2 px-4 rounded hover:bg-gray-700">
              戻る
            </button>
            <button onClick={handleSubmit} className="w-1/2 bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">
              送信する
            </button>
          </div>
        </div>
      );
    }

    function CompleteForm() {
      const history = useHistory();
      return (
        <div className="bg-white shadow-md rounded p-6 w-full max-w-md mx-auto">
          <h1 className="text-2xl font-bold mb-4">送信が完了しました</h1>
          <p>お問い合わせいただきありがとうございます。</p>
          <button
            onClick={() => history.push("/")}
            className="mt-4 w-full bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700"
          >
            フォームに戻る
          </button>
        </div>
      );
    }

    function App() {
      return (
        <BrowserRouter>
          <Switch>
            <Route exact path="/" component={InputForm} />
            <Route path="/confirm" component={ConfirmForm} />
            <Route path="/complete" component={CompleteForm} />
          </Switch>
        </BrowserRouter>
      );
    }

    ReactDOM.render(<App />, document.getElementById("root"));
  </script>
</body>
</html>

4. バックエンドコード

以下は、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----------------------\nお名前: {$data['name']}\nメールアドレス: {$data['email']}\nお問い合わせ内容:\n{$data['message']}\n----------------------\n\n担当者より折り返しご連絡いたします。\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' => 'メール送信に失敗しました。']);
}

5. 注意点

  • YOUR_SITE_KEYYOUR_SECRET_KEY を Google reCAPTCHA で発行したキーに置き換えてください。
  • バックエンドのコードは適切に保護してください(https の利用、サーバーのセキュリティ設定など)。

コメント

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