仅使用 PHP 的具有解码可能性(缩短 URL)的最短编码字符串

新手上路,请多包涵

我正在寻找一种将字符串编码为尽可能 的长度并使其可 解码 的方法(纯 PHP,无 SQL)。我有工作脚本,但我对编码字符串的长度不满意。

设想

链接到图像(这取决于我想向用户显示的文件分辨率):

编码链接(因此用户无法猜测如何获得更大的图像):

所以,基本上我只想对 URL 的搜索查询部分进行编码:

  • img=/dir/dir/hi-res-img.jpg&w=700&h=500

我现在使用的方法会将上述查询字符串编码为:

  • y8xNt9VPySwC44xM3aLUYt3M3HS9rIJ0tXJbcwMDtQxbUwMDAA

我使用的方法是:

  $raw_query_string = 'img=/dir/dir/hi-res-img.jpg&w=700&h=500';

 $encoded_query_string = base64_encode(gzdeflate($raw_query_string));
 $decoded_query_string = gzinflate(base64_decode($encoded_query_string));

如何缩短编码结果并仍然可以 使用 PHP 对其进行解码?

原文由 Artur Filipiak 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.3k
2 个回答

我怀疑如果您不希望它被用户解码,您将需要更多地考虑您的散列方法。 Base64 的问题是 Base64 字符串 看起来 像 base64 字符串。很有可能,精明的人会查看您的页面源代码,也可能会认出它。

第一部分:

一种将字符串编码为尽可能短的长度的方法

如果您的 URL 词汇/字符灵活,这将是一个很好的起点。由于 gzip 使用反向引用获得了很多收益,因此字符串太短没有什么意义。

考虑你的例子——你在压缩中只保存了 2 个字节,这些字节在 Base64 填充中再次丢失:

非 gzip 压缩: string(52) "aW1nPS9kaXIvZGlyL2hpLXJlcy1pbWcuanBnJnc9NzAwJmg9NTAw"

压缩: string(52) "y8xNt9VPySwC44xM3aLUYt3M3HS9rIJ0tXJbcwMDtQxbUwMDAA=="

如果你减少你的词汇量,这自然会让你更好的压缩。假设我们删除了一些冗余信息。

看一下功能:

 function compress($input, $ascii_offset = 38){
    $input = strtoupper($input);
    $output = '';
    //We can try for a 4:3 (8:6) compression (roughly), 24 bits for 4 characters
    foreach(str_split($input, 4) as $chunk) {
        $chunk = str_pad($chunk, 4, '=');

        $int_24 = 0;
        for($i=0; $i<4; $i++){
            //Shift the output to the left 6 bits
            $int_24 <<= 6;

            //Add the next 6 bits
            //Discard the leading ASCII chars, i.e make
            $int_24 |= (ord($chunk[$i]) - $ascii_offset) & 0b111111;
        }

        //Here we take the 4 sets of 6 apart in 3 sets of 8
        for($i=0; $i<3; $i++) {
            $output = pack('C', $int_24) . $output;
            $int_24 >>= 8;
        }
    }

    return $output;
}

function decompress($input, $ascii_offset = 38) {

    $output = '';
    foreach(str_split($input, 3) as $chunk) {

        //Reassemble the 24 bit ints from 3 bytes
        $int_24 = 0;
        foreach(unpack('C*', $chunk) as $char) {
            $int_24 <<= 8;
            $int_24 |= $char & 0b11111111;
        }

        //Expand the 24 bits to 4 sets of 6, and take their character values
        for($i = 0; $i < 4; $i++) {
            $output = chr($ascii_offset + ($int_24 & 0b111111)) . $output;
            $int_24 >>= 6;
        }
    }

    //Make lowercase again and trim off the padding.
    return strtolower(rtrim($output, '='));
}

它基本上是去除冗余信息,然后将 4 个字节压缩为 3 个字节。这是通过有效地拥有 ASCII 表的 6 位子集来实现的。此窗口已移动,因此偏移量从有用的字符开始,并包括您当前使用的所有字符。

使用我使用的偏移量,您可以使用从 ASCII 38 到 102 的任何内容。这为您提供了 30 字节 的结果字符串,即 9 字节 (24%) 压缩!不幸的是,您需要使其成为 URL 安全的(可能使用 base64),这使它回到 40 个字节。

我认为在这一点上,您可以很安全地假设您已经达到阻止 99.9% 的人所需的“通过默默无闻的安全”级别。让我们继续,你的问题的第二部分

所以用户无法猜测如何获得更大的图像

可以说这已经用上面的方法解决了,但是你需要通过服务器上的秘密来传递它,最好是使用 PHP 的 OpenSSL 接口。以下代码展示了上述函数的完整使用流程和加密:

 $method = 'AES-256-CBC';
$secret = base64_decode('tvFD4Vl6Pu2CmqdKYOhIkEQ8ZO4XA4D8CLowBpLSCvA=');
$iv = base64_decode('AVoIW0Zs2YY2zFm5fazLfg==');

$input = 'img=/dir/dir/hi-res-img.jpg&w=700&h=500';
var_dump($input);

$compressed = compress($input);
var_dump($compressed);

$encrypted = openssl_encrypt($compressed, $method, $secret, false, $iv);
var_dump($encrypted);

$decrypted = openssl_decrypt($encrypted, $method, $secret, false, $iv);
var_dump($decrypted);

$decompressed = decompress($compressed);
var_dump($decompressed);

