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

Shortest possible encoded string with a decode possibility (shorten URL) using only PHP

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

场景

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

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

所以,基本上我只想对 URL:

的搜索查询部分进行编码

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

我使用的方法是:

 $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 对其进行解码?

缩短网址的方法有很多种。您可以查看其他服务(如 TinyURL)如何缩短其 URL。这是一篇关于哈希和缩短 URL 的好文章:URL Shortening: Hashes In Practice

您可以使用 PHP 函数 mhash() 将哈希应用于字符串。

如果你向下滚动到 mhash 网站上的“可用哈希”,你可以看到你可以在函数中使用哪些哈希(尽管我会检查哪些 PHP 版本有哪些函数):mhash - Hash Library

在您的问题中,您声明它应该是纯粹的 PHP 而不是使用数据库,并且应该有可能对字符串进行解码。所以稍微改变一下规则:

  • 我解释这个问题的方式是,我们不太关心安全性,但我们确实想要返回图像的最短哈希值。
  • 我们还可以通过使用单向哈希算法对 "decode possibility" 持保留态度。
  • 我们可以将散列存储在一个 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 短得多。

阅读前面的答案和下面的评论,您需要一个解决方案来隐藏图像解析器的真实路径,为其提供固定的图像宽度。

第 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=&img= [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"

原文post:

基本上,如果您在示例中添加一些格式,您缩短的 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];
}

这并没有隐藏原来的路径,但是这个路径没有理由是public。一旦检查了密钥,您的“index.php”就可以从本地目录输出您的图像。

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

我认为完全不模糊会更好。您可以非常简单地缓存返回的图像并使用处理程序来提供它们。这需要将图像大小硬编码到 PHP 脚本中。当你获得新尺寸时,你可以删除缓存中的所有内容,因为它是 'lazy loaded'.

1.从请求中获取图像 这可能是这样的:/thumbnail.php?image=img.jpg&album=myalbum。它甚至可以使用 rewrite 变成任何东西,并且有一个 URL 像:/gallery/images/myalbum/img.jpg.

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);
?>

不对 URL 进行编码,而是输出原始图像的缩略图副本。这是我的想法:

  1. 通过使用随机字符命名图片(实际文件名)为 PHP 创建“地图”。 Random_bytes 是一个很好的起点。

  2. 将所需分辨率嵌入来自 #1 的随机 URL 字符串。

  3. 使用imagecopyresampled功能将原始图像复制到您想要输出的分辨率,然后再输出到客户端设备。

例如:

  1. 文件名示例(来自 bin2hex(random_bytes(6))):a1492fdbdcf2.jpg

  2. 所需分辨率:800x600。我的新 link 可能看起来像: http://myserver.com/?800a1492fdbdcf2600http://myserfer.com/?a1492800fdbdc600f2 甚至 http://myserver.com/?800a1492fdbdcf2=600 取决于我选择在 link

    中嵌入分辨率的位置
  3. PHP就知道文件名为a1492fdbdcf2.jpg,抓起来,用imagecopyresampled复制到你想要的分辨率,输出

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

第一部分:

a method that encodes an string to shortest possible length

如果您在 URL vocabulary/characters 上比较灵活,这将是一个很好的起点。由于 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 table 的 6 位子集来实现的。此 window 已移动,以便偏移量从有用的字符开始并包括您当前使用的所有字符。

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

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

so the user can't guess how to get the larger image

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

$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 encode/decode→解密→解压。这个输出将尽可能接近你真正能得到的,接近你能得到的最短长度。

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

恐怕您无法比任何已知的方法更好地缩短查询字符串 压缩算法。正如其他答案中提到的,压缩 版本将比原始版本短几个(大约 4-6 个)字符。 此外,原始字符串可以相对容易地解码(例如与解码 SHA-1 or MD5 相反)。

我建议通过 Web 服务器配置来缩短 URL。你可能 通过用 ID 替换图像路径进一步缩短它(store ID-filename 数据库中的对)。

例如,下面的Nginx配置接受 /t/123456/700/500/4fc286f1a6a9ac4862bdd39a94a80858 等网址,其中

  • 第一个数字 (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=&w=&h=&hash=;
    fastcgi_param  SCRIPT_FILENAME /var/www/img-thumb/public/t/resize.php;

    image_filter resize  ;
    error_page 415 = /empty;

    break;
  }

  location = /empty {
    empty_gif;
  }

  location / { return 404; }
}

服务器只接受指定模式的 URL,将请求转发到 /public/t/resize.php 脚本并修改查询字符串,然后使用 image_filter 模块调整 PHP 生成的图像的大小。如果出错,returns 一个空的 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);

关于 "security"

的简短说明

如果某处没有存储“秘密密码”,您将无法保护您的 link:只要 URI 包含访问您的资源的所有信息,那么它将是可解码的并且您的“自定义安全性”(顺便说一句,它们是相反的词)很容易被破坏。

你仍然可以在你的 PHP 代码中加盐(比如 $mysalt="....long random string..."),因为我怀疑你想要一个永恒的安全(这种方法很弱,因为你不能更新 $mysalt价值,但在您的情况下,几年的安全性听起来就足够了,因为无论如何,用户可以购买一张照片并在其他地方分享,这会破坏您的任何安全机制)。

