1
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

rails devise(sign_in時)でreCAPTCHA V3の実装

初めてQiitaに投稿します!
軽〜い気持ちで投稿しています。
間違い等あれば、優しく指摘していただければ幸いです!

reCAPTCHAとは?

googleが提供しているAPIの一つで、人間が操作しているかどうかを確認するためのものです。
会員登録やログイン時に「私はロボットではありません」とこのような表示を見た事があると思います。
チェックするだけで済んだり、その後画像認証が出た場合車とか信号機とか横断歩道とかとかチェックした経験があるかと思います。あれです。
c92f501bfd292e7a5ceb955675e35aee.png

reCAPTCHAは大きく分けてV1,V2,V3の3種類があります。(V2の中にはInvisibleもあります)
今回はV3の実装です。

reCAPTCHA V3の特徴と導入メリット

reCAPTCHAのAIが、ユーザーのページ内での行動をスコアとして算出し、人間の操作かbotの操作かを判別します。
さらに、reCAPTCHA v3を配置したページでのユーザーの動きを学習し、利用が増えるとともに行動スコアの精度が高まっていくところがv3の特徴です。
メリットとしてはユーザーが認証操作を行う必要がない(V1のぐにゃぐにゃの文字入力やV2のわかりにくい画像選択をしなくて良い)こと。これに尽きると思います。

処理の流れ

1、グーグルサーバ側にトークンの要求・取得し、そのトークンをHTMLにセットする
2、セットしたトークンをreCAPTCHAに送る
3、reCAPTCHAにSecret keyを使ってトークンのチェック
4、チェックの結果が返される。(スコアやtrue・false、action等が返される)
5、スコアが合格点以上で条件を満たしている場合はログイン、それ以外の時はエラー文を表示させる処理をする

はじめに

まず下準備としてグーグルアカウントを作成し、下記URLよりSite keyとSecret keyを発行します。
(他のQiitaの記事やサイト等みていただければ、発行方法は詳しく書かれていますので、ここは省きます!)
https://www.google.com/recaptcha/admin/create

エラーがなければ、Site keyとSecret keyが発行されますので、そちらをconfig/credentials.ymlに登録します。(GitHubに上げなければいいので、別にdotenvでも大丈夫です。)
credentials.yml.enc なんでターミナルからエディタを指定して、編集します。(credentials.yml.encは直接エディタから編集する事はできません。)

ターミナル
$ EDITOR=vim bin/rails credentials:edit
credentials.yml.enc
recaptcha_site_key: 6Lc************************************* #上記で発行したsite key
recaptcha_secret_key: 6Lc************************************* #上記で発行したsecret key

deviseの導入

ここはサクッといきます。

Gemfile.
gem 'devise'
ターミナル
$ bundle install
$ rails g devise:install
$ rails g devise user
$ rake db:migrate
$ rails g devise:views          #ビューの生成
$ rails g devise:controllers users   #controller(カスタマイズ用)の作成

尚、今回はdeviseのデフォルト(emailとパスワード)での実装なので、編集を加える場合は適宜変更をお願いします。

routesの編集

config/routes.rb
  devise_for :users, :controllers => {
    :registrations => 'users/registrations',
    :sessions => 'users/sessions'   
  } 

  devise_scope :user do
    get "sign_in", :to => "users/sessions#new"
    get "sign_out", :to => "users/sessions#destroy" 
  end

(railsの再起動をお忘れなく、、、)

トークンの要求・取得・セット

それでは処理の流れをみていきます!!
reCAPTCHA 側にトークンの要求・取得・トークンをセットする

application.html.erb
    <script src="https://www.google.com/recaptcha/api.js?render=先ほど上記で発行したsite key"></script>
    <script>
      grecaptcha.ready(function () {
        grecaptcha.execute('先ほど上記で発行したsite key', { action: 'submit' }).then(function (token) {  
          var recaptchaResponse = document.getElementById('recaptchaResponse');
          recaptchaResponse.value = token;      
        });
      });
    </script>
  </head>

セットしたトークンをreCAPTCHAに送る(その際にSecret keyを使う)またその結果を受け取る

application_controller.rb
class ApplicationController < ActionController::Base
  RECAPTCHA_MINIMUM_SCORE = 0.5 #スコアの定義(0.5が一般的なようですが、自由に変更できます)
  # ... other methods

  def verify_recaptcha?(token,recaptcha_action)
    secret_key = Rails.application.credentials.dig(: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

json(reCAPTCHA側からのレスポンス)の中身

=> {"success"=>true, "challenge_ts"=>"2020-06-06T13:26:52Z", "hostname"=>"localhost", "score"=>0.9, "action"=>"login"}

ちなみにreCAPTCHAスコアの正確な判別アルゴリズムの内容は公開されていませんが、
ブラウザが保持するCookieやユーザーエージェントなどの情報に不整合があるとスコアは下がるようです。

controllerで先ほど定義したverify_recaptcha?を呼び出して、false・true時の処理

今回はログイン時にbotチェックをしたいので、deviseのcontrollerを変更していきます。

app/controllers/users/session_controller.rb
class Users::SessionsController < Devise::SessionsController
  #sign_in
  def create
    unless verify_recaptcha?(params[:recaptcha_token], 'login')
      flash.now[:recaptcha_error] = I18n.t('recaptcha.errors.verification_failed')  #reCAPTCHAのエラーメッセージi18nに対応
      return render action: :new
    end
    self.resource = warden.authenticate!(auth_options)
    sign_in(resource_name, resource)
    yield resource if block_given?
    respond_with resource, location: after_sign_in_path_for(resource)
  end

  protected

  def sign_in_params
    devise_parameter_sanitizer.sanitize(:sign_in)
  end

  def auth_options
    { scope: resource_name, recall: "#{controller_path}#new" }
  end

  def translation_scope
    'devise.sessions'
  end
end

view側の実装

app/views/users/sessions/new.html.erb

<% if flash[:recaptcha_error] %>
<div class="text">
  <p><span class="error"><%= flash[:recaptcha_error] %></span></p>
</div>
<% end %>

<h2>Log in</h2>

<%= form_with(model: @user, local: true,url: session_path(resource_name)) do |f| %>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password, autocomplete: "current-password" %>
  </div>

  <% if devise_mapping.rememberable? %>
    <div class="field">
      <%= f.check_box :remember_me %>
      <%= f.label :remember_me %>
    </div>
  <% end %>

  <div class="actions">
    <input type="hidden" name="recaptcha_response" id="recaptchaResponse"> <%# idをrecaptchaResponseにして、トークンをセットできるようにする %>
    <%= f.submit "Log in" %>
  </div>
    <%= recaptcha_execute('login') %> <%# viewへのreCaptcha追加し、actionの指定。 %>
<% end %>

<%= render "users/shared/links" %

V3は下記のロゴが表示されます。

5343de69f6284245c6f6fb7674014062.png

まとめ

今回は簡単な実装としましたが、スコアが満たなかった場合は柔軟に実装できるので便利だと感じました!!
また新規登録時に取り入れるのもいいかと思いますが、公式によると登録時はV2を推奨しているようです。
(V2とV3どちらも引き続きサポート・改善されるようで、ケースバイケースで使い分けを推奨しています)
ちなみに同ページ内で v2とv3の実装もできるようです。

以上。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
1
Help us understand the problem. What are the problem?