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

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

## 今回の環境
* nextjs 13.4.19
    * App router
* Rails 7.0.6
    * apiモード

## 目次

1. [reCAPTCHAとは](#reCAPTCHAとは)
2. [Google-reCAPTCHAの設定](#google-recaptchaの設定)
3. [フロントエンド（Next.js）の実装](#フロントエンドnextjsの実装)
4. [バックエンド（Rails）の実装](#バックエンドrailsの実装)
5. [デプロイ後の注意](#デプロイ後の注意)
6. [おわりに](#おわりに)
7. [参考URL](#参考URL)

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1922066/3cdfb1ea-aea4-0f02-68ee-f2b2a2821637.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1922066/3a7ee8af-73b1-ebf9-0275-202ebda71250.png)

[画像の引用元](https://www.google.com/url?sa=i&url=https%3A%2F%2Fkabufuji.com%2Fi-do-not-robot%2F&psig=AOvVaw02GbCJ5f9uqz66H_ys-gCl&ust=1729339875759000&source=images&cd=vfe&opi=89978449&ved=0CBQQjRxqFwoTCJCSr87zl4kDFQAAAAAdAAAAABAK)


## 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1922066/12502b1e-1229-228d-c903-153fc406830d.png)

## フロントエンド（Next.js）の実装

### reCAPTCHAスクリプトの読み込み

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

```tsx: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を取得できます。

```diff_tsx: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をポストするように変更しました。
```diff_tsx: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:.env.local
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=your_site_key
```

# バックエンド（Rails）の実装
必要なgemを導入します。以下をGemfileに記載してください。
* httparty: reCAPTCHAのサービスクラスの導入で使用します
* dotenv-rails: 環境変数を使用するため、必要です。
```ruby:Gemfile
gem 'httparty'
gem 'dotenv-rails'
```

# RecaptchaVerifierサービスクラスの作成
* attr_reader :score, :action はインスタンス変数 score と action に対する読み取り専用のメソッドを提供し、外部からこの2つの値にアクセスできるようにします。
* initializeメソッドで認証に使用するオブジェクトの初期化を行います。
* verifyでGoogle reCAPTCHA APIにPOSTリクエストを送りスコアを判定します。


```ruby: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トークンの検証をします。


```ruby: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:.env
RECAPTCHA_SECRET_KEY=your_secret_key
```

:::note warn
デプロイ後の注意

環境変数を管理サイトで設定しましょう！
ローカルで動いていてもreCAPTCHのロゴが表示されない方はまさに環境変数の設定が原因だと思います。
自分はvercel、Herokuでデプロイをしていましたがどちらも管理サイトで環境変数（.env、.env.local）を設定する必要がありました。
:::

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

# 参考URL
https://zenn.dev/angelecho/articles/daeb265bb3bf4b

https://pikawaka.com/rails/recaptcha