【Rails】如何在devise中使用Recaptcha

linjiahung(40)
Published in
#cn
Words
1460
Reading
7 min
9y
Listen to this post
~10 min

第一步:取得金鑰

在開始使用ReCaptcha之前,需要先申請recaptcha的金鑰。進入頁面後點擊右上角的「Get reCAPTCHA」。
alt

進入頁面後先為你的App命名,任何方便你辨識的名稱都可以,這邊為了教學方便我使用Demo。接下來的教學中我們會選擇常見的reCAPTCHA V2來開發。

alt

在Domains的地方我們會加上localhost和127.0.0.1(兩者都代表本機端,為了避免預期外的錯誤,所以兩個都設置)。如果有production記得加上對應的網域。

alt

按下Register後我們就會進到下一個頁面,在這裡我們可以得到開發所需要的金鑰。

alt

第二步:安裝devise和recaptcha

為了在devise中使用recaptcha,我們需要在Gemfile中安裝這兩個gem。

gem "devise"
gem "recaptcha", require: "recaptcha/rails"

接著執行bundle install。

第三步:配置reCAPTCHA金耀和設定envrionment variable

配置reCAPTCHA金耀

安裝成功後我們需要在intializer這個folder中新增一個recaptcha.rb的檔案來配置(configure)recaptcha。

Recaptcha.configure do |config|
  config.site_key  = ENV["RECAPTCHA_SITE_KEY"]
  config.secret_key = ENV["RECAPTCHA_SECRET_KEY"]
end

在這邊config.site_key和config.secret_key分別對應到我們從google上取得的recaptcha金鑰。

設定envrionment variable

接下來我們會透過local_env.yml這個檔案來設定environment variable來儲存我們的金鑰。(註:其他常見的environment variable設定方式還有透過figaro和設定unix environment variable等等,請參考。)

之所以會需要這樣environment variable的原因是,我們不希望在把檔案推上github時,把自己的金鑰暴露出來,以免被有心人士盜用。

在config這個folder中新建一個local_env.yml檔,並設定我們的environment variable。

# config/local_env.yml
RECAPTCHA_SITE_KEY: '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
RECAPTCHA_SECRET_KEY: '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx'

接著記得在.gitignore.rb這個檔案中加入/config/local_env.yml 這段程式碼,這樣才能夠確保local_env.yml這個檔案不會被加入到git repository中。

最後我們需要到config/application.rb這個檔案來配置

config.before_configuration do
  env_file = File.join(Rails.root, 'config', 'local_env.yml')
  YAML.load(File.open(env_file)).each do |key, value|
    ENV[key.to_s] = value
  end if File.exists?(env_file)
end

這段程式碼會打開/config/local_env.yml,然後讀取每一個key-value pair並設置environment variable。

第四步:在view中加入reCAPTCHA

註:因為devise的設置需要花上比較大的篇幅,所以在這裡我們假定你已經完成了devise的設置。

假設我們今天要再註冊頁面加入reCAPTCHA,那我們就在app/views/devise/registrations/new.html.slim中加入= recaptcha_tags

    = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
      = devise_error_messages!
      .form-group
        = f.label :email, '電子郵件', class: 'form-control-label'
        = f.email_field :email, autofocus: false, class: 'form-control form-control-sm'
      .form-group
        = f.fields_for :mobiles do |ff|
          = ff.label :number, '手機號碼', class: 'form-control-label'
          = ff.phone_field :number, autofocus: false, class: 'form-control form-control-sm'
      .form-group
        = f.label :password, '密碼', class: 'form-control-label'
          - if @minimum_password_length
              span.form-text.text-danger
                = @minimum_password_length 
                |characters minimum
        = f.password_field :password, autocomplete: "off", class: 'form-control form-control-sm'
      .form-group
        = f.label :password_confirmation, '重新輸入密碼', class: 'form-control-label'
        = f.password_field :password_confirmation, autocomplete: "off", class: 'form-control form-control-sm'
      .form-row
        .col
          label 安全驗證
        .col
          = recaptcha_tags

預計頁面上會出現這樣的效果。

alt

第五步:在controller設定reCAPTCHA

在我們送出建立帳號的請求之前,我們希望可以先驗證使用者是否通過reCAPTCHA的驗證。

所以在view中加入recaptcha_tags的helper method後,我們需要到Devise::RegistrationsController來設定。

在Devise::RegistrationsController中,我們先建立一個private methodcheck_captcha,再使用append_before_action,規範每次執行create method前,都要先執行check_recaptcha。

class RegistrationsController < Devise::RegistrationsController
  prepend_before_action :check_captcha, only: [:create] # Change this to be any actions you want to protect.

  private
    def check_captcha
      unless verify_recaptcha
        self.resource = resource_class.new sign_up_params
        resource.validate # Look for any other validation errors besides Recaptcha
        respond_with_navigational(resource) { render :new }
      end 
    end
end

第六步:客製化錯誤訊息

一般來說要客製化錯誤訊息可以從兩個地方下手

  1. 在verify_captcha method中透過argument傳入客製化訊息
  2. 在config/local/en.yml 檔中更改

如何透過verify_captcha method客製錯誤訊息

直接將想要更改的訊息,直接傳入verify_captcha即可。
key為:message,value則是你期望的錯誤訊息。

verify_captcha(:message=>"reCAPTCHA驗證失敗,請再試一次")

如何在config/locales/zh-TW.yml 檔中更改

