MessageVerifierは、RailsのActiveSupportライブラリに含まれるクラスで、データの署名と検証を行うために使用されます。これにより、データが改ざんされていないことを保証できます。以下に、ActiveSupport::MessageVerifierの実装を簡単に説明します。
1
2
3
4
5
6
7
8
| verifier = Rails.application.message_verifier(:hoge)
token = verifier.generate('aiueo', expires_in: 1.hour)
=> "eyJfcmF...--bead..."
verifier.verify(token)
=> "aiueo"
verifier.verify('kakikukeko')
=> mismatched digest (ActiveSupport::MessageVerifier::InvalidSignature)
|
これがどういう仕組みなのか、実装を追いかけてみます。
なおrails8.1.2時点のコードを参照しています。
コードリーディング#
Rails.application.message_verifier#
Rails.application.message_verifier(:hoge)からたどります。
1
2
3
4
5
6
7
8
9
10
| def message_verifier(verifier_name)
message_verifiers[verifier_name] # :hogeを渡す
end
def message_verifiers
@message_verifiers ||=
ActiveSupport::MessageVerifiers.new do |salt, secret_key_base: self.secret_key_base|
key_generator(secret_key_base).generate_key(salt)
end.rotate_defaults
end
|
https://github.com/rails/rails/blob/v8.1.2/railties/lib/rails/application.rb#L238-L240
https://github.com/rails/rails/blob/v8.1.2/railties/lib/rails/application.rb#L210-L215
1
2
3
4
5
6
7
8
9
10
11
| def initialize(&secret_generator)
raise ArgumentError, "A secret generator block is required" unless secret_generator
@secret_generator = secret_generator
@rotate_options = []
@on_rotation = nil
@codecs = {}
end
def [](salt)
@codecs[salt] ||= build_with_rotations(salt) # :hogeをsaltに渡す
end
|
https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/messages/rotation_coordinator.rb#L10-L16
https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/messages/rotation_coordinator.rb#L18-L20
1
2
3
4
5
| def build(salt, secret_generator:, secret_generator_options:, **options)
# ここでsalt=:hogeとsecret_key_baseが渡る
# secret_generaterorはRails.application.message_verifiersのブロック
MessageVerifier.new(secret_generator.call(salt, **secret_generator_options), **options)
end
|
https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/message_verifiers.rb#L185-L187
1
2
3
4
5
| def key_generator(secret_key_base = self.secret_key_base)
@key_generators[secret_key_base] ||= ActiveSupport::CachingKeyGenerator.new(
ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
)
end
|
https://github.com/rails/rails/blob/v8.1.2/railties/lib/rails/application.rb#L174-L180
1
2
3
4
| def generate_key(salt, key_size = 64)
# 1000回ハッシュをかけて鍵を生成するため少し重めな処理
OpenSSL::PKCS5.pbkdf2_hmac(@secret, salt, @iterations, key_size, @hash_digest_class.new)
end
|
https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/key_generator.rb#L41-L43
ここまでで、verifier = Rails.application.message_verifier(:hoge)が完了し、MessageVerifierインスタンスが生成されました。
この時点で、以下のことが行われています。
- secret_key_baseから鍵が生成されている
- saltに:hogeが設定されている
- pbkdf2_hmacで複数回ハッシュ化された鍵が生成されている
generate#
次に、verifier.generate('aiueo')を見ていきます。
1
2
3
4
5
6
7
| def generate(value, **options)
create_message(value, **options)
end
def create_message(value, **options) # :nodoc:
sign_encoded(encode(serialize_with_metadata(value, **options)))
end
|
https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/message_verifier.rb#L306-L312
1
2
3
| def encode(data, url_safe: @url_safe)
url_safe ? ::Base64.urlsafe_encode64(data, padding: false) : ::Base64.strict_encode64(data)
end
|
https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/messages/codec.rb#L25-L27
1
2
3
4
5
6
7
8
| def sign_encoded(encoded)
digest = generate_digest(encoded)
encoded << SEPARATOR << digest
end
def generate_digest(data)
OpenSSL::HMAC.hexdigest(@digest, @secret, data)
end
|
https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/message_verifier.rb#L330-L333
https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/message_verifier.rb#L352C1-L354
digestのデフォルトはSHA1です。
secretはOpenSSL::PKCS5.pbkdf2_hmac(@secret, salt, @iterations, key_size, @hash_digest_class.new)で生成した派生鍵です。
最終的に、BASE64エンコードされたデータとHMAC署名が--で連結されて返されます。
前半部分はデコード可能なので、ユーザーが簡単に知ることができます。
が、HMAC署名はsecret_key_baseとソルトを知らないと生成できないため、データの改ざんを防止できます。
まとめ#
- インスタンス生成時に、secret_key_baseとソルトから派生鍵を生成している
- generate時に、データをBASE64エンコードし、HMAC署名を付与している
- データストアを使用していないので、ステートレス
おまけ#
冒頭のコードはこんな形でdecodeできます。
1
2
3
4
5
| verifier = Rails.application.message_verifier(:hoge)
token = verifier.generate('aiueo', expires_in: 1.hour)
=> "eyJfcmF...--bead..."
::Base64.strict_decode64("eyJfcmFpbHMiOnsiZGF0YSI6ImFpdWVvIiwiZXhwIjoiMjAyNi0wMi0wN1QwODo0NDozMS44NzhaIn19")
=> "{\"_rails\":{\"data\":\"aiueo\",\"exp\":\"2026-02-07T08:44:31.878Z\"}}"
|