仅使用 PHP 具有解码可能性的最短可能编码字符串(缩短 URL)
Shortest possible encoded string with a decode possibility (shorten URL) using only PHP
我正在寻找一种将字符串编码为 最短 可能长度并使其成为 可解码 的方法(纯 PHP,没有 SQL)。我有工作脚本,但我对编码字符串的长度不满意。
场景
Link 到图像(这取决于我想向用户显示的文件分辨率):
编码link(因此用户无法猜测如何获得更大的图像):
所以,基本上我只想对 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 对其进行解码?
缩短网址的方法有很多种。您可以查看其他服务(如 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.jpg").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 进行编码,而是输出原始图像的缩略图副本。这是我的想法:
通过使用随机字符命名图片(实际文件名)为 PHP 创建“地图”。 Random_bytes 是一个很好的起点。
将所需分辨率嵌入来自 #1 的随机 URL 字符串。
使用imagecopyresampled功能将原始图像复制到您想要输出的分辨率,然后再输出到客户端设备。
例如:
文件名示例(来自 bin2hex(random_bytes(6))
):a1492fdbdcf2.jpg
所需分辨率:800x600。我的新 link 可能看起来像:
http://myserver.com/?800a1492fdbdcf2600
或 http://myserfer.com/?a1492800fdbdc600f2
甚至 http://myserver.com/?800a1492fdbdcf2=600
取决于我选择在 link
中嵌入分辨率的位置
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;
700
和 500
是图像尺寸;
- 最后一部分是 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');
聪明地缩短
使用通用压缩算法“缩短”在这里没有用,因为 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 对象,例如:name
、title
、description
。利用这一点,不需要数据库,可以使用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 可以轻松解码字符串。但是你可以洗牌字符集(一次)。然后对攻击者来说有点困难,但并不真正安全。也许这对您的用例来说已经足够了。
我正在寻找一种将字符串编码为 最短 可能长度并使其成为 可解码 的方法(纯 PHP,没有 SQL)。我有工作脚本,但我对编码字符串的长度不满意。
场景
Link 到图像(这取决于我想向用户显示的文件分辨率):
编码link(因此用户无法猜测如何获得更大的图像):
所以,基本上我只想对 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 对其进行解码?
缩短网址的方法有很多种。您可以查看其他服务(如 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.jpg").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 进行编码,而是输出原始图像的缩略图副本。这是我的想法:
通过使用随机字符命名图片(实际文件名)为 PHP 创建“地图”。 Random_bytes 是一个很好的起点。
将所需分辨率嵌入来自 #1 的随机 URL 字符串。
使用imagecopyresampled功能将原始图像复制到您想要输出的分辨率,然后再输出到客户端设备。
例如:
文件名示例(来自
bin2hex(random_bytes(6))
):a1492fdbdcf2.jpg所需分辨率:800x600。我的新 link 可能看起来像:
中嵌入分辨率的位置http://myserver.com/?800a1492fdbdcf2600
或http://myserfer.com/?a1492800fdbdc600f2
甚至http://myserver.com/?800a1492fdbdcf2=600
取决于我选择在 linkPHP就知道文件名为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; 700
和500
是图像尺寸;- 最后一部分是 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');
聪明地缩短
使用通用压缩算法“缩短”在这里没有用,因为 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 对象,例如:name
、title
、description
。利用这一点,不需要数据库,可以使用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 可以轻松解码字符串。但是你可以洗牌字符集(一次)。然后对攻击者来说有点困难,但并不真正安全。也许这对您的用例来说已经足够了。