PHP - 从长md5哈希生成短字母数字字符串的好方法是什么?

时间:2010-07-22 22:45:27

标签: php random base

这是为了拥有一个很好的短URL,它指的是数据库中的md5哈希。我想转换这样的东西:

  

a7d2cd9e0e09bebb6a520af48205ced1

这样的事情:

  

hW9lM5f27

这些都包含大约相同数量的信息。该方法不必是直接的和可逆的,但这样会很好(更灵活)。至少我希望随机生成的字符串以十六进制哈希作为种子,因此它是可重现的。我确信有很多可能的答案,我很想知道人们会如何以优雅的方式做到这一点。

哦,这不需要与原始哈希完美的1:1对应,但这将是一个奖励(我想我已经暗示了可逆性标准)。如果可能的话,我想避免碰撞。

修改 我意识到我的初步计算是完全错误的(感谢人们在这里回答,但是我花了一些时间才知道)并且你不能通过将所有小写字母和大写字母放入混合中来真正减少字符串长度。所以我想我会想要一些不能直接从hex转换为base 62的东西。

6 个答案:

答案 0 :(得分:8)

这是一个需要考虑的小功能:

/** Return 22-char compressed version of 32-char hex string (eg from PHP md5). */
function compress_md5($md5_hash_str) {
    // (we start with 32-char $md5_hash_str eg "a7d2cd9e0e09bebb6a520af48205ced1")
    $md5_bin_str = "";
    foreach (str_split($md5_hash_str, 2) as $byte_str) { // ("a7", "d2", ...)
        $md5_bin_str .= chr(hexdec($byte_str));
    }
    // ($md5_bin_str is now a 16-byte string equivalent to $md5_hash_str)
    $md5_b64_str = base64_encode($md5_bin_str);
    // (now it's a 24-char string version of $md5_hash_str eg "VUDNng4JvrtqUgr0QwXOIg==")
    $md5_b64_str = substr($md5_b64_str, 0, 22);
    // (but we know the last two chars will be ==, so drop them eg "VUDNng4JvrtqUgr0QwXOIg")
    $url_safe_str = str_replace(array("+", "/"), array("-", "_"), $md5_b64_str);
    // (Base64 includes two non-URL safe chars, so we replace them with safe ones)
    return $url_safe_str;
}

基本上,MD5哈希字符串中有16个字节的数据。它是32个字符长,因为每个字节编码为2个十六进制数字(即00-FF)。所以我们将它们分解为字节并构建一个16字节的字符串。但是因为它不再是人类可读或有效的ASCII,我们base-64将它编码回可读的字符。但由于base-64导致~4 / 3扩展(我们每8位输入仅输出6位,因此需要32位来编码24位),16字节变为22字节。但是因为base-64编码通常填充长度为4的倍数,所以我们只能获取24个字符输出的前22个字符(最后2个是填充)。然后,我们将base-64编码使用的非URL安全字符替换为URL安全等效字符。

这是完全可逆的,但这是留给读者的练习。

我认为这是你能做的最好的事情,除非你不关心人类可读/ ASCII,在这种情况下你可以直接使用$ md5_bin_str。

如果您不需要保留所有位,也可以使用此函数的结果的前缀或其他子集。抛出数据显然是缩短事情的最简单方法! (但那是不可逆的)

P.S。输入“a7d2cd9e0e09bebb6a520af48205ced1”(32个字符)时,此函数将返回“VUDNng4JvrtqUgr0QwXO0Q”(22个字符)。

答案 1 :(得分:5)

以下是Base-16到Base-64转换的两个转换函数,以及任意输入长度的Base Base-64到Base-16的转换函数:

function base16_to_base64($base16) {
    return base64_encode(pack('H*', $base16));
}
function base64_to_base16($base64) {
    return implode('', unpack('H*', base64_decode($base64)));
}

如果您需要Base-64 encoding with the URL and filename safe alphabet ,可以使用以下功能:

function base64_to_base64safe($base64) {
    return strtr($base64, '+/', '-_');
}
function base64safe_to_base64($base64safe) {
    return strtr($base64safe, '-_', '+/');
}

如果您现在想要一个函数使用URL安全字符压缩十六进制MD5值,您可以使用:

function compress_hash($hash) {
    return base64_to_base64safe(rtrim(base16_to_base64($hash), '='));
}

反函数:

function uncompress_hash($hash) {
    return base64_to_base16(base64safe_to_base64($hash));
}

