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

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


# reCAPTCHAとは？
googleが提供しているAPIの一つで、人間が操作しているかどうかを確認するためのものです。
会員登録やログイン時に「私はロボットではありません」とこのような表示を見た事があると思います。
チェックするだけで済んだり、その後画像認証が出た場合車とか信号機とか横断歩道とかとかチェックした経験があるかと思います。あれです。
![c92f501bfd292e7a5ceb955675e35aee.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606335/6ea0faa6-dab7-0176-743b-9db30949c2ca.png)




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

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


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


# はじめに
まず下準備としてグーグルアカウントを作成し、下記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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606335/1a5283ac-81b5-3bdb-a7e8-9318dfd345bb.png)


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

以上。