ログイン画面にreCAPTCHA v3を導入する

ユーザー登録やログインなどで不安になるのが「ボット」の存在です。

検索エンジンクローラーなど悪意の無いものであればよいのですが、機械的に大量のユーザーを作成されたり、悪意のある人が第三者のアカウントをゲットしようとプログラムを組んで攻めてくる可能性が有名なサービスになればなるほど高くなります。知名度の低いサービスであっても決済情報が保存されているような場合は狙われることもあるでしょう。

そんな心配に比較的かんたんに立ち向かえるのがGoogle社が提供する「reCAPTCHA」です。「私はロボットではありません」のチェックボックスを見かけたことがある方も多いと思いますが、最新版のv3ではこのチェックも不要になり導入のハードルが非常に下がりました。 developers.google.com

今回はこのreCAPTCHAをログインする仕組みに導入するという設定でまとめていきます。

基本的な原理

仕組み自体はシンプルです。

クライアントからサーバへ送信する直前にreCAPTCHAサーバからトークンをもらい一緒に送りつけます。サーバではもらったトークンをreCAPTCHAサーバに送って正しい物かどうか検証するだけ。

「サイトキー」と「シークレット」はreCAPTCHAに登録した際に発行された物を用います。トークンはリクエスト毎に取得し基本的に毎回異なった文字列が返ってくる前提で処理を書きます(再利用はできない)。

トークンの有効期限は2分間

トークンはreCAPTCHAサーバからもらってくることもあり、ページの読み込みが終わったタイミングで予め行っておきたい気持ちにかられますが残念ながら有効期限が発行から2分間となかなかシビアな設定になっていることから、サーバへの送信直前に行う必要があります。

reCAPTCHA tokens expire after two minutes. If you're protecting an action with reCAPTCHA, make sure to call execute when the user takes the action rather than on page load.

※reCAPTCHAドキュメントより

この一文を見逃していて見事にハマりましたw 悪い人の試行回数を減らすためでしょうから仕方ないのですがボタンを押してから待ち時間が若干ですが伸びる点が気になりますね。フォームの無効化や「Now Loading」などの表示はしておいた方が良さそうです。

大量に利用する場合は有料プラン

reCAPTCHAは月間100万回までの検証は無料ですが、それ以降は有料プラン(Enterprise)に移行する必要があります。こちらはGCPから提供されており1,000回あたり1ドルの料金となっています。 cloud.google.com

それなりの規模のサービスでなければあまり心配する必要は無さそうですが、定期的に管理画面から利用状況はチェックしておいた方が良さそうですね。

サンプル

IDとパスワードでログインするページにreCAPTCHAを導入するサンプルです。サーバ側はNode.js+expressでサクッと書いていますが、他の環境でもロジック自体は同じです。

reCAPTCHA側の設定

v3 Admin Console」をクリックし適当に設定を行います。Googleのアカウントが必要です。 www.google.com

最終的に「サイトキー」と「シークレットキー」が発行されるのでこれをメモします。サイトキーはHTML中に埋め込むので第三者に見られてもかまいませんが、シークレットキーはサーバ側の検証で利用するため誰にも見られないよう慎重に取り扱う必要があります。

クライアント

書き方

クライアント(HTML)側のは本来の処理をgrecaptcha.executeの実行後のthen内に書くだけです。

<form>
  <!-- テキストボックスなど -->
  <button id="btn" type="button">送信</button>
</form>

<script src="https://www.google.com/recaptcha/api.js?render=【サイトキー】"></script>
<script>
document.getElementById('btn').addEventListener('click', ()=>{
  grecaptcha.ready( ()=>{
    grecaptcha.execute('【サイトキー】', {action: 'submit'}).then( async (token)=>{
        // ここにサーバへ送信する処理を書く
        // reCAPTCHAのトークンは引数tokenに入っています
    })
  })
})
</script>

grecaptcha.readyトークンを取得する準備が整ったか確認できますので、この中にトークンを実際に取得するgrecaptcha.executeを記述します。あとはもらってきたトークンを他の値と一緒にサーバへ送り、サーバ側で検証します。

サンプルコード

実際の処理を書いてみます。「【サイトキー】」とある場所はreCAPTCHAの管理画面で発行された物に置き換えてください。2箇所あります。

<form>
  <label for="loginid">ログインID</label>
  <input type="text" id="loginid">

  <label for="passwd">パスワード</label>
  <input type="password" id="passwd">
  <button id="btn-submit" type="button">送信</button>
</form>

<script src="https://www.google.com/recaptcha/api.js?render=【サイトキー】"></script>
<script>
document.getElementById('btn-submit').addEventListener('click', ()=>{
  grecaptcha.ready( ()=>{
    grecaptcha.execute('【サイトキー】', {action: 'submit'}).then( async (token)=>{
      const loginid = document.getElementById('loginid').value
      const passwd = document.getElementById('passwd').value
      const params = {
        loginid,   // ログインID
        passwd,    // パスワード
        token      // reCAPTCHAのトークン
      }

      // 自前のサーバへログイン情報+トークンを送る
      const json = await requestLoginServer(params)

      // レスポンスを確認
      if( json.status ){
        alert('ログイン成功')
      }
      else{
        alert('残念!')
      }
    })
  })
})

/**
 * ログインサーバへリクエスト
 */
