Google reCAPTCHA v3でスパムbot対策をする

4 min読了の目安(約4200字TECH技術記事

はじめに

投稿型Webサービスにおいて頭を悩まされることの一つがbotからのスパム投稿対策です。
そのようなbotからの投稿を防ぐ仕組みをCAPTCHAといいます。CAPTCHAをサービスとして提供しているものにreCAPTCHAがあります。reCAPTCHAはGoogleが提供するCAPTCHAサービスです。

reCAPTCHAとは

公式URL: https://www.google.com/recaptcha/about/

reCAPTCHAはGoogleが提供するCAPTCHAサービスです。v2、v3、Enterpriseの3つのバージョンがあります。このうちv2は「私はロボットではありません」のチェックボックスをユーザーにクリックしてもらうもので、どこかで見覚えがあるのではないでしょうか。
v3とEnterpriseは(おそらく)同じ中身で、ユーザーからの明示的なアクションを必要としないものです。導入前後でユーザー体験をほとんど変化させないため、導入ハードルが低いです。本記事ではこのreCAPTCHA v3を利用する方法について記述していきます。

サイトを登録してキーを発行する

reCAPTCHA admin: https://www.google.com/recaptcha/admin

実装を開始する前に、reCAPTCHAのadminでサイトを登録しサイトキー、シークレットキーの二つを発行する必要があります。
ラベルには自分がわかりやすいような名前をつけ、ドメインにはreCAPTCHA v3を利用したいサイトのドメインを追加してください。なお、localhostで検証する場合にはlocalhostも追加する必要があります。

reCAPTCHA v3導入実装の大まかな流れ

サイト登録とキー発行が済んだら、実装に移ります。
reCAPTCHA v3導入の大まかな流れは以下の2ステップです。

  1. クライアントでreCAPTCHA v3のJavaScriptを実行し、トークンを発行する
  2. 発行されたトークンをサーバーサイドで受け取り、Google reCAPTCHAの認証サーバーに問い合わせてトークンを検証し、successしていなければ(botと判断されれば)投稿を弾く

上記のとおり、クライアントとサーバーサイドの両方で実装が必要となります。

クライアント側の実装

公式ドキュメント: https://developers.google.com/recaptcha/docs/v3

クライアント側では、reCAPTCHA v3のトークンを発行し、そのトークンを何らかの方法でサーバーサイドに送信する実装をします。
公式ドキュメントでは Automatically bind the challenge to a buttonProgrammatically invoke the challenge の二つが紹介されていますが、今回は汎用性と明示性の観点から後者をベースにします。

まず、reCAPTCHA v3を利用したいページにて以下のスクリプトを実行します。スクリプトが実行されると、当該ページのJavaScriptのグローバル空間に grecaptcha オブジェクトが定義されます。

<!-- reCAPTCHA_site_keyは事前に発行したサイトキーで置き換えてください -->
<script src="https://www.google.com/recaptcha/api.js?render=reCAPTCHA_site_key"></script>

投稿ロジックの前に以下のようなコードを挟み込み、 grecaptcha オブジェクトに生えたメソッドを使ってトークンを発行します。発行したトークンは投稿データと一緒にサーバーへ送信します。

grecaptcha.ready(function() {
  // reCAPTCHA_site_keyは事前に発行したサイトキーで置き換えます。
  // action: 'submit' ではアクション名を指定しています。アクション名は英数字とスラッシュの範囲内で自由に設定できます。
  // 記事投稿には action: 'article', コメント投稿には action: 'comment' を指定するなど名前を分けることで、管理コンソールの中で各アクションの状況を区別することができます。
  grecaptcha.execute('reCAPTCHA_site_key', {action: 'submit'}).then(function(token) {
      // ここに元々あったデータ送信ロジックを書きます。送信データの中にtokenを追加することを忘れないでください。
      // XHRやfetchではなく<form />のPOSTを利用して送信する場合は、フォームの中にhiddenなinputを追加し、そのvalueにtokenを与えるような記述をすることになると思います。
  });
});

reCAPTCHA v3のバッジ表示を消す

公式ドキュメント: https://developers.google.com/recaptcha/docs/faq#id-like-to-hide-the-recaptcha-badge.-what-is-allowed

reCAPTCHA v3を導入するとページの右下に「reCAPTCHAによって保護されています」というバッジが表示されるようになります。ページデザインの都合で非表示にしたい場合は、以下のCSSをどこかに記述します。

.grecaptcha-badge { visibility: hidden; }

ただし、その場合は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.

サーバーサイドの実装

公式ドキュメント: https://developers.google.com/recaptcha/docs/verify

前述のクライアント側実装で送信されてきたトークンをサーバーサイドで検証し、botと判定されたら弾きます。
具体的には、 https://www.google.com/recaptcha/api/siteverify へPOSTでトークンとシークレットキーを送信し、返ってきたJSONのsuccessフィールドがfalseならbotと判断して投稿を弾く実装をします。当記事ではサンプルとしてpythonで書きます。d

# Djangoか何かを想定しています
def create_post(request):
    # 投稿者のIPアドレスを取得する。get_ip_addressは何かよしなに作ってください
    sender_ip_address = get_ip_address(request)
    
    # https://www.google.com/recaptcha/api/siteverify にトークン検証をしてもらう
    # requestsはpythonのrequestsライブラリを想定しています
    res = requests.post(
        'https://www.google.com/recaptcha/api/siteverify',
        data={
	    # シークレットキーに置き換える
            'secret': settings.RECAPTCHA_SECRET,
	    # クライアントで発行しサーバーに送ってもらったトークンで置き換える
            'response': request.POST.get('recaptcha_token'),
            'remoteip': sender_ip_address,
        }
    )
    # レスポンスJSONのsuccessフィールドを読む
    # 実際のコードではレスポンスボディが空だった場合などの対処についてもきちんと記述したほうがよいです
    is_recaptcha_verify_succeeded = res.json()['success']
    
    # botと判定されていれば(is_recaptcha_verify_succeededがfalseなら)投稿エラーにする
    if not is_recaptcha_verify_succeeded:
        return JsonResponse({'message': 'たぶんbotなので投稿失敗です'}, status_code=400)
	
    # ...以下、本来の投稿受付ロジックを色々書いていく

おわりに

reCAPTCHA v3の導入手順を紹介しました。簡単に無料で使えて嬉しかったです。2020年からはEnterpriseバージョンが出て、Enterpriseとは銘打たれているものの無料枠もかなり大きいようなので、今から使うならそちらも要検討かもしれません。
もし間違いがあればコメントいただけると嬉しいです。