ReactへのreCAPTCHA v3の導入

更新日2022-01-15
投稿日2022-01-15
タグfootlog, React, reCAPTCHA
投稿者 @amount86

スパム対策に有効なgoogle reCAPTCHA v3をReactへの導入した際、少しハマったので記事にまとめます。



フロントエンドはタイトルどおりReact、バックエンドはruby on rails(apiモード)です。



0. はじめに

フロントエンドとバックエンドでそれぞれどのような動きをするかというと、

  1. javascriptでgoogleのサーバに問い合わせて、トークンを発行
  2. javascriptからサーバーにリクエストを送信する時に、1.で発行したトークンを合わせて送信する
  3. トークンを受け取ったサーバーは、トークンを使ってgoogleのサーバーに問い合わせをする
  4. レスポンスとしてスコア(0〜1.0)を受け取り、サーバはスコアの値に応じてjavascriptからリクエストを実行するか判断する

という流れです。


Qiitaに分かりやすく図解してくださっている方がいます。

reCAPTCHAをシステム導入するときに見るイメージ図





1. google reCAPTCHAのAPIキーを発行

まずは、recaptcha/adminにアクセスして、APIキーを発行します。

recaptcha/admin


ラベル

わかりやすい任意のラベル名を入力します。


reCAPTCHA タイプ

reCAPTCHA v3を選択します。

ちなみに、この2つの違いはというと、v2はユーザーに操作をさせる認証方法で、v3はユーザー操作は不要な認証方法となります。

これから導入するのであれば、v3が推奨されているみたいです。


ドメイン

設置するサーバのドメインを入力します。

また、開発環境(ローカル環境)も合わせて登録するように紹介されている記事がありますが、本番環境と開発環境を一緒にすることは、セキュリティー上、推奨されていないようなので、別々に登録しましょう。

ローカル環境を登録するのであれば、「localhost」又は「127.0.0.1」となります。


送信すると・・・

APIキーが発行されます。

APIキーは、サイトキーとシークレットキーの2つが発行されます。

  • サイトキー:フロントエンドで使用
  • シークレットキー:バックエンドで使用

シークレットキーは秘密鍵なので、取り扱いには注意してください。





2. フロントエンド(React)の設定

フロントエンドの設定では、以下の作業をおこないます。

  1. 発行したサイトキーを環境変数に設定
  2. ライブラリreact-google-recaptcha-v3をインストール
  3. 最上位タグをGoogleReCaptchaProviderコンポーネントで囲う
  4. useGoogleReCaptchaを利用してgoogleのサーバからトークンを取得する
  5. バックエンドへのリクエストにトークンを付加する



2-1. 発行したサイトキーを環境変数に設定

.envに以下を追加する。

REACT_APP_RECAPTCHA_KEY=[サイトキー]

create-react-appで作成したプロジェクトであれば、プロジェクト直下に.envファイルを作成し、prefixにREACT_APP_を付けるだけで.envから変数を読み込むことができます。

同様に、本番環境の環境変数にもサイトキーを設定します。




2-2. ライブラリreact-google-recaptcha-v3をインストール

以下のコマンドでライブラリをインストールする。

yarn add react-google-recaptcha-v3

react-google-recaptcha-v3のUsageを確認すると、以下の3つの方法があると紹介されています。

  • GoogleReCaptcha
  • useGoogleReCaptcha
  • withGoogleReCaptcha

公式では、2番目のuseGoogleReCaptchaが推奨されていますし、一番、応用が効くと思いますので、その方法で説明します。




2-3. 最上位タグをGoogleReCaptchaProviderコンポーネントで囲う

最上位タグが記載されているファイル(index.js)を以下のように修正します。

import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; // 追加

ReactDOM.render(
  <React.StrictMode>
    <GoogleReCaptchaProvider reCaptchaKey={process.env.REACT_APP_RECAPTCHA_KEY} language="ja"> {/* ↓追加 ここから↓ */}
      <App />
    </GoogleReCaptchaProvider> {/* ↑追加 ここまで↑ */}
  </React.StrictMode>,
  document.getElementById('root')
);



2-4. useGoogleReCaptchaを利用してgoogleのサーバからトークンを取得する

reCAPTCHAを利用するコンポーネントを以下のとおり修正します。

import React, { useCallback } from 'react';
import Layout from '../components/Layout';
import { useHistory } from 'react-router-dom';
import axios from 'axios'
import { useState, useEffect } from 'react';
import { Container, Button, Form } from 'react-bootstrap';
import { GoogleReCaptchaProvider, useGoogleReCaptcha } from 'react-google-recaptcha-v3'; // 追加

