跳至内容 跳至搜索

Active Support 消息验证器

MessageVerifier 使生成和验证经过签名的消息变得容易,以防止篡改。

在 Rails 应用程序中,您可以使用 Rails.application.message_verifier 来管理每个用例的验证器实例。 了解更多.

这对于诸如记住我的令牌和自动取消订阅链接之类的用例很有用,在这些用例中,会话存储不适合或不可用。

首先,生成一个已签名的消息

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

将消息限制在特定用途

不建议在应用程序中将相同的验证器用于不同的目的。这样做可能会允许恶意行为者重新使用已签名的消息来执行未经授权的操作。您可以通过将已签名的消息限制在特定的 :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 进行序列化,但可以使用 MarshalActiveSupport::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 153
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)

为提供的值生成一个签名消息。

该消息使用 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 292
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 169
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 210
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 248
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