Blog

Next.js(App Router)のAPI RoutesでReCAPTCHA V3を導入

reCAPTCHAを導入する上で、Next.jsのApp Routerを使ったアプリでの実装方法で色々と試行錯誤したので、その記録です。
サーバー側の処理は、Next.jsのAPI Routesを使っています。

その方法自体は色々紹介されているものがあるのですが、Next13.4からstable(安定)になったApp Routerでアプリを構築している場合は、やり方が異なるところがあります。

というわけでこの記事はApp Routerでの実装方法を書いています。App Routerを使っていない場合はコードが異なるのでお気をつけください。

reCAPTCHAの導入

reCAPTCHAはv3を使用します。

v3は、ユーザーが何もする必要がないので、コンバージョンに影響を与えることなくいつでも実装できるのが売りです。

見た目も四角いバッジが出るだけです。そのバッジもGoogleの規定に従えばテキストに置き換えることが可能で、サイトのデザインを損なわないのもいいところだと思います。
ぱっと見の守られ感はないですが、サイトの背後できっちり仕事をしています。

reCAPTCHAの設定は、コンソール画面から行えます。

https://www.google.com/recaptcha/about/

「v3 Admin Console」から設定画面に飛べるようになっています。
コンソール画面で、対応したいドメインを紐付け、サイトキーシークレットキーを取得します。

Next.jsのApp Routerでの実装

Next.jsは「API Routes」という、Next.js でAPIを構築する方法を提供しています。

pagesフォルダにapiフォルダを作成し、その中にファイルを作成すると、APIルートが作成できます。(App Routerのプロジェクトの場合、フォルダ構成が異なります。後述)

API Routesを使えば、Next.jsのフロントからNext.jsのサーバーへreCAPTCHAのトークンを送信し、そのトークンをreCAPTCHAのサーバーに送信して検証することができます。

JavaScript API を読み込み

設置したいページに、JavaScript API を読み込みます。

<script
  src="https://www.google.com/recaptcha/api.js?render=サイトキー"
  async
></script>

のちにGoogle Analytics4のタグを入れるときにasyncがないとエラーが出ることがあるので、asyncを入れています。
reCAPTCHAもGoogle Analyticsも、本番と同じ環境がないと確認しづらいので、競合すると辛いです。

reCAPTCHAのトークンを取得し、API Routesへ送信

reCAPTCHAトークンの有効期限は2分です。ページ読み込み時にトークンを発行してしまうと、フォーム送信のときには有効期限切れになっている可能性があります。
そこで、submitボタンを押した時に発行するようにします。

reCAPTCHA を実行するタイミングをより細かく制御するには、grecaptcha オブジェクトで execute メソッドを使用します。これを行うには、reCAPTCHA スクリプトの読み込みに render パラメータを追加する必要があります。

https://developers.google.com/recaptcha/docs/v3?hl=ja#programmatically_invoke_the_challenge

executeメソッドを使った書き方は以下のようになりました。

const onSubmit = async (data: FormData) => {
  grecaptcha.ready(function () {
    grecaptcha.execute("サイトキー", { action: "submit" }).then(async (token) => {
      // トークンをサーバーに送信
      const response = await fetch("api/recaptcha", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ token }),
      });
      const res = await response.json();

      //success:falseの場合はreturn
      if (!res.responceJsonRecaptcha.success) {
        console.error("reCAPTCHA: ", res.responceJsonRecaptcha["error-codes"]);
        return;
      }
			
			//successした時の処理

    });
  });
};

typeScriptを使う場合、@types/grecaptchaをインストールしておきます。

grecaptcha.executeの部分でGoogleのサーバーからトークンを非同期で取得しています。
引数にサイトキーとアクションが必要です。
アクションは、この場合はsubmitで発火しているので、submitとしています。(アクション名は任意のアクション名をつけられます。)

API Routesのファイルを作成し、サーバー側の処理を書く

App Routerで実装する場合は、pagesフォルダの下ではなく(pagesがない)、src/app/api/任意の名前のフォルダ/ の配下にroutes.tsを作成します。

recaptcha/route.tsという名前で作成しました。

このAPI Routesのファイルに、「フロントからreCAPTCHAのトークンを受け取って、reCAPTCHAのサーバーに送信して検証 → 検証結果をフロントに返却する」処理を書きます。

Next.js公式 Route Handlers

import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const res = await request.json();
  const serverSecretKey = `secret=${process.env.RECAPTCHA_SERVER_SECRET_KEY}&response=${res.token}`;
  const responce_recaptcha = await fetch("https://www.google.com/recaptcha/api/siteverify", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: serverSecretKey,
  });

  const responceJsonRecaptcha = await responce_recaptcha.json();

  return NextResponse.json({ responceJsonRecaptcha });
}

export async function POST(request: Request) {
のあたりが、App Routerの場合のPOSTの書き方になります。

シークレットキーは環境変数にしておき、.env.localに記載しています。今回本番はvercelなのでそちらにも環境変数を登録しました。

https://www.google.com/recaptcha/api/siteverifyにPOSTで送信すると、reCAPTCHAのサーバーから以下のようなレスポンスが返ってきます。

{
  "success": true|false,      // whether this request was a valid reCAPTCHA token for your site
  "score": number             // the score for this request (0.0 - 1.0)
  "action": string            // the action name for this request (important to verify)
  "challenge_ts": timestamp,  // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
  "hostname": string,         // the hostname of the site where the reCAPTCHA was solved
  "error-codes": [...]        // optional
}
https://developers.google.com/recaptcha/docs/v3?hl=ja#site_verify_response

successがtrueであれば、トークンは正しいものとして扱えます。

そのほかにも、scoreやactionなどの情報が返ってきます。

scoreは0.0から1.0の間で、0.0はbotである可能性が高く、1.0は人間である可能性が高いというものです。
actionは、トークンを発行するときに指定したアクション名が返ってきます。

error-codesは、successがfalseの場合に返ってくるエラーコードです。
例えば、タイムアウトまたは重複の場合は「timeout-or-duplicate」というエラーコードが返ってきます。

ちなみに検証中、公式にあるエラーコードの他に、'browser-error'というエラーコードも返ってきました。
'browser-error'とは何なのかと思いましたが、こちらも公式にヒントがかいてありました。

BROWSER_ERROR トークンは、reCAPTCHA Enterprise スクリプトが execute オペレーションを実行できなかった場合に発生します。ほとんどの場合、これはクライアント側のネットワーク障害またはタイムアウトが原因です。execute() は、JavaScript で再試行する必要があります。

https://cloud.google.com/recaptcha-enterprise/docs/faq?hl=ja

reCAPTCHA登録の際、対象ドメインにlocalhostを追加していなかったので、localhostの環境でbrowser-errorが発生していました。

おわりに

これで、reCAPTCHAの結果によってフォームを送信しないなどの処理ができるようになりました。
サーバー側の処理はバックエンドに使用している技術で書けばいいので、Next.jsを使っていない場合はその技術に合わせて書けばいいのですが、
今回はFirestoreを使っており、自分的にCloud FunctionでいくよりAPI Routesの方が書きやすかったのでこの方法で実装することにしました。

おすすめの記事 recommend blog

新着 new blog

github