跳到內文 跳到搜尋
方法
H
具備模組

實例公開方法

has_secure_password(屬性 = :密碼, 驗證: true, 重設令牌: true)

新增方法,用於設定及針對 BCrypt 密碼進行驗證。此機制要求您具備一個 XXX_摘要 屬性,其中 XXX 是您想要的密碼的屬性名稱。

會自動新增下列驗證

  • 建立時,密碼必須存在

  • 密碼長度應小於或等於 72 位元組

  • 確認密碼(使用 XXX_確認 屬性)

如果不需要確認驗證,只需略去 XXX_確認 的值即可(亦即不要提供表單欄位)。當此屬性具有 nil 值時,就不會觸發驗證。

此外,會建立一個 XXX_挑戰 屬性。將其設定為非 nil 的值時,會驗證目前的持久化密碼。此驗證倚賴 ActiveModel::Dirty 提供的髒汙追蹤;如果未定義髒汙追蹤方法,就會失敗。

可以傳遞 驗證: false 作為引數,來忽略以上所有驗證。這樣就能完全自訂驗證行為。

最後,如果將 重設令牌 設定為 true(預設採用此設定),且物件回應 generates_token_for(Active Records 會這麼做),就會自動設定在發出後 15 分鐘內有效的密碼重設令牌。

若要使用 has_secure_password,請將 bcrypt (~> 3.1.7) 新增到您的 Gemfile

gem "bcrypt", "~> 3.1.7"

範例

使用 Active Record(它會自動包含 ActiveModel::SecurePassword
# Schema: User(name:string, password_digest:string, recovery_password_digest:string)
class User < ActiveRecord::Base
  has_secure_password
  has_secure_password :recovery_password, validations: false
end

user = User.new(name: "david", password: "", password_confirmation: "nomatch")

user.save                                                      # => false, password required
user.password = "vr00m"
user.save                                                      # => false, confirmation doesn't match
user.password_confirmation = "vr00m"
user.save                                                      # => true

user.authenticate("notright")                                  # => false
user.authenticate("vr00m")                                     # => user
User.find_by(name: "david")&.authenticate("notright")          # => false
User.find_by(name: "david")&.authenticate("vr00m")             # => user

user.recovery_password = "42password"
user.recovery_password_digest                                  # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
user.save                                                      # => true

user.authenticate_recovery_password("42password")              # => user

user.update(password: "pwn3d", password_challenge: "")         # => false, challenge doesn't authenticate
user.update(password: "nohack4u", password_challenge: "vr00m") # => true

user.authenticate("vr00m")                                     # => false, old password
user.authenticate("nohack4u")                                  # => user
有條件地要求密碼
class Account
  include ActiveModel::SecurePassword

  attr_accessor :is_guest, :password_digest

  has_secure_password

  def errors
    super.tap { |errors| errors.delete(:password, :blank) if is_guest }
  end
end

account = Account.new
account.valid? # => false, password required

account.is_guest = true
account.valid? # => true
使用密碼重設令牌
user = User.create!(name: "david", password: "123", password_confirmation: "123")
token = user.password_reset_token
User.find_by_password_reset_token(token) # returns user

# 16 minutes later...
User.find_by_password_reset_token(token) # returns nil

# raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
User.find_by_password_reset_token!(token)
# File activemodel/lib/active_model/secure_password.rb, line 116
      def has_secure_password(attribute = :password, validations: true, reset_token: true)
        # Load bcrypt gem only when has_secure_password is used.
        # This is to avoid ActiveModel (and by extension the entire framework)
        # being dependent on a binary library.
        begin
          require "bcrypt"
        rescue LoadError
          warn "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install."
          raise
        end

        include InstanceMethodsOnActivation.new(attribute, reset_token: reset_token)

        if validations
          include ActiveModel::Validations

          # This ensures the model has a password by checking whether the password_digest
          # is present, so that this works with both new and existing records. However,
          # when there is an error, the message is added to the password attribute instead
          # so that the error message will make sense to the end-user.
          validate do |record|
            record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
          end

          validate do |record|
            if challenge = record.public_send(:"#{attribute}_challenge")
              digest_was = record.public_send(:"#{attribute}_digest_was") if record.respond_to?(:"#{attribute}_digest_was")

              unless digest_was.present? && BCrypt::Password.new(digest_was).is_password?(challenge)
                record.errors.add(:"#{attribute}_challenge")
              end
            end
          end

          # Validates that the password does not exceed the maximum allowed bytes for BCrypt (72 bytes).
          validate do |record|
            password_value = record.public_send(attribute)
            if password_value.present? && password_value.bytesize > ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
              record.errors.add(attribute, :password_too_long)
            end
          end

          validates_confirmation_of attribute, allow_blank: true
        end

        # Only generate tokens for records that are capable of doing so (Active Records, not vanilla Active Models)
        if reset_token && respond_to?(:generates_token_for)
          generates_token_for :"#{attribute}_reset", expires_in: 15.minutes do
            public_send(:"#{attribute}_salt")&.last(10)
          end

          class_eval <<-RUBY, __FILE__, __LINE__ + 1
            silence_redefinition_of_method :find_by_#{attribute}_reset_token
            def self.find_by_#{attribute}_reset_token(token)
              find_by_token_for(:#{attribute}_reset, token)
            end

            silence_redefinition_of_method :find_by_#{attribute}_reset_token!
            def self.find_by_#{attribute}_reset_token!(token)
              find_by_token_for!(:#{attribute}_reset, token)
            end
          RUBY
        end
      end