async function requestLoginServer(params){
  const res = await fetch('http://example.com:3000/api/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams(params)
  })

  return await res.json()
}
</script>

普通にsubmitしたい場合

ここではFetch APIを使ってAjax/SPA的なことをしていますが、普通にフォームをsubmitしたい場合はといった感じのタグを用意しておきsubmit前にreCAPTCHA用のトークンをセットする方法もあります。

<form id="frm-login" action="http://example.com:3000/login" method="POST">
  <input id="input-token" type="hidden" name="token">
  (中略)
  <button id="btn-submit" type="button">ログイン</button>
</form>

<script>
document.getElementById('btn-submit').addEventListener('click', ()=>{
  grecaptcha.ready( ()=>{
    grecaptcha.execute('【サイトキー】', {action: 'submit'}).then( (token)=>{
      // reCAPTCHAトークンをinputタグに詰める
      document.getElementById('input-token').value = token

      // フォーム送信
      const form = document.getElementById('frm-login')
      form.submit()
    })
  })
})
</script>

このパターンを採用する場合はformタグにaction属性やmethod属性の指定をお忘れなく。サーバからもJSONではなく通常のWebページを返却する必要があります。

サーバー

ここではNode.js + expressで適当に用意していますが、他の環境でも同様のサーバ間通信を行えば実現できます。

トークン検証の原理

トークンを検証するAPIに対してPOSTでシークレットとトークンを渡すと、結果がJSONで返ってきます。

コマンドライン(CLI)で実行するなら以下のような感じになります。

$ curl -X POST -d 'secret=【シークレット】&amp;response=【トークン】' https://www.google.com/recaptcha/api/siteverify

レスポンスはJSONで返ってきます。以下は公式ドキュメントからの抜粋です。

{
  "success": true|false,      // whether this request was a valid reCAPTCHA token for your site
  "score": number             // the score for this request (0.0 - 1.0)
  "action": string            // the action name for this request (important to verify)
  "challenge_ts": timestamp,  // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
  "hostname": string,         // the hostname of the site where the reCAPTCHA was solved
  "error-codes": [...]        // optional
}

successtrueであればトークンが正しいことの証明になります。これだけでも不正なアクセスは防げそうですが、ボットかどうかを判定するには同時にscoreが一定以上あるかも見る必要があります(数値が低い方がボットの可能性が高い)。いくつからボットと判定するかの閾値はこちらで決める必要があるようです。 scoreを集計した値が管理画面にグラフで表示されますので、最初はsuccessだけチェックしておいて運用しながらこのグラフを見て閾値を決めても良いかもしれませんね。

サンプルコード

トークンの検証はtokenVerify関数で行っています。認証はloginAuth関数のお仕事ですがこちらは本題では無いのでコードは割愛しています。

//-----------------------------
// 定数
//-----------------------------
// reCAPTCHA トークン検証用 エンドポイント
const RECAPTCHA_API_URL = 'https://www.google.com/recaptcha/api/siteverify';

// reCAPTCHA シークレット
// (reCAPTCHA管理画面で発行されたものに置き換えてください)
const RECAPTCHA_SITE_SECRET = '【サイトシークレット】'

// HTTPサーバのポート番号
const PORT = 3000

//-----------------------------
// モジュール
//-----------------------------
const fetch = require('node-fetch')
const express = require('express')
const app = express()

// POST時のパラメーターを良い感じに受け取る
app.use(express.urlencoded({extended:true}))

//-----------------------------
// ルーティング
//-----------------------------
/**
 * ログイン認証
 */
app.post('/api/login', async (req, res) =>{
  // パラメーターを受け取る
  const loginid = req.body.loginid  // ログインID
  const passwd  = req.body.passwd   // パスワード
  const token   = req.body.token    // reCAPTCHAのトークン

  // reCAPTCHAのトークンを検証
  if( ! await tokenVerify(token) ){
    res.json({status:false, message:'Invalid reCAPTCHA token'})
    return
  }

  // ログイン
  if( ! loginAuth(loginid, passwd) ){
    res.json({status:false, message:'Invalid id or password'})
    return;
  }
  res.json({status:true})
})

//-----------------------------
// HTTPサーバ起動
//-----------------------------
app.listen(PORT, () => {
  console.log(`listening at http://localhost:${PORT}`)
})


/**
 * reCAPTCHA検証
 *
 * @param {string} token
 * @return {boolean}
 */
async function tokenVerify(token){
  const response = await fetch(RECAPTCHA_API_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      secret: RECAPTCHA_SITE_SECRET,  // シークレット
      response: token                 // トークン
    }),
  })
  const json = await response.json()
  if( 'success' in json ){  // ボットかチェックしたい場合はscoreも見る
    return( json.success )
  }
  return( false )
}

/**
 * ログイン認証
 *
 * @param {string} id
 * @param {string} pw
 * @return {boolean}
 */
function loginAuth(id, pw){
  // ここに必要な処理を書く(省略)
  return(true)
}

サンプルコードを実行

Node.jsをインストールします。こだわりが無い場合は最新の安定版(LTS)でOKです。

サンプルコードをserve.jsなど適当なファイル名で保存、npm initでpackage.jsonを生成し必要なモジュールをインストールします。node-fetchは3系からESModule用になったためここでは2系の最新版を入れます。

$ npm init
$ npm install express node-fetch@2.x

最後にnodeコマンドでサーバを起動するだけです。

$ node serve.js

参考ページ