在config/locales/zh-TW中加入以下的內容即可更改訊息。

---
zh-TW:
  recaptcha:
    errors:
      verification_failed: "reCAPTCHA驗證失敗,請再試一次"

alt

第七步:整合devise和recaptcha的error message

在測試devise和recaptcha的錯誤訊息時,你可能會發現這兩種的錯誤訊息出現的位置不同、格式不同,非常不協調。

alt

到Recaptcha Gem中的原始檔,你可以發現有兩個method負責處理驗證失敗後產生錯誤訊息,分別是

  • verify_recaptcha:如果驗證失敗,觸發recapthca_error
  • recaptha_error:先去找18n中有沒有對應的錯誤訊息。接著看argument中有沒有傳入對應的model名稱,如果有,直接將error message加到該object中;如果沒有,在recaptcha_flash_supported?成立的情況下,建立flash message。
def verify_recaptcha(options = {})
# ...
  if verified
    flash.delete(:recaptcha_error) if recaptcha_flash_supported? && !model
    true 
  else 
    recaptcha_error( 
    model, 
    attribute, 
    options[:message], 
    "recaptcha.errors.verification_failed", 
    "reCAPTCHA  verification  failed,  please  try  again." 
    )
    false
  end
end
# ...

def recaptcha_error(model, attribute, message, key, default)
  message ||= Recaptcha.i18n(key, default) 
  if model 
    model.errors.add attribute, message 
  else 
    flash[:recaptcha_error] = message if recaptcha_flash_supported? 
  end 
end

所以我們可以知道,在verify_recaptcha驗證失敗後會觸發recaptcha_error。recaptcha_error被觸發後會有兩種顯示錯誤訊息的方式

  1. 透過model.errors.add將訊息加入model object的錯誤訊息集合中
  2. 透過flash message顯示

而在Devise中,如果要客製化或複寫原先前端頁面中的error message的話,可以透過新增一個devise_helper.rb的檔案來修改devise_error_messages!的內容

如果參考devise的這份文件,我們可以看到預設的devise_error_message的原始碼。

module DeviseHelper
  def devise_error_messages!
    return "" unless devise_error_messages?

    messages = resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join
    sentence = I18n.t("errors.messages.not_saved",
                      :count => resource.errors.count,
                      :resource => resource.class.model_name.human.downcase)

    html = <<-HTML
 <div id="error_explanation">
 <h2>#{sentence}
 <ul>#{messages}
 </div>
 HTML

    html.html_safe
  end

  def devise_error_messages?
    !resource.errors.empty?
  end

end

Devise預設的方法也是將error message加入object的錯誤訊息集合中,所以我們必須要把Recaptcha的錯誤訊息加進去。

於是我們改寫了一下check_captcha的方法,建立self.resource的object並傳入verify_recaptcha中,並加入byebug來確認是否有建立error message

def check_captcha
    self.resource = resource_class.new sign_up_params
    unless verify_recaptcha({model: self.resource})
      byebug
      resource.validate # Look for any other validation errors besides Recaptcha
      respond_with_navigational(resource) { render :new }
    #       在這邊進行render,所以不會執行create觸發驗證
    end
  end

而我們可以透過resource.errors的回傳值知道,我們已經確實地將錯誤訊息塞入resource.errors的集合當中。

#<ActiveModel::Errors:0x007fb5a4fa9928 base@base=#<User id: nil, email: "", created_at: nil, updated_at: nil, first_name: nil, last_name: nil, gender: nil, id_card: nil, marriage: nil, children_count: nil, bank_account: nil, date_of_birth: nil, is_same_address: false, education: nil, safe_question: nil, safe_question_answer: nil, bank_id: nil, is_subscription: nil, sign_up_step: "new_b2", bankbook: nil, atm_card: nil, id_card_front: nil, id_card_back: nil, id_card_front_updated_at: nil, id_card_back_updated_at: nil, bank_account_info: nil, bank_account_info_updated_at: nil, id_card_date_of_issue: nil, id_card_issue_of_place: nil, id_card_collar_replacement: nil, recaptcha: nil>, messages@messages={:base=>["驗證失敗,請再試一次"]}, details@details={:base=>[{:error=>"驗證失敗,請再試一次"}]}>

但是在下一個步驟的resource.validate這個方法會觸發valid?這個方法,而這個方法會將errors中所有的錯誤訊息清空。

def valid?(context = nil) 
  current_context, self.validation_context = validation_context, context 
  errors.clear # 清空所有錯誤訊息
  run_validations! 
ensure 
  self.validation_context = current_context 
end 

所以我們不能在verify_captcha這個步驟中加入錯誤訊息,而是應該到devise_helper.rb中做這件事。

在devise_helper.rb中的最上方加入以下的code,這樣可以將error message從flash message中拉出來加入到object.errors中的錯誤訊息的集合,並且保證Recaptcha有錯誤會觸發devise_error_messages!。

module DeviseHelper
  def devise_error_messages!
    if flash[:recaptcha_error]
      resource.errors.add(:base, "Recaptcha #{flash[:recaptcha_error]}") 
      flash.delete(:recaptcha_error)
    end

    return "" unless devise_error_messages? # 沒有錯誤訊息就不觸發    
  # ...     
end

resource.errors.add(:base, "Something wrong")可以針對object加入錯誤訊息,而不指定任一個attribute。(請參考errors[:base])

讓我們刷新頁面再嘗試一次。

alt

看來我們大功告成了呢!

參考資料: