カルキチブログ

Next.jsで作った技術ブログにreCAPTCHAを導入した時の話

カルキチブログにお問い合わせフォームを実装したとき、スパム対策のためにreCAPTCHAを導入したので、その時のことについて記事を書いてみようと思いました。

reCAPTCHAとは?

reCAPTCHAとは、Googleが無料で提供している不正なボットからのアクセスを制限することができるセキュリティ対策のためのサービスです。

不正なボットによるスパムメールを減らす目的で、導入されるケースが一番多いと思います。

以下公式サイトの説明文の引用です。

reCAPTCHA uses an advanced risk analysis engine and adaptive challenges to keep malicious software from engaging in abusive activities on your website. Meanwhile, legitimate users will be able to login, make purchases, view pages, or create accounts and fake users will be blocked.

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

v1(既に終了済み)〜v3までのバージョンがあるのですが、v3の場合は、v2の画像を使用した方法ではなく、reCAPTCHAがあるページにアクセスしたユーザーの行動をスコア化することで、ボットかどうかの判定を行うそうです。

reCAPTCHAについての説明はこちらのサイトが非常に分かりやすかったです。

https://www.synergy-marketing.co.jp/blog/using_recaptcha_on_form

Next.jsにreCAPTCHAを導入する方法

今回はお問い合わせフォームを設置する際のセキュリティ対策を行うために、reCAPTCHAを導入したのですが、ReactやVueを用いたサイトやアプリケーションにreCAPTCHAを導入するには、フロントでトークンを発行する処理と、発行したトークンをバックエンド側で受け取りチェックを行う処理を実装する必要がありそうです。

実装した所感ですが、簡単ではないけど、一度やってしまえば、そこまで難しくはないという感じのレベル感のように感じました。

話が脱線しますが、WordPressのお問い合わせにreCAPTCHAを導入する際は、Contact Form 7などのお問い合わせを表示するためのプラグインを使えばかなり簡単に実装できます。

→WPの管理画面上でreCAPTCHAのトークンの設定をするだけでいけた気がします。

前準備① reCAPTCHAの設定を行う

まずは、reCAPTCHAを使うための設定が必要になります。

下記のURLにアクセスを行います。

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

赤枠で囲ったv3 Admin Consoleというリンクをクリックすると、サイト・アプリケーションの登録画面が表示されるので、必要な情報の登録を行います。

特筆すべきことは特にありませんが、ドメインの部分は本番環境以外の環境(テスト環境やローカル環境など)でもreCAPTCHAの実行を行いたい場合は、複数のドメインを登録する必要があるので、複数登録します。

登録が完了すると、サイトキーとシークレットキーが発行されます。

サイトキーがフロント側で、シークレットキーがバックエンド側で使用するreCAPTCHA のキーになります。

前準備② 必要なライブラリのインストールを行う

まず、Next.jsで構築したアプリケーションやサイトにreCAPTCHAを導入するためのライブラリの導入を行います。

今回は、reCAPTCHA v3(画像認証が完全になくなったバージョンのもの)を導入したいので、react-google-recaptcha-v3というライブラリを使用します。

npm install react-google-recaptcha-v3

Vue・Nuxtの場合は、vue-recaptcha-v3というライブラリを使えばいけそうです。

ここまで終われば前準備は完了です。

フロント側の処理

では、フロント側の処理について書いていきます。

今回はNext.jsで構築したカルキチブログのコードを一部引用する形で書いていこうと思います。

※実際にこのブログに実装したお問い合わせフォームでは、入力された値のバリデーションチェックや送信ボタンの連打を防ぐための処理、state変更に伴う再レンダリングの防止(useRefを使用)など、色々やっています。

まずは、_app.tsx(ない場合は作成してください)に記述されているサイト・アプリケーション全体の要素をGoogleReCaptchaProviderでラップします。

process.env.RECAPTCHA_KEYでは、フロント側のreCAPTCHAキーを環境変数として読み込むようにしています。

_app.tsx

import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
import { AppProps } from 'next/app';

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <GoogleReCaptchaProvider reCaptchaKey={process.env.RECAPTCHA_KEY} language="ja">
      <Component {...pageProps} />
    </GoogleReCaptchaProvider>
  );
}

export default MyApp

reCAPTCHAによるチェックの実施方法ですが、以下の関数を呼び出すことで、フロント側でreCAPTCHAトークンを発行することができます。

const onSubmit = async () => {
  const reCaptchaToken = await executeRecaptcha('contactPage');
}

console.log(reCaptchaToken)で値を出力すると、トークン(英数字が混ざった500文字くらいの文字列)が発行されているはずです。

→発行されたトークンの有効期間は2分間となっているので、トークンが発行されてから2分以内にreCaptchaによるチェックを行う必要があります。

APIに対してPOSTリクエストを送る際には、発行されたトークンをメールの送信処理を行うバックエンド側に対して送信します。

→実際の処理では、お問い合わせフォームの内容も一緒にバックエンドに送っています。

const onSubmit = async () => {
  const reCaptchaToken = await executeRecaptcha('contactPage');

  const apiEndPoint = 'APIのエンドポイント';
  
  await fetch(apiEndPoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      token: reCaptchaToken,
    }),
  });
}

以上でフロント側の処理は完了です。

バックエンド側の処理

続いて、バックエンド側の処理について書いていきます。

