整合 reCAPTCHA 到你的專案上,搭配 rails、devise

reCAPTCHA 是 Google 在 2009年收購一家叫 CAPTCHA 的公司,其服務的目的是為了辨認機器人與防止廣告垃圾訊息氾濫。

reCAPTCHA 問題的所需的文字圖片,首先會由 reCAPTCHA 計畫網站利用 Javascript API 取得,在終端使用者回答問題後,伺服器再連回 reCAPTCHA 計畫的主機驗證使用者的輸入是否正確 - wiki


reCAPTCHA 大致分為 3 種:

  • v2 Checkbox:「我不是機器人」核取方塊,網站上最常看到的一種,選取圖片或文字來做驗證。

  • v2 Invisible:隱形 reCAPTCHA 標記,在發送之前先在背景依照使用者滑鼠軌跡、瀏覽器資訊、Cookie等…判斷是否為機器人,如果判斷為是,則會跳出 v2 Checkbox 的驗證方式來做第二次驗證。

  • v3:以分數驗證要求,驗證完在後端可以得到驗證分數,區間為 0.0 ~ 1.0,分數越低則機器人的可能性越高,可以自行以分數高低,來決定是否要進行第二次驗證,例如:寄送 email、簡訊驗證或是使用 v2 Checkbox …等。


本次會實作以下幾個項目:

  1. 實作 v2 Checkbox 在註冊帳號頁面
  2. 實作 reCAPTCHA v3 在登入頁面
  3. 實作 reCAPTCHA v3 fallback validate

環境

  • ruby 2.5.0
  • Rails 5.2.3
  • sqlite3 3.22.0

先準備一個擁有 Devise 登入頁面的專案:

recaptcha-with-rails-devise-demo

$ git clone https://github.com/treekey999/recaptcha-with-rails-devise-demo.git

$ cd recaptcha-with-rails-devise-demo

$ git checkout prepare_for_start # 切到已準備好環境的分支
$ git checkout -b my_practice # 在此建立一個 branch 並 checkout 過去

$ bundle
$ yarn
$ rails db:migrate
$ rails s

主要先準備了幾個前置作業:

每一步驟都可以在 git commit 中找的到。


實作 v2 Checkbox 在註冊帳號頁面

1. 註冊取得 site_key & secret_key

site key:前端用來跟 google recaptcha 取得 recaptcha token。

secret key:後端用來驗證從前端來的 recaptcha token。

註冊: https://www.google.com/u/1/recaptcha/admin/create

recaptcha_register_1

recaptcha_register_2

回到專案下,把 .env 複製一份並命名為 .env.local,由於已經把 .env.local 加到 .gitignore 所以不會把敏感資訊推到 github 上。

剛剛註冊得到得 site_keysecret_key,填到 v2 對應的 key 上,這時候你在 rails 就可以用 ENV["RECAPTCHA_V2_CHECKBOX_SITE_KEY"] 來取得內容。

RECAPTCHA_V2_CHECKBOX_SITE_KEY="your_site_key......."
RECAPTCHA_V2_CHECKBOX_SECRET_KEY="your_secret_key......"

RECAPTCHA_V3_SITE_KEY=""
RECAPTCHA_V3_SECRET_KEY=""

2. 安裝 gem

Gemfile 加上這一段,並做 $ bundle

https://github.com/ambethia/recaptcha

gem 'recaptcha', git: "https://github.com/ambethia/recaptcha.git"

3. overwrite devise user controller

匯出 devise controller。

$ rails g devise:controllers users

修改 routes.rb 告訴 devise user/registrations controller 要被覆寫。

Rails.application.routes.draw do
  root 'todos#index'  
  resources :todos

  devise_for :users, controllers: {
    registrations: 'users/registrations',
  }
end

4. 新增 before_action

修改 app/controllers/users/registrations_controller.rb, 在 create 的時候,要做 recaptcha 檢查,如果沒通過就 redirect_to 註冊頁面。

class Users::RegistrationsController < Devise::RegistrationsController

  before_action :check_recaptcha_v2, only: [:create]

  ......

  private

  def check_recaptcha_v2
    valid = verify_recaptcha secret_key: ENV["RECAPTCHA_V2_CHECKBOX_SECRET_KEY"]

    if not valid
      redirect_to new_user_registration_path
    end
  end
end

此時如果去註冊帳號,就會發現被檔下來了。

registration_not_valid

5. 加入 recaptcha_tags 到 register form

修改 app/views/devise/registrations/new.html.erb 24 行, 在 submit 前面加上 recaptcha_tag

  ......

  <div class="form-group">
    <%= recaptcha_tags site_key: ENV["RECAPTCHA_V2_CHECKBOX_SITE_KEY"] %>
  </div>

  <div class="form-group">
    <%= f.submit t('.sign_up'), class: 'btn btn-primary' %>
  </div>

  ......

如果一切都順利的話,可以看到表單上出現了 recaptcha 按鈕,並可以正確的送出表單 🎉 。

recaptcha_set_correct

可以看到帶了一個 g-recaptcha-response 的 params。

post_registration

如果失敗,應該會看到這個畫面,應該檢查一下 ENV 的值有沒有正確、.env.local 有沒有正確 或 recaptcha設定 有沒有加 localhost 到 domain 清單裡面。

