класс ActiveSupport::MessageVerifier
Проверка сообщений 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
Подпись — это не шифрование
Подписанные сообщения не зашифрованы. Полезная нагрузка лишь кодируется (по умолчанию в 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, чтобы создать и добавить средство проверки: тогда при проверке verified или verify также попробуют использовать резервный вариант.
По умолчанию для всех ротируемых средств проверки используются значения основного средства проверки, если не указано иное.
Задайте для средства проверки новые значения по умолчанию:
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)
Открытые методы класса
# File activesupport/lib/active_support/message_verifier.rb, line 167 def initialize(secret, **options) raise ArgumentError, "Secret should not be nil." unless secret super(**options) @secret = secret @digest = options[:digest]&.to_s || "SHA1" end
Инициализирует новый объект MessageVerifier, используя секретный ключ для подписи.
Параметры
:digest-
Digest, используемый для подписи. По умолчанию —"SHA1". Другие варианты см. вOpenSSL::Digest. :serializer-
Сериализатор, используемый для сериализации данных сообщения. Можно указать любой объект, который отвечает на
dumpиload, или выбрать один из нескольких предварительно настроенных сериализаторов::marshal,:json_allow_marshal,:json,:message_pack_allow_marshal,:message_pack.В предварительно настроенных сериализаторах предусмотрен механизм резервного варианта для поддержки нескольких форматов десериализации. Например, сериализатор
:marshalвыполняет сериализацию с помощьюMarshal, но может выполнять десериализацию с помощьюMarshal,ActiveSupport::JSONилиActiveSupport::MessagePack. Это упрощает переход между сериализаторами.Сериализаторы
:marshal,:json_allow_marshalи:message_pack_allow_marshalподдерживают десериализацию с помощьюMarshal, а остальные — нет. Учтите, чтоMarshalможет стать вектором атак с использованием десериализации, если секрет для подписи сообщения был раскрыт. По возможности выбирайте сериализатор, который не поддерживаетMarshal.Сериализаторы
:message_packи:message_pack_allow_marshalиспользуютActiveSupport::MessagePack, который позволяет сериализовать и десериализовать некоторые типы Ruby, не поддерживаемыеJSON, и может обеспечивать более высокую производительность. Однако для этого требуется гемmsgpack.При использовании Rails значение по умолчанию зависит от
config.active_support.message_serializer. В противном случае используется:marshal. :url_safe-
По умолчанию
MessageVerifierсоздаёт строки, соответствующие RFC 4648, но небезопасные для URL. Иными словами, они могут содержать символы «+» и «/». Чтобы создавать строки, безопасные для URL (в соответствии с «Кодированием Base 64 с алфавитом, безопасным для URL и имён файлов» в RFC 4648), передайтеtrue. Обратите внимание, чтоMessageVerifierвсегда принимает как безопасные, так и небезопасные для URL кодированные сообщения, что позволяет плавно перейти с одного параметра на другой. :force_legacy_metadata_serializer-
Использовать ли устаревший сериализатор метаданных, который сначала сериализует сообщение, а затем помещает его в оболочку, которая также сериализуется. Этот параметр использовался по умолчанию в Rails 7.0 и более ранних версиях.
Если не передать истинное значение, значение по умолчанию задаётся с помощью
config.active_support.use_message_serializer_for_metadata.
Открытые методы экземпляра
# File activesupport/lib/active_support/message_verifier.rb, line 306 def generate(value, **options) create_message(value, **options) end
Создаёт подписанное сообщение для указанного значения.
Сообщение подписывается секретным ключом 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-
Цель сообщения. Если она указана, при проверке сообщения необходимо указать ту же цель; в противном случае проверка завершится неудачей. (См.
verifiedиverify.)
# File activesupport/lib/active_support/message_verifier.rb, line 183
def valid_message?(message)
!!catch_and_ignore(:invalid_message_format) { extract_encoded(message) }
end Проверяет, могло ли подписанное сообщение быть создано путём подписания объекта секретным ключом 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 224
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 Декодирует подписанное сообщение с помощью секретного ключа 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
Возвращает nil, если сообщение не закодировано в Base64.
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 262
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 Декодирует подписанное сообщение с помощью секретного ключа MessageVerifier.
verifier = ActiveSupport::MessageVerifier.new("secret")
signed_message = verifier.generate("signed message")
verifier.verify(signed_message) # => "signed message"
Вызывает исключение InvalidSignature, если сообщение было подписано другим секретным ключом или не закодировано в Base64.
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
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.