トミケルの本音

日々の仕事や趣味で感じたことを綴るブログ

Next.jsでreCapture V3を導入!

Next.js(App Router)で Google reCAPTCHA v3 を導入する方法

🎯 目的

フォーム送信時に、ボットによるスパムを防ぐために Google reCAPTCHA v3 を導入します。
Next.js の App Router 環境に対応した、クライアント〜サーバー検証までの構成をわかりやすくまとめます。

reCapture V3 の仕組み

  1. FormのSubmit時にフロントサイドのscriptでtoken(唯一の)を発行
  2. フォームの処理を行う時に一緒にtokenをバックエンドに送り、バックエンドで検査
    • バックエンドでgoogle APIにtokenとSecret Keyを送る
  3. 検査した結果(0 ~ 1の値)をバックエンドに返す
    • 開発者自身が定めた値以上であればバックエンドの処理(その他のフォームデータの処理)を進めるようにコーディング

✅ ステップ 1:Google reCAPTCHA v3 の設定

まずは Google の reCAPTCHA 管理ページ にアクセスし、以下を設定します:

  • タイプ:「reCAPTCHA v3」
  • 動作させるドメインの設定(複数設定できます!)
    • 開発環境:localhost
    • 本番環境:your-domain.com

発行される以下のキーを控えます:

  • サイトキー(クライアント用)
  • シークレットキー(サーバー検証用)

✅ ステップ 2:環境変数の設定

.env.local に以下を追加:

NEXT_PUBLIC_RECAPTCHA_SITE_KEY=サイトキー
RECAPTCHA_SECRET_KEY=シークレットキー
JavaScript

注意:NEXT_PUBLIC_ を付けることで、クライアント側でも読み込めるようになります。
サイトキーはクライアント側で使うため、必ずNEXT_PUBLICを使う


✅ ステップ 3:<Script> タグで reCAPTCHA を読み込む

app/layout.tsx 内で reCAPTCHA のスクリプトを読み込む必要があります。
このステップを忘れると 、ステップ4で使用するgrecaptcha が未定義になります。

なお、strategy=”beforeInteractive” は、「表示するpage.tsxのJSを実行する前に実行させる」という意味。(ただし、page.tsxのJS実行時に、Scriptタグの実行完了は保証しない)

import Script from "next/script";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ja">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <Script
          strategy="beforeInteractive"
          src={`https://www.google.com/recaptcha/api.js?      
          render=${process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}`}
        />
        <Header />
        <main className="min-h-screen --foreground p-6">{children}</main>
        <Analytics />
        <Footer />
      </body>
    </html>
  );
}
Layout.tsx

✅ ステップ 4:クライアントでトークンを取得する

grecaptcha.execute() を使って、トークンを取得します。
取得したトークンを FormData に追加して、サーバーへ送信します。

  • excecuteの引数
    • 第一引数はsite key
    • 第二引数はオブジェクト
      {action: “ユーザーのアクション(submit, checkout, loginなど任意の文字列)”}
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import { useForm } from "react-hook-form";
import { validationSchema } from "../utils/validationSchema";
import { sendEmail } from "../contact/actions";
import { captureRecaptchaToken } from "../utils/capture";

interface FormData {
  name: string;
  email: string;
  contents: string;
}

