🌀

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

2024/10/19に公開

はじめに

自分は大学の授業レビューサイトを運営しています。
この前、ふとbotに大量にレビューを登録されてしまったらどうしよう!と思いました。大量にレビューは獲得できますが、全く参考になりませんし、サーバーがダウンしてしまいます。
そこでreCAPTCHAを導入することにしました。

今回の環境

  • nextjs 13.4.19
    • App router
  • Rails 7.0.6
    • apiモード

reCAPTCHAとは

普段フォームを送信する際に「私はロボットではありません」というボタンや画像認証を見たことがないでしょうか?まさにそれです。これによってbotかどうか判別することで攻撃から防ぐことができます。

画像認証を行うものがv2で、最近主流になっているv3は、ユーザーの行動を自動で判別するため、ユーザーが何も操作する必要がありません
レビューサイトではレビューのしやすさが重要なので、今回はそのv3を導入していきます。

image.png

image.png

画像の引用元

Google-reCAPTCHAの設定

1 . Google reCAPTCHA管理コンソールにアクセスします

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

2 . 「新しいサイトを登録する」で以下を登録します。

  • ラベル
  • reCAPRCHAタイプ。今回はv3
  • ドメイン
    • ドメインの先頭にhttpsはつけられません
    • ローカルのIPアドレス、127.0.0.1も登録しておきましょう!

3 . サイトキーシークレットキーをメモします。これらは後ほど使用します。

Screenshot 2024-10-18 at 21.25.06.png

フロントエンド(Next.js)の実装

reCAPTCHAスクリプトの読み込み

Nextjs13のApp routerの場合、app/layout.jsまたはapp/layout.tsxにスクリプトを追加します。next/scriptを使用してGoogleのreCAPTCHAスクリプトを読み込みます。

app/layout.jsx
import './globals.css';
import Script from 'next/script';

export const metadata = {
  title: 'My App',
  description: 'Next.js 13 with Rails API',
};

export default function RootLayout({ children }) {
  const SITE_KEY = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;

  return (
    <html lang="ja">
      <head>
        <Script
          src={`https://www.google.com/recaptcha/api.js?render=${SITE_KEY}`}
          strategy="beforeInteractive"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

formページへのreCAPTCHA実装

  • window.grecaptchaが存在しない場合、つまりreCAPTCHAのスクリプトが読み込まれていない場合は、エラーメッセージを表示して処理を終了させるううにしています。
  • window.grecaptcha.executeでtokenを取得できます。
app/lectures/[id]/new/page.tsx
  const Submit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const errors = validateReview(review);
    if (!isEmptyObject(errors)) {
      setFormErrors(errors);
      return;
    }

+    if (!window.grecaptcha) {
+      setFormErrors({ recaptcha: 'reCAPTCHAが読み込まれていません。' +});
+      return;
+    }
+
+    try {
+      const SITE_KEY = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!;
+      const token = await window.grecaptcha.execute(SITE_KEY, { action: 'submit' });
+      addReview({ ...review, rating: value }, token);
+    } catch (error) {
+      setFormErrors({ recaptcha: 'reCAPTCHAの取得に失敗しまし+た。' });
+    }
+  };
  • reviewに加えてtokenをポストするように変更しました。
app/lectures/[id]/new/page.tsx
  const addReview = async (newReview: ReviewData, token: string) => {
    try {
      const res = await axios.post(`${process.env.NEXT_PUBLIC_ENV}/api/v1/lectures/${params.id}/reviews`, {
        review: newReview,
+        token,
      }, {
        headers: {
          'Content-Type': 'application/json',
        },
      });
  • reCAPTCHAのサイトキーを設定します。
  • Next.jsでは.env.localで基本的に定義するようです。
  • サイトキーは漏洩してはいけないので、gitignoreに記載してgitの追跡対象から除外するようにしましょう。
.env.local
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=your_site_key

バックエンド(Rails)の実装

必要なgemを導入します。以下をGemfileに記載してください。

  • httparty: reCAPTCHAのサービスクラスの導入で使用します
  • dotenv-rails: 環境変数を使用するため、必要です。
Gemfile
gem 'httparty'
gem 'dotenv-rails'

RecaptchaVerifierサービスクラスの作成

  • attr_reader :score, :action はインスタンス変数 score と action に対する読み取り専用のメソッドを提供し、外部からこの2つの値にアクセスできるようにします。
  • initializeメソッドで認証に使用するオブジェクトの初期化を行います。
  • verifyでGoogle reCAPTCHA APIにPOSTリクエストを送りスコアを判定します。
app/services/recaptcha_verifier.rb
require 'httparty'

class RecaptchaVerifier
  RECAPTCHA_SECRET_KEY = ENV['RECAPTCHA_SECRET_KEY']

  attr_reader :score, :action

  def initialize(token, expected_action = 'submit', minimum_score = 0.5)
    @token = token
    @expected_action = expected_action
    @minimum_score = minimum_score
    @score = 0.0
    @action = ''
  end

  def verify
    response = HTTParty.post(
      "https://www.google.com/recaptcha/api/siteverify",
      body: {
        secret: RECAPTCHA_SECRET_KEY,
        response: @token
      }
    )

    result = JSON.parse(response.body)

    if result['success'] && result['action'] == @expected_action && result['score'] >= @minimum_score
      @score = result['score']
      @action = result['action']
      true
    else
      false
    end
  rescue StandardError => e
    false
  end
end

ReviewsControllerの実装

  • RecaptchaVerifier クラスのインスタンスを作成し、reCAPTCHAトークンの検証をします。
app/controllers/api/v1/reviews_controller.rb
  def create
    permitted = params.permit(:token, review: [
      :rating, :content, :period_year, :period_term, :textbook, :attendance,
      :grading_type, :content_difficulty, :content_quality
    ])

    token = permitted[:token]
    review_attributes = permitted[:review]

    verifier = RecaptchaVerifier.new(token, 'submit', 0.5)

    if verifier.verify
      create_review(review_attributes)
    else
      render json: { success: false, message: 'reCAPTCHA認証に失敗しました。' }, status: :unprocessable_entity
    end
  end

envファイルを作成

  • こちらもgitignoreに記載しましょう。
.env
RECAPTCHA_SECRET_KEY=your_secret_key

おわりに

Next.js、Railsの環境でreCAPTCHAを導入するという記事が少なかったため投稿しました。少しでもお力になれれば幸いです。

参考URL

https://zenn.dev/angelecho/articles/daeb265bb3bf4b

https://pikawaka.com/rails/recaptcha

Discussion