跳至內容 跳至搜尋

Active Support 訊息驗證器

MessageVerifier 易於產生和驗證已簽章以防止竄改的訊息。

在 Rails 應用程式中,你可以使用 Rails.application.message_verifier 來管理各個使用案例中驗證器的獨特實體。 深入了解

這適用於下列狀況,例如,記錄我或自動取消訂閱連結,其中,Session 儲存不適合或不可用。

首先,產生已簽章的訊息

cookies[:remember_me] = Rails.application.message_verifier(:remember_me).generate([@user.id, 2.weeks.from_now])

稍後驗證訊息

id, time = Rails.application.message_verifier(:remember_me).verify(cookies[:remember_me])
if time.future?
  self.current_user = User.find(id)
end

簽章並非加密

已簽章的訊息並未加密。載荷僅編碼(預設為 Base64),且任何人都可以解碼。簽章僅確保訊息未遭竄改。例如:

message = Rails.application.message_verifier('my_purpose').generate('never put secrets here')
# => "BAhJIhtuZXZlciBwdXQgc2VjcmV0cyBoZXJlBjoGRVQ=--a0c1c0827919da5e949e989c971249355735e140"
Base64.decode64(message.split("--").first) # no key needed
# => 'never put secrets here'

如果您還需要加密內容,則必須改用 ActiveSupport::MessageEncryptor

將訊息限定於特定用途

建議不要在應用程式中為不同用途使用同一個驗證器。這樣做可能允許惡意行為者重新使用已簽章的訊息來執行未經授權的動作。你可以透過將已簽章的訊息限定於特定 :purpose 來降低此風險。

token = @verifier.generate("signed message", purpose: :login)

在驗證時必須傳遞相同的用途以取回資料

@verifier.verified(token, purpose: :login)    # => "signed message"
@verifier.verified(token, purpose: :shipping) # => nil
@verifier.verified(token)                     # => nil

@verifier.verify(token, purpose: :login)      # => "signed message"
@verifier.verify(token, purpose: :shipping)   # => raises ActiveSupport::MessageVerifier::InvalidSignature
@verifier.verify(token)                       # => raises ActiveSupport::MessageVerifier::InvalidSignature

同樣地,如果訊息沒有用途,則在使用特定用途驗證時,訊息將不會被傳回。

token = @verifier.generate("signed message")
@verifier.verified(token, purpose: :redirect) # => nil
@verifier.verified(token)                     # => "signed message"

@verifier.verify(token, purpose: :redirect)   # => raises ActiveSupport::MessageVerifier::InvalidSignature
@verifier.verify(token)                       # => "signed message"

訊息到期

預設情況下,訊息會永久存在,即使在一年後驗證訊息,仍然會傳回原始值。但可設定訊息在特定時間過後到期(使用 :expires_in:expires_at)。

@verifier.generate("signed message", expires_in: 1.month)
@verifier.generate("signed message", expires_at: Time.now.end_of_year)

Messages 可以在到期之前驗證並傳回訊息。在此之後,verified 方法傳回 nil,而 verify 引發 ActiveSupport::MessageVerifier::InvalidSignature

輪替金鑰

MessageVerifier 也支援輪替舊設定,只要回歸到驗證器堆疊即可。呼叫 rotate 來建置並新增驗證器,以便 verifiedverify 也會嘗試使用後備方案驗證。

預設情況下,任何輪替的驗證器都會使用主驗證器的值,除非另有指定。

你會將新的預設值提供給驗證器

verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512", serializer: JSON)

然後逐步將舊值輪替出來,方法是將其新增為後備方案。使用舊值產生任何訊息,在輪替移除之前,訊息都能運作。

verifier.rotate(old_secret)          # Fallback to an old secret instead of @secret.
verifier.rotate(digest: "SHA256")    # Fallback to an old digest instead of SHA512.
verifier.rotate(serializer: Marshal) # Fallback to an old serializer instead of JSON.

然而,上述範例很可能會合併到一個輪替中

verifier.rotate(old_secret, digest: "SHA256", serializer: Marshal)
命名空間
方法
G
N
V

類別公開方法

new(secret, **options)

使用用於簽章的機密,初始化新的 MessageVerifier

選項

:digest

用於簽章的 Digest。預設為 "SHA1"。請參閱 OpenSSL::Digest 以取得其他選項。

:serializer

用於序列化訊息資料的序列化器。您可以指定任何回應 dumpload 的物件,或者從幾個預先設定的序列化器中選擇::marshal:json_allow_marshal:json:message_pack_allow_marshal:message_pack

預先設定的序列化器包含一個備用機制來支援多種序列化格式。例如,:marshal 序列化器將會使用 Marshal 進行序列化,但可以使用 Marshal、ActiveSupport::JSONActiveSupport::MessagePack 進行反序列化。這使得在序列化器之間的轉換變得很容易。

:marshal、:json_allow_marshal 和:message_pack_allow_marshal 序列化器支援使用 Marshal 進行反序列化,但其他序列化器不支援。請注意,在訊息簽署密鑰已洩露的情況下,Marshal 是反序列化攻擊的潛在媒介。如果可以的話,請選擇不支援 Marshal 的序列化器。

:message_pack 和:message_pack_allow_marshal 序列化器使用 ActiveSupport::MessagePack,它可以往返一些 JSON 不支援的 Ruby 類型,並可能提供更好的效能。但是,這些需要 msgpack gem。

在使用 Rails 時,預設值取決於 config.active_support.message_serializer。否則,預設值是:marshal。

:url_safe