const Contact = () => {

  const [message,setMessage] = useState('');
  const [name,setName] = useState('');
  const [email,setEmail] = useState('');
  const [isSubmitDisable, setIsSubmitDisable] = useState(true);
  const [sendButtonLabel, setSendButtonLabel] = useState('送信する');
  const history = useHistory();

  // ↓ 追加 ここから ↓
  const [token, setToken] = useState('');

  const { executeRecaptcha } = useGoogleReCaptcha();
  // ↑ 追加 ここまで ↑

  useEffect(() => {
    name && email && message ? setIsSubmitDisable(false) : setIsSubmitDisable(true);
  }, [name, email, message])

  const handleSubmit = (e) =>{
    e.preventDefault();
  }

  const postContact = async () => {

    // ↓ 追加 ここから ↓
    if (!executeRecaptcha) {
      console.log('Execute recaptcha not yet available');
      return;
    }

    const reCaptchaToken = await executeRecaptcha('contact');
    setToken(reCaptchaToken);
    // ↑ 追加 ここまで ↑

    setIsSubmitDisable(true);
    setSendButtonLabel('送信中...');
    axios.post(`${process.env.REACT_APP_API_ENDPOINT}/contacts`,{
      name: name,
      email: email,
      message: message,
      recaptcha_token: token  // 追加
    }).then(res => {
      if(res.status === 204){
        history.push('/top');
        console.log('204');
      } else if(res.status === 500){
        console.log('500');
        setIsSubmitDisable(false);
        setSendButtonLabel('送信する');
      }
    })
    .catch(error => {
      console.log(error);
      setIsSubmitDisable(false);
      setSendButtonLabel('送信する');
    })
  }
  return (
    <Layout>
      <Container>
        <Form onSubmit={handleSubmit} className="my-3">
          <Form.Group>
            <Form.Label>お名前</Form.Label>
            <Form.Control value={name} onChange={(e) => setName(e.target.value)} />
            </Form.Group>
            <Form.Group>
            <Form.Label>メールアドレス</Form.Label>
            <Form.Control value={email} onChange={(e) => setEmail(e.target.value)} />
          </Form.Group>
          <Form.Group className="mb-3" controlId="formBiography">
            <Form.Label>お問い合わせフォーム</Form.Label>
            <Form.Control as="textarea" value={message} onChange={(e) => setMessage(e.target.value)} style={{ height: '100px' }} />
          </Form.Group>
          <div className="mb-3 text-muted">
            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.
          </div>
          <Form.Group className="mb-3 text-end">
            <Button variant="dark" type="submit" onClick={postContact} disabled={isSubmitDisable}>
              {sendButtonLabel}
            </Button>
          </Form.Group>
        </Form>
      </Container>
    </Layout>
  )
}
export default Contact;



2-5. バックエンドへのリクエストにトークンを付加する

postメソッドのリクエストボディに、3-4.で取得したトークンを付加します。 以下では、axiosのpostメソッドのリクエストボディににトークンを付加する例です。

axios.post(`${process.env.REACT_APP_API_ENDPOINT}/contacts`,{
  name: name,
  email: email,
  message: message,
  recaptcha_token: token  // 追加
})

以上で、フロントエンドでの設定は完了です。





3 バックエンド(ruby on rails)の設定

バックエンドの設定では、以下の作業をおこないます。

  1. 発行したシークレットキーを環境変数に設定
  2. gem recaptchaをインストール
  3. config/initializers/recaptcha.rbを作成
  4. googleのサーバからスコアを取得して、判定をおこなう関数を定義
  5. verify_recaptcha?を挿入



3-1. 発行したシークレットキーを環境変数に設定

発行したシークレットキーを環境変数に設定します。 railsで環境変数を使う方法はいくつかありますが、ここではgem dotenvを利用していますので、.envに以下を追加します。

RECAPTCHA_SECRET_KEY=[シークレットキー]

シークレットキーは秘密鍵なので、誤って、.envファイルをパブリックリポジトリにプッシュしないように気をつけてください。




3-2. gem recaptchaをインストール

Gemfileに以下を追加して、bundle installを実行します。

gem 'recaptcha', require: "recaptcha/rails"



3-3. recaptcha.rbを作成

以下のコマンドで、必要な設定ファイルを生成します。

touch config/initializers/recaptcha.rb

生成したら、環境変数からシークレットキーを読み込むため、以下のように記載してください。

Recaptcha.configure do |config|
  config.secret_key = ENV['RECAPTCHA_SECRET_KEY']
end



3-4. googleのサーバからスコアを取得して、判定をおこなう関数を定義

共通メソッドととしたいので、application_controller.rbverify_recaptcha?を追加します。

また、スコア RECAPTCHA_MINIMUM_SCOREは、0.5とします。

class ApplicationController < ActionController::API

  include DeviseTokenAuth::Concerns::SetUserByToken

  RECAPTCHA_MINIMUM_SCORE = 0.5
  
  def verify_recaptcha?(token,recaptcha_action)
    secret_key = ENV['RECAPTCHA_SECRET_KEY']
    uri = URI.parse("https://www.google.com/recaptcha/api/siteverify?secret=#{secret_key}&response=#{token}") 
    response = Net::HTTP.get_response(uri)
    json = JSON.parse(response.body)
    json['success'] && json['score'] > RECAPTCHA_MINIMUM_SCORE && json['action'] == recaptcha_action
  end

end

共通メソッドverify_recaptcha?は、googleのサーバからスコアを取得して、定数RECAPTCHA_MINIMUM_SCOREより大きければtrue、小さければfalseを返します。

今回は、定数RECAPTCHA_MINIMUM_SCOREを0.5としていますので、0.5より大きい値が返ってくればtrueを返します。




3-5. verify_recaptcha?を挿入

4-4.で作成したverify_recaptcha?をボットか否かを判定した上で実行したい処理の前に挿入します。

class V1::ContactsController < ApplicationController

  # 省略・・・

  def create

    # ↓ 追加 ここから ↓
    unless verify_recaptcha?(params[:recaptcha_token], 'contact')
      render json: {message: '不正なアクセスを検知しました。'}, status: 502
    end
    # ↑ 追加 ここまで ↑

    Contact.create(
      name: params[:name],
      email: params[:email],
      message: params[:message]
    )
  end

end




以上でgoogle reCAPTCHAの導入は完了です。