该脚本的输出如下:

 string(39) "img=/dir/dir/hi-res-img.jpg&w=700&h=500"
string(30) "<��(��tJ��@�xH��G&(�%��%��xW"
string(44) "xozYGselci9i70cTdmpvWkrYvGN9AmA7djc5eOcFoAM="
string(30) "<��(��tJ��@�xH��G&(�%��%��xW"
string(39) "img=/dir/dir/hi-res-img.jpg&w=700&h=500"

你会看到整个循环:压缩→加密→Base64编码/解码→解密→解压。这个输出将尽可能接近你真正能得到的,接近你能得到的最短长度。

撇开一切不谈,我觉得有必要得出这样的结论:它只是理论上的,这是一个很好的思考挑战。肯定有更好的方法可以达到您想要的结果 - 我会第一个承认我的解决方案有点荒谬!

原文由 calcinai 发布,翻译遵循 CC BY-SA 4.0 许可协议

理论

理论上我们需要一个短的输入字符集和一个大的输出字符集。我将通过以下示例对其进行演示。我们将数字 2468 作为整数,使用 10 个字符 (0-9) 作为字符集。我们可以将其转换为以 2 为底数(二进制数系统)的相同数字。然后我们有一个更短的字符集(0 和 1),结果更长:100110100100

但是,如果我们转换为字符集为 16(0-9 和 AF)的十六进制数(以 16 为底)。然后我们得到一个更短的结果:9A4

实践

因此,在您的情况下,我们为输入设置了以下字符集:

 $inputCharacterSet = "0123456789abcdefghijklmnopqrstuvwxyz=/-.&";

总共 41 个字符:数字、小写字母和特殊字符 = / - 。 &

输出的字符集有点棘手。我们只想使用 URL 保存字符。我从这里抓取了它们: Characters allowed in GET parameter

所以我们的输出字符集是(73个字符):

 $outputCharacterSet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~-_.!*'(),$";

数字、大小写 一些特殊字符。

我们的输出字符集比输入字符集多。理论上说我们可以缩短输入字符串。 _检查_!

编码

现在我们需要一个从 base 41 到 base 73 的编码函数。对于那种情况,我不知道 PHP 函数。幸运的是,我们可以从这里获取函数“convBase”: Convert an arbitrarily large number from any base to any base

 <?php
function convBase($numberInput, $fromBaseInput, $toBaseInput)
{
    if ($fromBaseInput == $toBaseInput) return $numberInput;
    $fromBase = str_split($fromBaseInput, 1);
    $toBase = str_split($toBaseInput, 1);
    $number = str_split($numberInput, 1);
    $fromLen = strlen($fromBaseInput);
    $toLen = strlen($toBaseInput);
    $numberLen = strlen($numberInput);
    $retval = '';
    if ($toBaseInput == '0123456789')
    {
        $retval = 0;
        for ($i = 1;$i <= $numberLen; $i++)
            $retval = bcadd($retval, bcmul(array_search($number[$i-1], $fromBase), bcpow($fromLen, $numberLen-$i)));
        return $retval;
    }
    if ($fromBaseInput != '0123456789')
        $base10 = convBase($numberInput, $fromBaseInput, '0123456789');
    else
        $base10 = $numberInput;
    if ($base10<strlen($toBaseInput))
        return $toBase[$base10];
    while($base10 != '0')
    {
        $retval = $toBase[bcmod($base10,$toLen)] . $retval;
        $base10 = bcdiv($base10, $toLen, 0);
    }
    return $retval;
}

现在我们可以缩短 URL。最终代码是:

 $input = 'img=/dir/dir/hi-res-img.jpg&w=700&h=500';
$inputCharacterSet = "0123456789abcdefghijklmnopqrstuvwxyz=/-.&";
$outputCharacterSet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~-_.!*'(),$";
$encoded = convBase($input, $inputCharacterSet, $outputCharacterSet);
var_dump($encoded); // string(34) "BhnuhSTc7LGZv.h((Y.tG_IXIh8AR.$!t*"
$decoded = convBase($encoded, $outputCharacterSet, $inputCharacterSet);
var_dump($decoded); // string(39) "img=/dir/dir/hi-res-img.jpg&w=700&h=500"

编码后的字符串只有 34 个字符。

优化

您可以通过以下方式优化字符数

  • 减少输入字符串的长度。您真的需要 URL 参数语法的开销吗?也许您可以按如下方式格式化您的字符串:

$input = '/dir/dir/hi-res-img.jpg,700,500';

这减少了输入本身 输入字符集。您的简化输入字符集是:

$inputCharacterSet = "0123456789abcdefghijklmnopqrstuvwxyz/-.,";

最终输出:

string(27) "E$AO.Y_JVIWMQ9BB_Xb3!Th*-Ut"

string(31) "/dir/dir/hi-res-img.jpg,700,500"

  • 减少输入字符集 ;-)。也许您可以排除更多字符?您可以先将数字编码为字符。那么你的输入字符集可以减少10个!

  • 增加输出字符集。因此,我在两分钟内用谷歌搜索了给定的集合。也许你可以使用更多的 URL 保存字符。

安全

注意:代码中没有加密逻辑。所以如果有人猜到了字符集,他/她可以很容易地解码字符串。但是你可以洗牌字符集(一次)。然后对攻击者来说有点困难,但并不真正安全。也许这对你的用例来说已经足够了。

原文由 Timo 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
logo
Stack Overflow 翻译
子站问答
访问
宣传栏