答案 2 :(得分:1)

你可以做普通的base conversion。哈希以十六进制表示,然后您可以创建要表示哈希值的字母表。 Base64可以很好地用于此目的,尽管您可能希望编写自己的函数,以便最终编码值,而不是字符串。

但请注意,标准Base64包含您不希望放入URL的字符; +,/和填充字符=。您可以在来回转换时使用其他内容替换这些字符以获得URL安全的Base64编码(或者如果您编写自己的函数,则使用一组安全的字符开始)。

答案 3 :(得分:1)

我会建议反对 1-1通信:

使用base-64编码,您只能将输入减少到(4/8)/(6/8) - > 4/6~66%的大小(这假设您处理“丑陋”的base64字符而不添加任何新内容)。

我可能会考虑使用(辅助)查找方法来获得真正“漂亮”的值。建立此备用方法后,选择如何生成该范围内的值 - 例如随机数 - 可以没有源哈希值(因为无论如何都会丢失对应关系),可以使用任意“漂亮”的目标集,也许[a-z] [A-Z] [0-9]。

只需按照分频进位方法和查找数组即可转换为基数(上面的62)。这应该是有趣的小运动。

注意:如果从[0,62 ^ 5]中选择随机数,那么您将获得一个完全打包编码输出的值(并且适合32位整数值)。然后,您可以连续多次执行此过程以获得-5个结果值的良好倍数,例如xxxxxyyyyyzzzzzz(其中x,y,z是不同的组,总值在范围内(62 ^ 5)^ 3 - > 62 ^ 15 - >“巨大的价值”)

编辑,发表评论

因为没有 1-1的对应关系,你可以制作真正简短的东西 - 也许是8个字符长的“小” - 使用base62,8个字符可以存储多达218340105584896个值,其中可能比你需要的更多。甚至6个字符“仅”允许存储56800235584不同的值! (你仍然不能将这个数字存储在一个简单的32位整数中:-)如果你减少到5个字符,你再次减少空间(到不到10亿:916,132,832),但现在你有一些可以适合签名的32位整数(尽管有点浪费)。

数据库应该确保没有重复,尽管此值的索引将使用随机源“快速分段”(但您可以使用计数器或诸如此类的东西)。分布均匀的PRNG应该在足够大的范围内具有最小的冲突(读取:重试)(假设您保持种子滚动并且不重置它,或者适当地重置它) - Super 7甚至可以保证在一个周期内没有重复(只有~32k),但正如您在上面所看到的,目标空间仍然是。在最小编码大小方面,请参阅维持1-1关系所需内容的数学。

分离和携带方法只是解释了如何将源编号放入不同的基础 - 也许是基础62。可以应用相同的通用方法从“自然”基础(PHP中的base10)到任何基础。

答案 4 :(得分:1)

当然,如果我想要一个功能来完美地满足我的需求,我最好自己做。这就是我想出来的。

//takes a string input, int length and optionally a string charset
//returns a hash 'length' digits long made up of characters a-z,A-Z,0-9 or those specified by charset
function custom_hash($input, $length, $charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUFWXIZ0123456789'){
    $output = '';
    $input = md5($input); //this gives us a nice random hex string regardless of input 

    do{
        foreach (str_split($input,8) as $chunk){
            srand(hexdec($chunk));
            $output .= substr($charset, rand(0,strlen($charset)), 1);
        }
        $input = md5($input);

    } while(strlen($output) < $length);

    return substr($output,0,$length);
}

这是一个非常通用的随机字符串生成器,但它不仅仅是任何旧的随机字符串生成器,因为结果由输入字符串确定,对该输入的任何轻微更改都将产生完全不同的结果。你可以用这个来做各种事情:

custom_hash('1d34ecc818c4d50e788f0e7a9fd33662', 16); // 9FezqfFBIjbEWOdR
custom_hash('Bilbo Baggins', 5, '0123456789bcdfghjklmnpqrstvwxyz'); // lv4hb
custom_hash('', 100, '01'); 
// 1101011010110001100011111110100100101011001011010000101010010011000110000001010100111000100010101101

有人发现有任何问题或有任何改进空间吗?

答案 5 :(得分:0)

这取决于a7d2cd9e0e09bebb6a520af48205ced1是什么。假设您正在讨论十六进制数,因为它来自md5,您可以运行base64_encode。如果您有字符串形式的十六进制,则需要运行hexdec。小心你不要遇到maxint问题。