スパム対策に有効なgoogle reCAPTCHA v3をReactへの導入した際、少しハマったので記事にまとめます。
フロントエンドはタイトルどおりReact、バックエンドはruby on rails(apiモード)です。
フロントエンドとバックエンドでそれぞれどのような動きをするかというと、
という流れです。
Qiitaに分かりやすく図解してくださっている方がいます。
まずは、recaptcha/adminにアクセスして、APIキーを発行します。
わかりやすい任意のラベル名を入力します。
reCAPTCHA v3を選択します。
ちなみに、この2つの違いはというと、v2はユーザーに操作をさせる認証方法で、v3はユーザー操作は不要な認証方法となります。
これから導入するのであれば、v3が推奨されているみたいです。
設置するサーバのドメインを入力します。
また、開発環境(ローカル環境)も合わせて登録するように紹介されている記事がありますが、本番環境と開発環境を一緒にすることは、セキュリティー上、推奨されていないようなので、別々に登録しましょう。
ローカル環境を登録するのであれば、「localhost」又は「127.0.0.1」となります。
APIキーが発行されます。
APIキーは、サイトキーとシークレットキーの2つが発行されます。
シークレットキーは秘密鍵なので、取り扱いには注意してください。
フロントエンドの設定では、以下の作業をおこないます。
react-google-recaptcha-v3
をインストールGoogleReCaptchaProvider
コンポーネントで囲うuseGoogleReCaptcha
を利用してgoogleのサーバからトークンを取得する.env
に以下を追加する。
REACT_APP_RECAPTCHA_KEY=[サイトキー]
create-react-appで作成したプロジェクトであれば、プロジェクト直下に.envファイルを作成し、prefixにREACT_APP_
を付けるだけで.envから変数を読み込むことができます。
同様に、本番環境の環境変数にもサイトキーを設定します。
react-google-recaptcha-v3
をインストール以下のコマンドでライブラリをインストールする。
yarn add react-google-recaptcha-v3
react-google-recaptcha-v3のUsageを確認すると、以下の3つの方法があると紹介されています。
公式では、2番目のuseGoogleReCaptchaが推奨されていますし、一番、応用が効くと思いますので、その方法で説明します。
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')
);
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;
postメソッドのリクエストボディに、3-4.で取得したトークンを付加します。 以下では、axiosのpostメソッドのリクエストボディににトークンを付加する例です。
axios.post(`${process.env.REACT_APP_API_ENDPOINT}/contacts`,{
name: name,
email: email,
message: message,
recaptcha_token: token // 追加
})
以上で、フロントエンドでの設定は完了です。
バックエンドの設定では、以下の作業をおこないます。
recaptcha
をインストールconfig/initializers/recaptcha.rb
を作成verify_recaptcha?
を挿入発行したシークレットキーを環境変数に設定します。
railsで環境変数を使う方法はいくつかありますが、ここではgem dotenv
を利用していますので、.env
に以下を追加します。
RECAPTCHA_SECRET_KEY=[シークレットキー]
シークレットキーは秘密鍵なので、誤って、.env
ファイルをパブリックリポジトリにプッシュしないように気をつけてください。
recaptcha
をインストールGemfile
に以下を追加して、bundle install
を実行します。
gem 'recaptcha', require: "recaptcha/rails"
recaptcha.rb
を作成以下のコマンドで、必要な設定ファイルを生成します。
touch config/initializers/recaptcha.rb
生成したら、環境変数からシークレットキーを読み込むため、以下のように記載してください。
Recaptcha.configure do |config|
config.secret_key = ENV['RECAPTCHA_SECRET_KEY']
end
共通メソッドととしたいので、application_controller.rb
にverify_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
を返します。
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の導入は完了です。