const ContactForm = () => {
  ...

  const onSubmit = async (data: FormData) => {
    setIsSending(true);
    setIsSuccess(false);
    setIsError(false);

    const token = await captureRecaptchaToken(); //token出力の関数 *別ファイルに記載

    const formData = new FormData();
    formData.append("name", data.name);
    formData.append("email", data.email);
    formData.append("contents", data.contents);
    formData.append("g-recaptcha-response", token as string); //append the token to formData

    const result = await sendEmail(formData); //send formData with token to the backend.
    
    setIsSending(false);
    if (result.success) {
      setIsSuccess(true);
      setTimeout(() => {
        setIsSuccess(false);
      }, 3000);
    } else {
      setIsError(true);
      console.error("送信エラー:", JSON.stringify(result.errors, null, 2));
      setTimeout(() => {
        setIsError(false);
      }, 3000);
    }
  };
  return (
    <form
      onSubmit={handleSubmit(onSubmit)}
    ...
JavaScript
//戻り値はPromise型 → 呼び出し時はawaitかthenが必要
export async function captureRecaptchaToken(): Promise<string | null> {
  return new Promise((resolve) => {
    if (typeof window.grecaptcha ==="undefined") {
      console.error("reCAPTCHA がロードされていません");
      resolve(null);
      return;
    }

    window.grecaptcha.ready(async () => {
      const siteKey = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
      if (!siteKey) {
        console.error("reCAPTCHA site key が設定されていません");
        resolve(null);
        return;
      }

      try {
        const token = await window.grecaptcha.execute(siteKey, {
          action: "submit",
        }); //ここでtoken発行
        resolve(token);
      } catch (error) {
        console.error("reCAPTCHA 実行エラー:", error);
        resolve(null);
      }
    });
  });
}
JavaScript

reCAPTCHA の execute() を使うには、まず grecaptcha.ready() で準備ができるのを待つ必要がある。
ただし、ready() は非同期関数ではなく Promise を返さないため、
await で待つことはできない。
→ そのため、new Promise を使って「待てる形」にラップしてあげる必要がある。


✅ ステップ 5:サーバーでトークンを検証する

取得したトークンをサーバー側で Google に送信し、以下を確認します:

//バックエンドに(フォームデータと一緒に)トークンを送信
const result = await sendEmail(formData); 
JavaScript
//バックエンドでの受け取りと処理
export async function sendEmail(formData: FormData) {
  const token = formData.get("g-recaptcha-response")?.toString();
  const recaptchaSecretKey = process.env.RECAPTURE_SECRET_KEY;
  if (!token || !recaptchaSecretKey) {
    return {
      success: false,
      errors: {
        message: "reCAPTCHAのトークンが取得できませんでした。",
      },
    };
  }

  const url = new URL("https://www.google.com/recaptcha/api/siteverify");

  //apiにシークレットキーとtokenを送信
  const recaptchaRes = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      secret: recaptchaSecretKey,
      response: token,
    }),
  });

  const recaptchaData = await recaptchaRes.json(); //検証結果をjson文字列で受け取り
JavaScript

バックエンドではtokenとシークレットキーをAPIに送付して、検証させ結果を受け取る

結果のjson

{
  success: true | false,
  score: 0.0 ~ 1.0,
  action: submit | login | checkout | ...
}
JavaScript
  • success: true
  • score(0.0〜1.0)がスパム判定しきい値以上か
  • action(オプション)

スコアのしきい値は 0.5〜0.7 くらいがおすすめです。


✅ ステップ 6:スコアが低い場合は処理を中断

スコアが低い、success: falsebrowser-error などが返ってきた場合、
「スパム判定」としてメール送信などの処理をブロックします。

  //successプロパティがfalse, もしくはscoreが0.4以下はスパムと判定し、フロントエンドにエラーメッセージなどを返す
  if (!recaptchaData.success || recaptchaData.score < 0.4) {
    return { success: false, errors: ["スパム検出されました"] };
  }
  
  ... 問題がない場合の処理 ...
JavaScript

✅ ステップ 7:フォーム送信処理(メール送信やDB保存など)

フォーム内容が妥当か(Zodなどでバリデーション)チェックした上で、メール送信や保存などを行います。


✅ よくあるエラーと対処

エラーコード原因対処
browser-errorトークンの送信形式ミス、スクリプト未ロード、ドメイン未登録などPOST形式の見直し・ドメイン確認・grecaptchaロード確認
score < 0.5Googleが「怪しい」と判断したスパム扱いで弾くか、しきい値を調整
success: falseトークン無効・期限切れなどトークン再取得・短時間で送信する

✅ まとめ

Next.js App Router での reCAPTCHA v3 導入は以下が重要です:

  1. Googleでの登録とキーの取得
  2. <Script> タグによるスクリプト読み込み
  3. クライアントでのトークン取得と送信
  4. サーバーでの正しい POST 検証(Content-Type + URLSearchParams)
  5. スコアに基づいた判定処理

セキュリティを高めつつ、ユーザー体験を損なわないように活用しましょう。