預設情況下,MessageVerifier 會產生符合 RFC 4648 的字串,這些字串無法在 URL 中安全地使用。換句話說,它們可能包含「+」和「/」。如果您想要產生可以在 URL 中安全使用的字串(符合 RFC 4648 中的「使用 URL 和檔案名稱安全英數字元集的 Base 64 編碼」),您可以傳遞 true

:force_legacy_metadata_serializer

是否使用舊式資料序列化器,它會先序列化訊息,再將其封裝在一個同時也會被序列化的封套中。這是 Rails 7.0 及以下版本的預設值。

如果您沒有傳遞一個真值,預設值會使用 config.active_support.use_message_serializer_for_metadata 設定。

# File activesupport/lib/active_support/message_verifier.rb, line 165
def initialize(secret, **options)
  raise ArgumentError, "Secret should not be nil." unless secret
  super(**options)
  @secret = secret
  @digest = options[:digest]&.to_s || "SHA1"
end

實例公開方法

generate(value, **options)

為提供的 value 產生一個已簽署的訊息。

訊息會使用 MessageVerifier 的密鑰簽署。傳回 Base64 編碼訊息,連接產生的簽章。

verifier = ActiveSupport::MessageVerifier.new("secret")
verifier.generate("signed message") # => "BAhJIhNzaWduZWQgbWVzc2FnZQY6BkVU--f67d5f27c3ee0b8483cebf2103757455e947493b"

選項

:expires_at

訊息過期的日/時間。在此日/時間之後,訊息驗證將會失敗。

message = verifier.generate("hello", expires_at: Time.now.tomorrow)
verifier.verified(message) # => "hello"
# 24 hours later...
verifier.verified(message) # => nil
verifier.verify(message)   # => raises ActiveSupport::MessageVerifier::InvalidSignature
:expires_in

訊息有效期限。在此期限過後,訊息驗證將會失敗。

message = verifier.generate("hello", expires_in: 24.hours)
verifier.verified(message) # => "hello"
# 24 hours later...
verifier.verified(message) # => nil
verifier.verify(message)   # => raises ActiveSupport::MessageVerifier::InvalidSignature
:purpose

訊息的目的。如果指定,驗證訊息時必須指定相同的目的;否則,驗證將會失敗。(請參閱 verifiedverify。)

# File activesupport/lib/active_support/message_verifier.rb, line 304
def generate(value, **options)
  create_message(value, **options)
end

valid_message?(message)

檢查一個已簽署的訊息是否可以透過使用 MessageVerifier 的密鑰簽署一個物件產生。

verifier = ActiveSupport::MessageVerifier.new("secret")
signed_message = verifier.generate("signed message")
verifier.valid_message?(signed_message) # => true

tampered_message = signed_message.chop # editing the message invalidates the signature
verifier.valid_message?(tampered_message) # => false
# File activesupport/lib/active_support/message_verifier.rb, line 181
def valid_message?(message)
  !!catch_and_ignore(:invalid_message_format) { extract_encoded(message) }
end

verified(message, **options)

使用 MessageVerifier 的密鑰解碼已簽署的訊息。

verifier = ActiveSupport::MessageVerifier.new("secret")

signed_message = verifier.generate("signed message")
verifier.verified(signed_message) # => "signed message"

如果訊息不是使用相同的密鑰簽署,會傳回 nil

other_verifier = ActiveSupport::MessageVerifier.new("different_secret")
other_verifier.verified(signed_message) # => nil

如果訊息不是 Base64 編碼,會傳回 nil

invalid_message = "f--46a0120593880c733a53b6dad75b42ddc1c8996d"
verifier.verified(invalid_message) # => nil

會引發任何在解碼已簽署訊息時引發的錯誤。

incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff"
verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format

選項

:purpose

訊息用來產生的目的。如果目的不符,verified 將會回傳 nil

message = verifier.generate("hello", purpose: "greeting")
verifier.verified(message, purpose: "greeting") # => "hello"
verifier.verified(message, purpose: "chatting") # => nil
verifier.verified(message)                      # => nil

message = verifier.generate("bye")
verifier.verified(message)                      # => "bye"
verifier.verified(message, purpose: "greeting") # => nil
# File activesupport/lib/active_support/message_verifier.rb, line 222
def verified(message, **options)
  catch_and_ignore :invalid_message_format do
    catch_and_raise :invalid_message_serialization do
      catch_and_ignore :invalid_message_content do
        read_message(message, **options)
      end
    end
  end
end

verify(message, **options)

使用 MessageVerifier 的密鑰解碼已簽署的訊息。

verifier = ActiveSupport::MessageVerifier.new("secret")
signed_message = verifier.generate("signed message")

verifier.verify(signed_message) # => "signed message"

如果訊息未由相同的憑證簽名或未經 Base64 編碼,則會引發 InvalidSignature

other_verifier = ActiveSupport::MessageVerifier.new("different_secret")
other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature

選項

:purpose

訊息用來產生的目的。如果目的不符,verify 將會引發 ActiveSupport::MessageVerifier::InvalidSignature

message = verifier.generate("hello", purpose: "greeting")
verifier.verify(message, purpose: "greeting") # => "hello"
verifier.verify(message, purpose: "chatting") # => raises InvalidSignature
verifier.verify(message)                      # => raises InvalidSignature

message = verifier.generate("bye")
verifier.verify(message)                      # => "bye"
verifier.verify(message, purpose: "greeting") # => raises InvalidSignature
# File activesupport/lib/active_support/message_verifier.rb, line 260
def verify(message, **options)
  catch_and_raise :invalid_message_format, as: InvalidSignature do
    catch_and_raise :invalid_message_serialization do
      catch_and_raise :invalid_message_content, as: InvalidSignature do
        read_message(message, **options)
      end
    end
  end
end