如果你想要一个安全的机制,使用 well-known 一个(框架会携带),以及身份验证和用户权限管理机制(这样你就可以知道谁在寻找你的图像,并且他们是否被允许)。

安全是有代价的。如果你不想负担它的计算和存储要求,那就别管它了。


通过签署 URL

来确保安全

如果你想避免用户容易 by-passing 并获得全分辨率图片,那么你可以只签署 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


聪明地缩短

使用通用压缩算法“缩短”在这里没有用,因为 headers 会比 URI 长,所以它几乎永远不会缩短它。

如果你想缩短它,聪明一点:如果它总是相同的,就不要给出相对路径 (/dir/dir)(或者只有当它不是主要路径时才给出它)。如果扩展名始终相同,则不要提供扩展名(如果几乎所有内容都在 png 中,则在不 png 时提供扩展名)。不要给出 height,因为图像带有 aspect ratio:您只需要 width。如果不需要 pixel-accurate 宽度,请输入 x100px

从评论区的讨论看来,您真正想要的是保护您的高分辨率原始图像。

考虑到这一点,我建议首先使用您的 Web 服务器配置(例如 Apache mod_authz_core or Nginx ngx_http_access_module)实际执行此操作,以拒绝从 Web 访问存储原始图像的目录。

请注意,服务器只会拒绝从网络访问您的图像,但您仍然可以直接从 PHP 脚本访问它们。由于您已经在使用一些“resizer”脚本显示图像,我建议在那里设置一些硬性限制,并拒绝将图像调整到比该更大的尺寸(例如 $width = min(1000, $_GET['w']))。

我知道这不能回答您原来的问题,但我认为这是保护您的图像的正确解决方案。如果您仍然想混淆原始名称和调整参数大小,您可以按照您认为合适的方式进行操作,而不必担心有人可能会弄清楚背后的原因。

我认为生成的 URL 不会比您自己的示例中的更短。但我建议采取一些步骤来更好地混淆您的图像。

首先,我会从你正在压缩和 Base64 编码的基础 URL 中删除你能删除的所有内容,而不是

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

我会用

s=hi-res-img.jpg,700,500,062c02153d653119

那些最后 16 个字符是否是一个散列,用于验证打开的 URL 是否与您在代码中提供的相同 - 并且用户没有试图将高分辨率图像骗出系统。

您提供图像的 index.php 将像这样开始:

function myHash($sRaw) { // returns a 16-characters dual hash
    return hash('adler32', $sRaw) . strrev(hash('crc32', $sRaw));
} // These two hash algorithms are suggestions, there are more for you to chose.

// s=hi-res-img.jpg,700,500,062c02153d653119
$aParams = explode(',', $_GET['s']);
if (count($aParams) != 4) {
    die('Invalid call.');
}

list($sFileName, $iWidth, $iHeight, $sHash) = $aParams;

$sRaw = session_id() . $sFileName . $iWidth . $iHeight;
if ($sHash != myHash($sRaw)) {
    die('Invalid hash.');
}

此时您可以发送图像,因为打开它的用户可以访问有效的 link。

请注意,使用 session_id 作为原始字符串的一部分,这使得散列是可选的,但用户无法共享有效的 URL - 因为它将是会话绑定。 如果您希望 URL 可共享,则只需从该调用中删除 session_id。

我会像你已经做的那样包装结果 URL,zip + Base64。结果会比您的版本更大,但更难通过混淆查看,因此可以保护您的图像免遭未经授权的下载。

如果您只想缩短它,我看不到不重命名文件(或它们的文件夹)或不使用数据库的方法。

提出的文件数据库解决方案肯定会产生并发问题 - 除非您总是没有人或很少有人同时使用该系统。

你说你想要那里的大小,所以如果有一天你决定预览图像太小,你想增加大小 - 这里的解决方案是将图像大小硬编码到 PHP 脚本并将其从 URL.

中删除

如果您以后想更改大小,请更改 PHP 脚本中的硬编码值(或您创建的 config.php 文件中的硬编码值包含到脚本中)。

您还说过,您已经在使用文件将图像数据存储为 JSON 对象,例如:nametitledescription。利用这一点,不需要数据库,可以使用JSON文件名作为查找图像数据的key。

当用户像这样访问URL时:

www.mysite.com/share/index.php?ax9v

您从已经存储 JSON 文件的位置加载 ax9v.json,并且在该 JSON 文件中存储图像的真实路径。然后加载图像,根据脚本中的硬编码大小调整大小并将其发送给用户。

从中得出的结论 URL Shortening: Hashes In Practice,要获得 URL 的最小搜索字符串部分,您需要在上传新文件时迭代有效的字符组合(例如,第一个是“AAA”,然后是“AAB”、“AAC”等)而不是使用哈希算法。

您上传的前 238,328 张照片的字符串中只有三个字符。

我已经开始在 PhpFiddle 上制作 PHP 解决方案的原型,但代码消失了(不要使用 PhpFiddle)。

关于编码如何无助于安全已经说了很多,所以我只关注缩短和美观。

与其将其视为一个字符串,不如将其视为三个独立的组件。然后,如果你限制每个组件的代码 space,你可以把东西打包得更小。

例如,

  • path - 仅由 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" }

理论

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

但是如果我们转换为字符集为 16(0-9 和 A-F)的十六进制数(以 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 保存字符。

安全

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