在Ruby中创建一个数字、字符串、数组或哈希的md5哈希

49
我需要在Ruby中为一个变量创建一个签名字符串,该变量可以是数字、字符串、哈希或数组。哈希值和数组元素也可以是其中的任何一种类型。这个字符串将用于比较数据库(在这种情况下是Mongo)中的值。我的第一个想法是创建一个JSON编码值的MD5哈希,如下所示:(上面提到的变量是body)。
def createsig(body)    
  Digest::MD5.hexdigest(JSON.generate(body))
end

这个方法几乎可行,但是JSON.generate不会每次以相同的顺序编码哈希的键,因此 createsig({:a=>'a',:b=>'b'}) 并不总是等于 createsig({:b=>'b',:a=>'a'})

有什么最好的方法可以创建一个符合这个需求的签名字符串呢?

注意:对于那些注重细节的人,我知道你不能 JSON.generate()一个数字或字符串。在这些情况下,我会直接调用 MD5.hexdigest()


3
如果这将被用于任何安全目的,请不要使用MD5。 - Alan
2
它并不用于安全目的,而是通过字符串表示形式进行简单比较。我不需要md5,但这是我能想到的最接近的东西。 - TelegramSam
1
你需要这些值在单个进程内还是跨进程保持一致吗?如果你不需要它们在进程间保持一致,你可以使用 x.hash(或 x.hashx.class 的组合)。 - mu is too short
1
如问题所述,我将把这些值存储在数据库中进行比较。我需要它们在进程之间可移植。比较需要基于变量的值,而不是特定的变量本身。 - TelegramSam
只是为了扩展Alan的评论,为了安全起见,请使用bcrypt。一种具有时间成本的单向哈希,以防止暴力攻击。 - superluminary
只是想指出,在 Ruby 1.9.3+ 中,这不应该是一个问题。请参见:http://stackoverflow.com/questions/31850741/order-of-keys-in-a-json-object-converted-to-a-ruby-hash-with-json-parse?rq=1 - Joe Edgar
6个回答

34

我很快编写了以下代码,并没有时间在工作中进行真正的测试,但应该可以完成任务。如果您发现任何问题,请告诉我,我会看一下。

这个代码片段应该正确地展开和排序数组和哈希表,除非您有一些非常奇怪的字符串,否则不会有任何冲突。

def createsig(body)
  Digest::MD5.hexdigest( sigflat body )
end

def sigflat(body)
  if body.class == Hash
    arr = []
    body.each do |key, value|
      arr << "#{sigflat key}=>#{sigflat value}"
    end
    body = arr
  end
  if body.class == Array
    str = ''
    body.map! do |value|
      sigflat value
    end.sort!.each do |value|
      str << value
    end
  end
  if body.class != String
    body = body.to_s << body.class.to_s
  end
  body
end

> sigflat({:a => {:b => 'b', :c => 'c'}, :d => 'd'}) == sigflat({:d => 'd', :a => {:c => 'c', :b => 'b'}})
=> true

字符串不相等: ruby-1.9.2-p180 :001 > a = {:aa=>"aa",:bb=>"bb"} => {:aa=>"aa", :bb=>"bb"} ruby-1.9.2-p180 :002 > b = {:bb=>"bb",:aa=>"aa"} => {:bb=>"bb", :aa=>"aa"} ruby-1.9.2-p180 :003 > a.inspect => "{:aa=>"aa", :bb=>"bb"}" ruby-1.9.2-p180 :004 > b.inspect => "{:bb=>"bb", :aa=>"aa"}" - TelegramSam
修改了答案以解决排序问题。如果您能想到任何漏洞,请告诉我。 - Luke
我明白你的意思。我会尽力想出解决方案并编辑我的答案。 - Luke
@TelegramSam 这看起来怎么样? - Luke
4
警告:这会改变原始对象图。 - Joel
显示剩余2条评论

16

如果你只能获得一个body的字符串表示,而不是让Ruby 1.8哈希在每次返回时具有不同的顺序,那么你可以可靠地哈希该字符串表示。 让我们用一些猴子补丁来解决这个问题:

require 'digest/md5'

class Object
  def md5key
    to_s
  end
end

class Array
  def md5key
    map(&:md5key).join
  end
end

class Hash
  def md5key
    sort.map(&:md5key).join
  end
end

现在任何一个(在问题中提到的类型)对象都会通过返回可靠的键来响应md5key,因此:

def createsig(o)
  Digest::MD5.hexdigest(o.md5key)
end

示例:

body = [
  {
    'bar' => [
      345,
      "baz",
    ],
    'qux' => 7,
  },
  "foo",
  123,
]
p body.md5key        # => "bar345bazqux7foo123"
p createsig(body)    # => "3a92036374de88118faf19483fe2572e"

注意:此哈希表示不编码结构,仅编码值的串联。因此 ["a", "b", "c"] 的哈希与 ["abc"] 相同。


1
“只是我的两分钱意见:”
module Ext
  module Hash
    module InstanceMethods
      # Return a string suitable for generating content signature.
      # Signature image does not depend on order of keys.
      #
      #   {:a => 1, :b => 2}.signature_image == {:b => 2, :a => 1}.signature_image                  # => true
      #   {{:a => 1, :b => 2} => 3}.signature_image == {{:b => 2, :a => 1} => 3}.signature_image    # => true
      #   etc.
      #
      # NOTE: Signature images of identical content generated under different versions of Ruby are NOT GUARANTEED to be identical.
      def signature_image
        # Store normalized key-value pairs here.
        ar = []

        each do |k, v|
          ar << [
            k.is_a?(::Hash) ? k.signature_image : [k.class.to_s, k.inspect].join(":"),
            v.is_a?(::Hash) ? v.signature_image : [v.class.to_s, v.inspect].join(":"),
          ]
        end

        ar.sort.inspect
      end
    end
  end
end

class Hash    #:nodoc:
  include Ext::Hash::InstanceMethods
end

1
这是我的解决方案。我遍历数据结构并建立一个被连接成单个字符串的片段列表。为了确保所看到的类类型影响哈希值,我注入了一个单个的Unicode字符,其中包含基本类型信息。(例如,我们想要 ["1", "2", "3"].objsum != [1,2,3].objsum)
我将其作为Object的完善措施,很容易移植到猴子补丁中。要使用它,只需require该文件并运行“using ObjSum”。
module ObjSum
  refine Object do
    def objsum
      parts = []
      queue = [self]

      while queue.size > 0
        item = queue.shift

        if item.kind_of?(Hash)
          parts << "\\000"
          item.keys.sort.each do |k| 
            queue << k
            queue << item[k]
          end
        elsif item.kind_of?(Set)
          parts << "\\001"
          item.to_a.sort.each { |i| queue << i }
        elsif item.kind_of?(Enumerable)
          parts << "\\002"
          item.each { |i| queue << i }
        elsif item.kind_of?(Fixnum)
          parts << "\\003"
          parts << item.to_s
        elsif item.kind_of?(Float)
          parts << "\\004"
          parts << item.to_s
        else
          parts << item.to_s
        end
      end

      Digest::MD5.hexdigest(parts.join)
    end
  end
end

1

-1
根据您的需求,您可以调用ary.inspectary.to_yaml

3
如上所述,inspect方法不能可靠地按散列键排序。to_yaml方法也是一样的。 - TelegramSam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接