recaptcha_setup_failure


實作 reCAPTCHA v3 在登入頁面

1. 註冊 recaptcha 並選擇 v3,加到 .env.local

RECAPTCHA_V2_CHECKBOX_SITE_KEY="your_site_key......."
RECAPTCHA_V2_CHECKBOX_SECRET_KEY="your_secret_key......"

RECAPTCHA_V3_SITE_KEY="your_v3_site_key......"
RECAPTCHA_V3_SECRET_KEY="your_v3_secret_key......"

2. 修改 routes.rb,覆寫 user/sessions controller

Rails.application.routes.draw do
  root 'todos#index'  
  resources :todos

  devise_for :users, controllers: {
    registrations: 'users/registrations',
    sessions: 'users/sessions',
  }
end

3. user session controller 加入 before_action

修改 app/controllers/users/sessions_controller.rb

分數為 0.1 ~ 1.0 之間,越低代表越可能是機器人,minimum_score 設為 0.5,表示低於 0.5 就會回傳 false

class Users::SessionsController < Devise::SessionsController

  before_action :check_recaptcha_v3, only: [:create]

  ......

  private

  def check_recaptcha_v3
    valid = verify_recaptcha(
      # action:當收到 recaptcha token 的時候要認哪一個名稱來做驗證
      #         與前端 form 表單呼應
      action: 'login',
      minimum_score: 0.5,
      secret_key: ENV["RECAPTCHA_V3_SECRET_KEY"],
    )

    if not valid
      redirect_to new_user_registration_path
    end
  end
end

4. 加入 recaptcha_v3 到 login form

修改 app/views/devise/sessions/new.html.erb 22 行, 在 submit 前面加上 recaptcha_v3action 必須跟 controller 驗證端命名一致。

  ......

  <div class="form-group">
    <%= recaptcha_v3 action: 'login', site_key: ENV["RECAPTCHA_V3_SITE_KEY"] %>
  </div>

  <div class="form-group">
    <%= f.submit  t('.sign_in'), class: 'btn btn-primary' %>
  </div>

  ......

成功的話可以看到出現在該頁右下角的位置。

setup_v3_correct

可以看到帶了一個 g-recaptcha-response,並有剛剛設置的 action 名稱。

post_session

如果失敗,打開 js console 查看錯誤訊息。

setup_v3_failure

5. 加入 v3 驗證失敗的 fallback 驗證

修改 app/controllers/users/sessions_controller.rb

class Users::SessionsController < Devise::SessionsController
  before_action :check_recaptcha_v3, only: [:create]
  # before_action :configure_sign_in_params, only: [:create]

  # GET /resource/sign_in
  def new
    # 把參數 `verify_v2` 存起來留給 view 使用
    @verify_v2 = params[:verify_v2] === 'true'
    super
  end

  ......

  private

  def verify_recaptcha_tags
    # 用來驗證 v2 recaptcha_tags (secret_key 不一樣)
    verify_recaptcha secret_key: ENV["RECAPTCHA_V2_CHECKBOX_SECRET_KEY"]
  end 

  def check_recaptcha_v3
    v3_valid = verify_recaptcha(
      # action:當收到 recaptcha token 的時候要認哪一個名稱來做驗證
      #         與前端 form 表單呼應
      action: 'login',
      minimum_score: 0.5,
      secret_key: ENV["RECAPTCHA_V3_SECRET_KEY"],
    )
    v2_valid = verify_recaptcha_tags unless v3_valid

    if v3_valid || v2_valid
      # 如果驗證成功要額外做什麼事...
    else
      # 驗證失敗, 導到登入頁面並帶一個 verify_v2=true 的參數
      redirect_to new_user_session_path(verify_v2: true)
    end
  end
end

修改 app/views/devise/sessions/new.html.erb

  ......

  <div class="form-group">
    <% if @verify_v2 %>
      <%= recaptcha_tags site_key: ENV["RECAPTCHA_V2_CHECKBOX_SITE_KEY"] %>
    <% else %>
      <%= recaptcha_v3 action: 'login', site_key: ENV["RECAPTCHA_V3_SITE_KEY"] %>
    <% end %>
  </div>

  <div class="form-group">
    <%= f.submit  t('.sign_in'), class: 'btn btn-primary' %>
  </div>

  ......

由於無法完全模擬出分數很低的情況,所以可以直接在登入網址加上 ?verify_v2=true,來測試用 v2 checkbox 與用 v3 可不可以成功登入。


模擬機器人瀏覽

打開 js console,將 User agent 改成 Googlebot/2.1,recaptcha 就會認為這是機器人瀏覽,使用此情況最好是在浮動 IP 的網路上會比較好,recaptcha v3 會記錄你的 ip verify 次數。

但是這也沒辦法完全測試剛剛完成的 fallback 驗證,因為在進行 v2 checkbox 的過程中,他會吐出無窮盡的圖片給你點選。

simulate_low_score


參考:

https://github.com/ambethia/recaptcha

https://developers.google.com/recaptcha/intro?hl=zh-TW

react 版本 recaptcha

google recaptcha demo