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のサーバーに送信して検証 → 検証結果をフロントに返却する」処理を書きます。
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のサーバーから以下のようなレスポンスが返ってきます。
https://developers.google.com/recaptcha/docs/v3?hl=ja#site_verify_response{ "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 }
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の方が書きやすかったのでこの方法で実装することにしました。