カルキチブログでは、Serverless Framework(AWS Lambda・AWS API Gateway・AWS SESを使用)を使用してブログから送信されたお問い合わせ内容を指定したメールアドレスに送信する処理を実装しています。

機能自体は、Next.jsのAPI Routesやデプロイ先のNetlifyが提供しているNetlify Functionsでも実装は可能でしたが、5月からの転職先でServerlessを使う頻度が高くなりそうなので、触り慣れておくためにもServerless Frameworkを採用しました。

バックエンド側では、https://www.google.com/recaptcha/api/siteverifyに対してPOSTリクエストを行うことで、reCAPTCHAによるチェックを行うことができます。

import fetch from 'node-fetch';

const sendMail = async (
  event, 
  context, 
  callback
) => {
  // リクエストボディの内容を取得
  //  実際の処理ではお問い合わせ内容(名前やメールアドレス、お問い合わせ内容など)も取得しています
  const { token } = JSON.parse(event.body);
  
  // serverlessの場合は以下の記述でオリジンを取得可能
  const origin = event.headers.origin;
  
  // ホスト名のみを取得する
  const hostname = origin.replace(/https:\/\//, '');
  
  // reCAPTCHAによるチェックの実施
  const recaptchaRes = await fetch('https://www.google.com/recaptcha/api/siteverify', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: `secret=${process.env.RECAPTCHA_KEY}&response=${token}`,
  });
  
  // チェック結果が代入される
  const recaptchaResult = await recaptchaRes.json();
  
  if (!recaptchaResult.success && recaptchaResult.hostname !== hostname) {
    // reCAPTCHAによるチェックが失敗した際の処理を記述する
  }
}

公式ドキュメントで情報を見つけることはできなかったのですが、reCAPTCHAによるチェックを行うエンドポイントにリクエストを送る際のcontent-typeは、application/x-www-form-urlencodedにする必要があるそうです。

application/x-www-form-urlencoded: キーと値は、その間に '=' がある形でキーと値の組になり、 '&' で区切られてエンコードされます。キーや値の英数字以外の文字は、パーセントエンコーディングされます。このため、このタイプはバイナリデータを扱うのには向きません (代わりに multipart/form-data を使用してください)

https://developer.mozilla.org/ja/docs/Web/HTTP/Methods/POST

reCAPTCHAによるチェック結果は、recaptchaResultに代入されます。

console.log(recaptchaResult)でチェック結果を出力すると以下のようなオブジェクトが返却されます。

{
  success: true,
  challenge_ts: '2021-04-04T11:50:24Z',
  hostname: 'localhost',
  score: 0.9,
  action: 'contactPage'
}

successプロパティの値がtrueの時は、発行されたトークンが有効になっています。

流石にトークンのチェックだけではセキュリティ対策としては心許なさすぎるので、カルキチブログでは、hostnameによる判定(ブラウザ上でリクエストが実行されたかチェックする)も行っています。

よりチェックの精度を上げたい場合は、scoreによるチェックを行ってもいいかもしれません。

if (!recaptchaResult.success || recaptchaResult.hostname !== hostname || recaptchaResult.score < 0.5) {
  // reCAPTCHAによるチェックが失敗した際の処理を記述する
}

reCAPTCHA v3のロゴは非表示にしてもいい

割と最近知ったのですが、reCAPTCHA v3のロゴはGoogleが指定している規約を守れば、非表示にしても問題はないそうです。

ページ内の“I'd like to hide the reCAPTCHA badge. What is allowed”という見出しあたりに説明が書いてあります。

https://developers.google.com/recaptcha/docs/faq

reCAPTCHAのロゴを非表示にするための手順は簡単です。

以下の文章をサイト・アプリケーション内の見える位置に表示します。

→このサイトでは、以下の文章を和訳した状態で表示しています。

This site is protected by reCAPTCHA and the Google
    <a href="https://policies.google.com/privacy">Privacy Policy</a> and
    <a href="https://policies.google.com/terms">Terms of Service</a> apply.

文章を見える位置に掲載したら、CSSで非表示にするだけです。

.grecaptcha-badge {
  visibility: hidden;
}

まとめ

ではまとめです。

  • reCAPTCHAを使うと不正なボットからのアクセスを制限することができる
  • React・VueなどのアプリケーションにreCAPTCHAを導入する場合は、フロント・バックエンド両方のチェックが必要
  • reCAPTCHA v3のロゴは、Googleの規約を守れば非表示にしてもいい

結構長くなりましたが、reCAPTCHAを使えば不正なボットによるアクセスや操作を制限することができるので、不正なボットから守ることができます。

不正なボットのアクセスや操作を完全に遮断することは難しいかもしれませんが、入れないよりは入れた方が絶対にいいものだとは思うので、導入する価値はあると思います。

おまけ

Jamstackアーキテクチャのサイトにお問い合わせ機能を実装する際に一番大変だったのは、セキュリティ周りのことを考えることでした。

今回の記事でも軽く触れたのですが、フロント側だと、値の妥当性のチェックや送信ボタンの連打の防止、バックエンド側でも、値の妥当性のチェック(curlなどでリクエストを送信される可能性を考慮)や、CORSの設定(本番とステージング以外のオリジンから送信されたリクエスト以外は403を返すようにしている)、月にAPIを実行できる回数の上限設定など、とにかく考えることが多くて大変でした。