第一步:取得金鑰
在開始使用ReCaptcha之前,需要先申請recaptcha的金鑰。進入頁面後點擊右上角的「Get reCAPTCHA」。
進入頁面後先為你的App命名,任何方便你辨識的名稱都可以,這邊為了教學方便我使用Demo。接下來的教學中我們會選擇常見的reCAPTCHA V2來開發。
在Domains的地方我們會加上localhost和127.0.0.1(兩者都代表本機端,為了避免預期外的錯誤,所以兩個都設置)。如果有production記得加上對應的網域。
按下Register後我們就會進到下一個頁面,在這裡我們可以得到開發所需要的金鑰。
第二步:安裝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
預計頁面上會出現這樣的效果。
第五步:在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
第六步:客製化錯誤訊息
一般來說要客製化錯誤訊息可以從兩個地方下手
- 在
verify_captchamethod中透過argument傳入客製化訊息 - 在
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驗證失敗,請再試一次"
第七步:整合devise和recaptcha的error message
在測試devise和recaptcha的錯誤訊息時,你可能會發現這兩種的錯誤訊息出現的位置不同、格式不同,非常不協調。
到Recaptcha Gem中的原始檔,你可以發現有兩個method負責處理驗證失敗後產生錯誤訊息,分別是
verify_recaptcha:如果驗證失敗,觸發recapthca_errorrecaptha_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被觸發後會有兩種顯示錯誤訊息的方式
- 透過
model.errors.add將訊息加入model object的錯誤訊息集合中 - 透過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=#<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={:base=>["驗證失敗,請再試一次"]},
@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])
讓我們刷新頁面再嘗試一次。
看來我們大功告成了呢!