仅使用PHP实现最短的可解码字符串(缩短URL)

22

我正在寻找一种将字符串编码为最短长度并允许其进行解码的方法(仅使用纯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 进行解码?


6
我可以进行翻译。请问您需要的是什么语言到中文的翻译呢?后面需要翻译的内容是:“我会咬人:你为什么想这样做?” - PeeHaa
11
看起来像是自己制作的“安全性靠混淆”的东西。不要这么做。这样做毫无意义并且是一条死路。 - Marcin Orlowski
2
为什么你要阻止用户获取高分辨率图像? - th3falc0n
2
如果您希望用户购买高分辨率图像,则不要在网页中显示它们...请显示较低分辨率的图像和/或给显示的图像加上水印。 - Mark Baker
3
当您在网站上展示一张图片时,当用户打开该页面时,图片将下载到他们的电脑上... 如果您展示的是高分辨率的图片,那么他们现在拥有了这张图片... 这与您如何混淆链接没有关系。 - Mark Baker
显示剩余9条评论
13个回答

20

如果您不希望用户能够解码,那么我怀疑您需要更多地考虑您的哈希方法。问题在于Base64字符串看起来像一个Base64字符串。很可能有些聪明的用户会查看您的页面源代码并且认出它。

第一部分:

一种将字符串编码为最短长度的方法。

如果您可以灵活使用URL词汇和字符,这将是一个不错的起点。由于gzip利用回溯引用实现了很多的优化,在字符串很短的情况下使用gzip是没有意义的。

考虑您的示例-您仅在压缩中节省了2个字节,这些字节在Base64填充中又被浪费掉了:

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

gzip压缩: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编码/解码→解密→解压缩。 输出将尽可能地接近您真正能够获得的结果,并且长度尽可能短。

总之,我觉得有必要指出这仅是理论性的,这是一个不错的思考挑战。肯定有更好的方法来实现您想要的结果 - 我会首先承认我的解决方案有点荒谬!


2
谢谢您对这个问题进行了一些解释。这让我更好地理解了整个事情。 - Artur Filipiak

5

理论

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

100110100100

但是,如果我们采用16进制数字(基数为16)并且使用16个字符(0-9和A-F)作为字符集,那么我们得到的结果就更短了:

9A4

实践

因此,在你的情况下,我们的输入字符集如下:

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

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

输出的字符集有点棘手。我们只想使用URL安全的字符。我从这里抓取了它们:GET参数中允许的字符

因此,我们的输出字符集为(73个字符):

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

数字、小写字母和大写字母以及一些特殊字符。

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

编码

现在我们需要一个将基数41转换为基数73的编码函数。对于这种情况,我不知道PHP函数。幸运的是,我们可以从这里获取函数'convBase': 将任意大的数字从任意进制转换到任意进制

<?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安全字符。

安全性

注意:代码中没有密码逻辑。因此,如果有人猜测字符集,他/她可以轻松地解码字符串。但是您可以对字符集进行洗牌(一次)。然后对于攻击者来说会稍微困难一些,但并不真正安全。也许对于您的用例来说这已经足够了。


4
不要对URL进行编码,输出原始图像的缩略图副本。以下是我的想法:
1. 通过使用随机字符为图片(实际文件名)命名(文件名示例:从`bin2hex(random_bytes(6))`得到),创建PHP“映射”。此处建议使用random_bytes。 2. 在#1中生成的随机URL字符串中嵌入所需的分辨率。 3. 使用imagecopyresampled函数将原始图像复制到所需的分辨率,然后再将其输出到客户端设备。
例如: 1. 文件名示例(来自`bin2hex(random_bytes(6))`):a1492fdbdcf2.jpg 2. 所需分辨率:800x600。我的新链接可能看起来像:`http://myserver.com/?800a1492fdbdcf2600`或者`http://myserfer.com/?a1492800fdbdc600f2`或者`http://myserver.com/?800a1492fdbdcf2=600`,具体取决于我选择在链接中嵌入分辨率的位置。 3. PHP将知道文件名是a1492fdbdcf2.jpg,获取它,使用imagecopyresampled将其复制到所需的分辨率,然后输出它。

谢谢。不幸的是,重命名文件不是一个选项。 - Artur Filipiak
整个路径怎么样?因为您只考虑加密文件名。 - Artur Filipiak

3

根据之前的回答和下方评论,您需要一个解决方案来隐藏图像解析器的真实路径,并给它一个固定的图像宽度。

步骤1:http://www.example.com/tn/full/animals/images/lion.jpg

您可以通过利用.htaccess实现基本的“缩略图生成器”。

RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule tn/(full|small)/(.*) index.php?size=$1&img=$2 [QSA,L]

你的 PHP 文件:

 $basedir = "/public/content/";
 $filename = realpath($basedir.$_GET["img"]);

 ## Check that file is in $basedir
 if ((!strncmp($filename, $basedir, strlen($basedir))
    ||(!file_exists($filename)) die("Bad file path");

 switch ($_GET["size"]) {
    case "full":
        $width = 700;
        $height = 500;
        ## You can also use getimagesize() to test if the image is landscape or portrait
    break;
    default:
        $width = 350;
        $height = 250;
    break;
 }
 ## Here is your old code for resizing images.
 ## Note that the "tn" directory can exist and store the actual reduced images

这样可以使用URL www.example.com/tn/full/animals/images/lion.jpg 查看您缩小后的图像。

这对于SEO有优势,可以保留原始文件名。

步骤2:http://www.example.com/tn/full/lion.jpg

如果您想要更短的URL,并且图片数量不是太多,您可以使用文件的基本名称(例如“lion.jpg”)并进行递归搜索。当出现冲突时,请使用索引来标识您想要的内容(例如“1--lion.jpg”)。

function matching_files($filename, $base) {
    $directory_iterator = new RecursiveDirectoryIterator($base);
    $iterator       = new RecursiveIteratorIterator($directory_iterator);
    $regex_iterator = new RegexIterator($iterator, "#$filename\$#");
    $regex_iterator->setFlags(RegexIterator::USE_KEY);
    return array_map(create_function('$a', 'return $a->getpathName();'), iterator_to_array($regex_iterator, false));
}

function encode_name($filename) {
    $files = matching_files(basename($filename), realpath('public/content'));
    $tot = count($files);
    if (!$tot)
        return NULL;
    if ($tot == 1)
        return $filename;
    return "/tn/full/" . array_search(realpath($filename), $files) . "--" . basename($filename);
}

function decode_name($filename) {
    $i = 0;
    if (preg_match("#^([0-9]+)--(.*)#", $filename, $out)) {
        $i = $out[1];
        $filename = $out[2];
    }

    $files = matching_files($filename, realpath('public/content'));

    return $files ? $files[$i] : NULL;
}

echo $name = encode_name("gallery/animals/images/lion.jp‌​g").PHP_EOL;
 ## --> returns lion.jpg
 ## You can use with the above solution the URL http://www.example.com/tn/lion.jpg

 echo decode_name(basename($name)).PHP_EOL;
 ## -> returns the full path on disk to the image "lion.jpg"

原帖:

基本上,如果在你的示例中添加一些格式,你的缩短URL实际上会更长:

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

y8xNt9VPySwC44xM3aLUYt3M3HS9rIJ0tXJbcwMDtQxbUwMDAA // 50 characters

使用base64_encode总是会导致更长的字符串。而gzcompress至少需要存储一个不同字符的出现次数;这对于短字符串来说并不是一个好的解决方案。
因此,如果你想缩短你之前得到的结果,那么什么也不做(或仅使用一个简单的str_rot13)显然是首选考虑的选项。
你也可以使用一个简单的字符替换方法来完成:
 $raw_query_string = 'img=/dir/dir/hi-res-img.jpg&w=700&h=500';
 $from = "0123456789abcdefghijklmnopqrstuvwxyz&=/ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 // The following line if the result of str_shuffle($from)
 $to = "0IQFwAKU1JT8BM5npNEdi/DvZmXuflPVYChyrL4R7xc&SoG3Hq6ks=e9jW2abtOzg";
 echo strtr($raw_query_string, $from, $to) . "\n";

 // Result: EDpL4MEu4MEu4NE-u5f-EDp.dmprYLU00rNLA00 // 39 characters

根据您的评论,您真正想要“防止任何人获取高分辨率图像”。

最好的方法是使用私钥生成校验和。

编码:

$secret = "ujoo4Dae";
$raw_query_string = 'img=/dir/dir/hi-res-img.jpg&w=700&h=500';
$encoded_query_string = $raw_query_string . "&k=" . hash("crc32", $raw_query_string . $secret);

结果:img=/dir/dir/hi-res-img.jpg&w=700&h=500&k=2ae31804

解码:

if (preg_match("#(.*)&k=([^=]*)$#", $encoded_query_string, $out)
    && (hash("crc32", $out[1].$secret) == $out[2])) {
    $decoded_query_string = $out[1];
}

这并不隐藏原始路径,但是该路径没有必要公开。一旦检查过密钥,您的 "index.php" 可以从本地目录输出图像。

如果您真的想缩短原始 URL,则必须考虑限制原始 URL 中可接受的字符。许多压缩方法基于您可以使用一个完整字节来存储多个字符的事实。


该路径在网站上不是公开的。我已经设置了URL,以便它们看起来很好,并且对SEO友好:www.mysite.com/gallery/animals/lion.jpg,而真实路径是:/public/content/gallery/animals/images/lion.jpg。它在后端通过动态加载:index.php?img=/public/content/gallery/animals/images/lion.jpg&w=700&h=500。你只能通过打开dev工具或分享图片才能看到这个链接。缩短URL在“分享”时是必要的,例如:“在Facebook上分享此图像”等等。因此,我不太喜欢它以查询字符串格式显示。谢谢您的回答。 - Artur Filipiak
我编辑了我的答案,添加了另一种方法:使用.htaccess获取较短的URL,然后使用递归搜索获取“更短”的URL。 - Adam

2

我认为最好的做法是不要进行任何遮挡。您可以很简单地缓存返回的图像并使用处理程序来提供它们。这需要将图像大小硬编码到PHP脚本中。当您获得新的大小时,只需删除缓存中的所有内容,因为它是“懒加载”的。

1. 从请求中获取图像 可以使用以下方式: /thumbnail.php?image=img.jpg&album=myalbum。甚至可以使用重写使其成为任何东西,并具有类似于:/gallery/images/myalbum/img.jpg 的URL。

2. 检查临时版本是否不存在

您可以使用is_file()来进行检查。

3. 如果不存在则创建它

使用您当前的调整大小逻辑来完成它,但不要输出图像。将其保存到临时位置。

4. 将临时文件内容读入流中

它基本上只是输出它。

这是一个未经测试的代码示例...

<?php
    // Assuming we have a request /thumbnail.php?image=img.jpg&album=myalbum

    // These are temporary filenames places. You need to do this yourself on your system.
    $image = $_GET['image'];           // The file name
    $album = $_GET['album'];           // The album
    $temp_folder = sys_get_temp_dir(); // Temporary directory to store images
                                       // (this should really be a specific cache path)
    $image_gallery = "images";         // Root path to the image gallery

    $width = 700;
    $height = 500;

    $real_path = "$image_gallery/$album/$image";
    $temp_path = "$temp_folder/$album/$image";

    if(!is_file($temp_path))
    {
        // Read in the image
        $contents = file_get_contents($real_path);

        // Resize however you are doing it now.
        $thumb_contents = resizeImage($contents, $width, $height);

        // Write to the temporary file
        file_put_contents($temp_path, $thumb_contents);
    }

    $type = 'image/jpeg';
    header('Content-Type:' . $type);
    header('Content-Length: ' . filesize($temp_path));
    readfile($temp_path);
?>

“这样做可能更好,因为根本不需要隐藏任何东西。” 是的,你说得对。我以前用SQL完成了整个应用程序(基于数据库)。但是现在我需要一切都可以即插即用。支持那些无法处理简单数据库配置的用户非常痛苦。我收到的超过30%的工单都涉及SQL问题。我失去了客户,因为他们期望应用程序“开箱即用”,即使他们不知道自己的数据库密码是什么...不再依赖用户的编程知识。但是我必须给他们一些保证他们的图像安全的东西。不知怎么办。我会看看你的解决方案,谢谢! - Artur Filipiak
其实这个想法不错。我可以在管理员面板中创建一个“触发器”,让用户随时可以简单地重新缓存所有图片。 - Artur Filipiak
@ArturFilipiak,这基本上就是要点。它还可以节省CPU时间,因为图像只需要缓存一次。这正是WordPress和其他CMS的做法。您还可以添加一些额外的标头,以允许图像在客户端缓存 - 尤其是如果您采用重写路由 - 因为路径看起来像真正的静态图像。 - Michael Coxon
Content-Type: 后面不是缺少了一个空格吗?这里有个例子 - Peter Mortensen

2

关于"安全"的简短说明

如果没有存储“秘密密码”,那么您将无法保护您的链接:只要URI携带所有访问资源的信息,它就可以被解码,您的“自定义安全性”(顺便说一下,它们是相反的词)很容易被破坏。

您仍然可以在PHP代码中放置一个盐(例如$mysalt="....long random string..."),因为我怀疑您不需要永久的安全性(这种方法很弱,因为您无法更新$mysalt值,但在您的情况下,几年的安全性听起来足够了,因为无论如何,用户都可以购买一张图片并在其他地方分享它,从而破坏任何您的安全机制)。

如果您想拥有一个安全的机制,请使用一个众所周知的机制(例如框架),以及身份验证和用户权限管理机制(这样您就可以知道谁在寻找您的图像,以及他们是否被允许)。

安全性是有成本的。如果您不想承担其计算和存储要求,那就忘了它吧。


通过签署URL来保证安全

如果您想避免用户轻松绕过并获取完整分辨率的图片,则可以仅签署URI(但是为了安全起见,请使用已经存在的东西,而不是下面的快速草案示例):

$salt = '....long random stirng...';
$params = array('img' => '...', 'h' => '...', 'w' => '...');
$p = http_build_query($params);
$check = password_hash($p, PASSWORD_BCRYPT, array('salt' => $salt, 'cost' => 1000);
$uri = http_build_query(array_merge($params, 'sig' => $check));

解码:

$sig = $_GET['sig'];
$params = $_GET;
unset($params['sig']);

// Same as previous
$salt = '....long random stirng...';
$p = http_build_query($params);
$check = password_hash($p, PASSWORD_BCRYPT, array('salt' => $salt, 'cost' => 1000);
if ($sig !== $check) throw new DomainException('Invalid signature');

请参见 password_hash


智能缩短链接

使用通用压缩算法进行“缩短”是无效的,因为标头比URI更长,所以它几乎永远不会缩短。

如果想要缩短链接,请聪明地做:如果相对路径(/dir/dir)始终相同,则不要提供它(或仅在非主要路径上给出)。如果扩展名始终相同(或当几乎所有内容都是 png 时),则不要提供扩展名。不要提供 height,因为图像带有 aspect ratio:您只需要 width。如果不需要像素精确的宽度,请使用 x100px 进行表示。


2
很多人认为编码对安全没有帮助,所以我只集中在缩短和美观方面。您可以将其视为三个单独的组件,而不是字符串。然后,如果您限制每个组件的代码空间,就可以将它们打包得更小。例如:
- 路径 - 仅由26个字符(a-z)和/-.(可变长度) - 宽度 - 整数(0-65k)(固定长度,16位) - 高度 - 整数(0-65k)(固定长度,16位)
我将路径限制为最多31个字符,因此我们可以使用五位分组。首先打包您的固定长度尺寸,然后将每个路径字符附加为五位。可能还需要添加特殊的空字符来填充末字节。显然,您需要使用相同的字典字符串进行编码和解码。请参见下面的代码。
这表明,通过限制编码内容和编码量,您可以获得更短的字符串。您甚至可以通过仅使用12位维度整数(最大2048),或者删除已知的路径部分(如基本路径或文件扩展名)来使其更短(请参见最后一个示例)。
<?php

function encodeImageAndDimensions($path, $width, $height) {
    $dictionary = str_split("abcdefghijklmnopqrstuvwxyz/-."); // Maximum 31 characters, please

    if ($width >= pow(2, 16)) {
        throw new Exception("Width value is too high to encode with 16 bits");
    }
    if ($height >= pow(2, 16)) {
        throw new Exception("Height value is too high to encode with 16 bits");
    }

    // Pack width, then height first
    $packed = pack("nn", $width, $height);

    $path_bits = "";
    foreach (str_split($path) as $ch) {
        $index = array_search($ch, $dictionary, true);
        if ($index === false) {
            throw new Exception("Cannot encode character outside of the allowed dictionary");
        }

        $index++; // Add 1 due to index 0 meaning NULL rather than a.

        // Work with a bit string here rather than using complicated binary bit shift operators.
        $path_bits .= str_pad(base_convert($index, 10, 2), 5, "0", STR_PAD_LEFT);
    }

    // Remaining space left?
    $modulo = (8 - (strlen($path_bits) % 8)) %8;

    if ($modulo >= 5) {
        // There is space for a null character to fill up to the next byte
        $path_bits .= "00000";
        $modulo -= 5;
    }

    // Pad with zeros
    $path_bits .= str_repeat("0", $modulo);

    // Split in to nibbles and pack as a hex string
    $path_bits = str_split($path_bits, 4);
    $hex_string = implode("", array_map(function($bit_string) {
        return base_convert($bit_string, 2, 16);
    }, $path_bits));
    $packed .= pack('H*', $hex_string);

    return base64_url_encode($packed);
}

function decodeImageAndDimensions($str) {
    $dictionary = str_split("abcdefghijklmnopqrstuvwxyz/-.");

    $data = base64_url_decode($str);

    $decoded = unpack("nwidth/nheight/H*path", $data);

    $path_bit_stream = implode("", array_map(function($nibble) {
        return str_pad(base_convert($nibble, 16, 2), 4, "0", STR_PAD_LEFT);
    }, str_split($decoded['path'])));

    $five_pieces = str_split($path_bit_stream, 5);

    $real_path_indexes = array_map(function($code) {
        return base_convert($code, 2, 10) - 1;
    }, $five_pieces);

    $real_path = "";
    foreach ($real_path_indexes as $index) {
        if ($index == -1) {
            break;
        }
        $real_path .= $dictionary[$index];
    }

    $decoded['path'] = $real_path;

    return $decoded;
}

// These do a bit of magic to get rid of the double equals sign and obfuscate a bit.  It could save an extra byte.
function base64_url_encode($input) {
    $trans = array('+' => '-', '/' => ':', '*' => '$', '=' => 'B', 'B' => '!');
    return strtr(str_replace('==', '*', base64_encode($input)), $trans);
}
function base64_url_decode($input) {
    $trans = array('-' => '+', ':' => '/', '$' => '*', 'B' => '=', '!' => 'B');
    return base64_decode(str_replace('*', '==', strtr($input, $trans)));
}

// Example usage

$encoded = encodeImageAndDimensions("/dir/dir/hi-res-img.jpg", 700, 500);
var_dump($encoded); // string(27) "Arw!9NkTLZEy2hPJFnxLT9VA4A$"
$decoded = decodeImageAndDimensions($encoded);
var_dump($decoded); // array(3) { ["width"] => int(700) ["height"] => int(500) ["path"] => string(23) "/dir/dir/hi-res-img.jpg" }

$encoded = encodeImageAndDimensions("/another/example/image.png", 4500, 2500);
var_dump($encoded); // string(28) "EZQJxNhc-iCy2XAWwYXaWhOXsHHA"
$decoded = decodeImageAndDimensions($encoded);
var_dump($decoded); // array(3) { ["width"] => int(4500) ["height"] => int(2500) ["path"] => string(26) "/another/example/image.png" }

$encoded = encodeImageAndDimensions("/short/eg.png", 300, 200);
var_dump($encoded); // string(19) "ASwAyNzQ-VNlP2DjgA$"
$decoded = decodeImageAndDimensions($encoded);
var_dump($decoded); // array(3) { ["width"] => int(300) ["height"] => int(200) ["path"] => string(13) "/short/eg.png" }

$encoded = encodeImageAndDimensions("/very/very/very/very/very-hyper/long/example.png", 300, 200);
var_dump($encoded); // string(47) "ASwAyN2LLO7FlndiyzuxZZ3Yss8Rm!ZbY9x9lwFsGF7!xw$"
$decoded = decodeImageAndDimensions($encoded);
var_dump($decoded); // array(3) { ["width"] => int(300) ["height"] => int(200) ["path"] => string(48) "/very/very/very/very/very-hyper/long/example.png" }

$encoded = encodeImageAndDimensions("only-file-name", 300, 200);
var_dump($encoded); //string(19) "ASwAyHuZnhksLxwWlA$"
$decoded = decodeImageAndDimensions($encoded);
var_dump($decoded); // array(3) { ["width"] => int(300) ["height"] => int(200) ["path"] => string(14) "only-file-name" }

2
有许多方法可以缩短URL。您可以查看其他服务(如TinyURL)如何缩短其URL。以下是一篇关于哈希和缩短URL的好文章:URL Shortening: Hashes In Practice 您可以使用PHP函数mhash()对字符串应用哈希。
如果您向下滚动到mhash网站上的“可用哈希”部分,则可以查看在该函数中可以使用哪些哈希(尽管我建议检查哪些PHP版本具有哪些函数):mhash - Hash Library

谢谢你的回答。非常有帮助,但不适合我的问题,因为我需要非数据库解决方案(纯PHP)。 - Artur Filipiak

1

根据你的问题,你提到它应该是纯PHP而不使用数据库,并且应该有解码字符串的可能性。所以我们可以稍微打破一下规则:

  • 我理解这个问题的方式是,我们并不太关心安全性,但是我们想要最短的哈希值来找回图像。
  • 我们也可以通过使用单向哈希算法来“解码可能性”。
  • 我们可以将哈希值存储在JSON对象中,然后将数据存储在文件中,所以最终我们只需要进行字符串匹配即可。

```

class FooBarHashing {

    private $hashes;

    private $handle;

    /**
     * In producton this should be outside the web root
     * to stop pesky users downloading it and geting hold of all the keys.
     */
    private $file_name = './my-image-hashes.json';

    public function __construct() {
        $this->hashes = $this->get_hashes();
    }

    public function get_hashes() {
        // Open or create a file.
        if (! file_exists($this->file_name)) {
            fopen($this->file_name, "w");
        }
        $this->handle = fopen($this->file_name, "r");


        $hashes = [];
        if (filesize($this->file_name) > 0) {
            $contents = fread($this->handle, filesize($this->file_name));
            $hashes = get_object_vars(json_decode($contents));
        }

        return $hashes;
    }

    public function __destroy() {
        // Close the file handle
        fclose($this->handle);
    }

    private function update() {
        $handle = fopen($this->file_name, 'w');
        $res = fwrite($handle, json_encode($this->hashes));
        if (false === $res) {
            //throw new Exception('Could not write to file');
        }

        return true;
    }

    public function add_hash($image_file_name) {
        $new_hash = md5($image_file_name, false);

        if (! in_array($new_hash, array_keys($this->hashes) ) ) {
            $this->hashes[$new_hash] =  $image_file_name;
            return $this->update();
        }

        //throw new Exception('File already exists');
    }

    public function resolve_hash($hash_string='') {
        if (in_array($hash_string, array_keys($this->hashes))) {
            return $this->hashes[$hash_string];
        }

        //throw new Exception('File not found');
    }
}

```

使用示例:

<?php
// Include our class
require_once('FooBarHashing.php');
$hashing = new FooBarHashing;

// You will need to add the query string you want to resolve first.
$hashing->add_hash('img=/dir/dir/hi-res-img.jpg&w=700&h=500');

// Then when the user requests the hash the query string is returned.
echo $hashing->resolve_hash('65992be720ea3b4d93cf998460737ac6');

因此,最终结果是一个只有32个字符长度的字符串,比之前的52个字符要短得多。

是的,我们离解决方案更近了。我使用实际文件将图像数据存储为JSON对象,例如:名称标题描述... 我会查看您的解决方案,谢谢。 - Artur Filipiak
2
哦,亲爱的。这个答案是使用JSON文件作为存储(也称为数据库)的哈希表实现非常低效。当你有几千条记录时,这可能比使用真正的数据库更慢。想想每个请求上的所有解析。考虑I/O等待和并发。这不是一个好的解决方案。 - Phil
1
@Phil_1984_,JSON文件(在第一次加载时)连同localStorage。 - Artur Filipiak
1
@Phil_1984_,我不同意它与“真正”的数据库相比由于I/O而低效,因为我们只读取文件一次,然后将哈希表缓存在内存中。我期望我的解决方案被用作单例,并且可以同时进行多个查找。但是你正确,解析JSON在PHP中是昂贵的,所以CSV将是更好的选择。 - Aron
也许我误解了使用场景,但现在纯粹谈论PHP...即使您将其用作单例,每个使用库的请求(例如图像请求)都会构造1个单例。如果10个不同的用户同时请求不同的图像,则需要解码每个URL,并且由于没有共享内存(除非您使用类似于memcache的东西),因此每个用户都必须读取和解析文件。 - Phil
它的要点是什么?它对哈希做了什么以及如何做到的?它使用了哪种哈希方式?你能把它加入到你的答案中吗?请通过编辑(更改)你的答案来回复,而不是在评论区回复(不要添加“编辑:”,“更新:”或类似的内容 - 答案应该看起来像是今天写的)。 - Peter Mortensen

0

恐怕您无法比任何已知的压缩算法更好地缩短查询字符串。如其他答案中所述,压缩版本将比原始版本短几个字符(约为4-6个字符)。此外,相对于解码SHA-1MD5等内容,原始字符串可以相对容易地解码。

我建议通过Web服务器配置来缩短URL。您可以通过将图像路径替换为ID(在数据库中存储ID-文件名对)来进一步缩短它。

例如,以下Nginx配置接受类似于/t/123456/700/500/4fc286f1a6a9ac4862bdd39a94a80858的URL:

  • 第一个数字 (123456) 应该是来自数据库的图像 ID;
  • 700500 是图像的尺寸;
  • 最后一部分是一个 MD5 哈希值,用于保护不同尺寸的请求
# Adjust maximum image size
# image_filter_buffer 5M;

server {
  listen          127.0.0.13:80;
  server_name     img-thumb.local;

  access_log /var/www/img-thumb/logs/access.log;
  error_log /var/www/img-thumb/logs/error.log info;

  set $root "/var/www/img-thumb/public";

  # /t/image_id/width/height/md5
  location ~* "(*UTF8)^/t/(\d+)/(\d+)/(\d+)/([a-zA-Z0-9]{32})$" {
    include        fastcgi_params;
    fastcgi_pass   unix:/tmp/php-fpm-img-thumb.sock;
    fastcgi_param  QUERY_STRING image_id=$1&w=$2&h=$3&hash=$4;
    fastcgi_param  SCRIPT_FILENAME /var/www/img-thumb/public/t/resize.php;

    image_filter resize $2 $3;
    error_page 415 = /empty;

    break;
  }

  location = /empty {
    empty_gif;
  }

  location / { return 404; }
}

服务器仅接受指定模式的URL,将请求转发到带有修改查询字符串的/public/t/resize.php脚本,然后使用image_filter模块调整PHP生成的图像大小。如果出现错误,则返回空GIF图像。

image_filter是可选的,仅作为示例包含在内。调整大小可以完全在PHP端执行。顺便说一下,使用Nginx可以摆脱PHP部分。

PHP脚本应按以下方式验证哈希值:

// Store this in some configuration file.
$salt = '^sYsdfc_sd&9wa.';

$w = $_GET['w'];
$h = $_GET['h'];

$true_hash = md5($w . $h . $salt . $image_id);
if ($true_hash != $_GET['hash']) {
  die('invalid hash');
}

$filename = fetch_image_from_database((int)$_GET['image_id']);
$img = imagecreatefrompng($filename);
header('Content-Type: image/png');
imagepng($img);
imagedestroy($img);

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