使用PHP脚本提供图像与直接加载图像的区别

24

我想要监测一些外部图片被加载的频率。因此,我的想法是不直接提供像这样的uri:

www.site.com/image1.jpg

我可以创建一个读取图像的PHP脚本,所以我编写了一个PHP文件,我的HTML如下:

<img src="www.site.com/serveImage.php?img=image1.jpg">

但我不知道如何从磁盘中读取图像并返回它。我应该返回一个字节数组还是设置内容类型?

敬礼, 米歇尔

9个回答

41
通过脚本发送图片对其他功能比如按需缩放和缓存非常有用。
根据Pascal MARTIN所回答的,使用函数readfile和以下头信息是必要的:
  • Content-Type
    • 内容的MIME类型
    • 例如:header('Content-Type: image/gif');
    • 查看函数mime_content_type
    • 类型:
      • image/gif
      • image/jpeg
      • image/png
除了明显的Content-Type之外,你还应该关注其他头部信息,如:
  • Content-Length
    • 响应正文的长度(以八位字节为单位)
    • 例如:header('Content-Length: 348');
    • 查看函数filesize
    • 可以更好地利用连接。
  • Last-Modified
    • 请求对象的最后修改日期,格式为RFC 2822
    • 例如:header('Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT');
    • 查看函数filemtimedate将其格式化为所需的RFC 2822格式
      • 例如:header('Last-Modified: '.date(DATE_RFC2822, filemtime($filename)));
    • 如果文件修改时间相同,可以在发送304后退出脚本。
  • 状态码
    • 例如:header("HTTP/1.1 304 Not Modified");
  • 现在您可以退出并不再发送图像
  • 要查找上次修改时间,请在 $_SERVER 中寻找以下内容

    • If-Modified-Since
      • 如果内容未更改,则允许返回 304 Not Modified
      • 示例: If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT
      • 以键http_if_modified_since的形式在 $_SERVER

    HTTP头响应列表


    这里有一段不错的代码,可以帮助实现所有这些头部信息:链接 - Felipe Balduino Cassar

    23

    要实现这样的效果,你的脚本需要:

    • 发送正确的头文件,取决于图片的类型:image/gif、image/png、image/jpeg等
    • 发送图片的数据
    • 确保没有任何其他内容被发送(没有空格,没有其他东西)

    使用header 函数来完成这些操作,像下面这样编写代码:

    header("Content-type: image/gif");
    

    或者

    header("Content-type: image/jpeg");
    

    根据图像类型,可以选择使用不同的方法。


    要发送图像数据,您可以使用 readfile 函数:

    读取文件并将其写入输出缓冲区。

    这样,您就可以在一个函数中同时读取文件并输出其内容。


    作为一则旁注:

    • 您必须采取某些安全措施,以确保用户无法通过您的脚本请求任何他们想要的内容:您必须确保它只提供来自您预期目录的图像;例如,像 serveImage.php?file=/etc/passwd 这样的请求是不可以接受的。
    • 如果您只想获取每天加载文件的次数,则解析 Apache 的日志文件可能是个好主意(例如通过 cron 每天在 00:05 运行的批处理程序,解析前一天的日志)。您将没有实时统计数据,但在服务器上需要的资源较少,因为没有 PHP 来提供静态文件。

    1
    感谢您的附注: •您必须加入一些安全措施,以确保用户无法通过您的脚本请求任何他们想要的内容:您必须确保它只提供您预期目录中的图像;例如,serveImage.php?file=/etc/passwd之类的内容是不可以的。 - Michel

    12

    我使用“passthru”函数调用“cat”命令,就像这样:

    header('Content-type: image/jpeg');
    passthru('cat /path/to/image/file.jpg');
    

    适用于Linux。节省资源。


    优秀的解决方案。只需记住,一些主机出于安全原因禁用了 passthru 命令。 - Philipp

    11

    你必须设置内容类型:

    header("Content-type: image/jpeg");
    

    然后你加载图像并像这样输出:

    $image=imagecreatefromjpeg($_GET['img']);
    imagejpeg($image);
    

    2
    imagecreatefromjpeg + imagejpeg可能有点过度,而且会消耗CPU资源,如果你只想从文件中发送数据的话。我看到的优点是确保你真正加载了图像;但可能还有其他方法来确保这一点(比如只允许一个目录,只包含图像)。 - Pascal MARTIN
    23
    如果您只想直接发送图像,使用readfile更好。 - OIS

    9

    与其在HTML中更改直接的图像URL,您可以在Apache配置或.htaccess中添加一行来将目录中所有图像的请求重写到一个PHP脚本中。然后,在该脚本中,您可以利用请求头和$_server数组来处理请求并提供文件。

    首先在您的.htaccess文件中:

    RewriteRule ^(.*)\.jpg$ serve.php [NC]
    RewriteRule ^(.*)\.jpeg$ serve.php [NC]
    RewriteRule ^(.*)\.png$ serve.php [NC]
    RewriteRule ^(.*)\.gif$ serve.php [NC]
    RewriteRule ^(.*)\.bmp$ serve.php [NC]
    

    脚本serve.php必须与.htaccess位于同一目录中。你可能会写出类似以下的代码:
    <?php
    $filepath=$_SERVER['REQUEST_URI'];
    $filepath='.'.$filepath;
    if (file_exists($filepath))
    {
    touch($filepath,filemtime($filepath),time()); // this will just record the time of access in file inode. you can write your own code to do whatever
    $path_parts=pathinfo($filepath);
    switch(strtolower($path_parts['extension']))
    {
    case "gif":
    header("Content-type: image/gif");
    break;
    case "jpg":
    case "jpeg":
    header("Content-type: image/jpeg");
    break;
    case "png":
    header("Content-type: image/png");
    break;
    case "bmp":
    header("Content-type: image/bmp");
    break;
    }
    header("Accept-Ranges: bytes");
    header('Content-Length: ' . filesize($filepath));
    header("Last-Modified: Fri, 03 Mar 2004 06:32:31 GMT");
    readfile($filepath);
    
    }
    else
    {
     header( "HTTP/1.0 404 Not Found");
     header("Content-type: image/jpeg");
     header('Content-Length: ' . filesize("404_files.jpg"));
     header("Accept-Ranges: bytes");
     header("Last-Modified: Fri, 03 Mar 2004 06:32:31 GMT");
     readfile("404_files.jpg");
    }
    /*
    By Samer Mhana
    www.dorar-aliraq.net
    */
    ?>
    

    (这个脚本可以改进!)


    4
    此外,如果您希望用户在单击图像并选择“另存为”时看到真实的文件名而不是脚本名称,则还需要设置此标头:
    header('Content-Disposition: filename=$filename');
    

    1
    这个与Apache中的重定向(虚拟主机或.htaccess)到脚本文件也可以解决。因此,/img/somefile_small.jpg会被内部重定向到showimage.php以显示图片。 - OIS

    2

    我也使用readfile来提供图片服务,但为了安全和额外的功能,我做了更多的工作。

    我建立了一个数据库,存储图片ID、尺寸和文件扩展名。这也意味着需要上传图片(允许可选的调整大小),因此我只将系统用于内容,而不是网站本身所需的图片(如背景或精灵)。

    它还非常擅长确保您只能请求图像。

    因此,提供简化的工作流程如下(无法在此处发布生产代码):

    1)获取所请求图像的ID

    2)在数据库中查找

    3)基于扩展名抛出标头(上传时,“jpg”被重新映射为“jpeg”)

    4)readfile("/images/$id.$extension");

    5)可选地保护/images/目录,以使其无法被索引(在我的系统中不是问题,因为它将URL映射为类似于/index.php?module=image&action=view&id=11/image/view/11


    2

    您最好检查服务器访问日志以查找此问题。将所有图像都通过php运行可能会给您的服务器带来一些负载。


    嗯,那肯定会奏效。当负载成为问题时,我会保存它。 - Michel
    此外,通过 PHP 脚本运行它们会影响客户端缓存,这实际上可能会增加读取负载。 - Rob
    2
    @rob:这与运行脚本无关,而是忘记发送正确的标头。我在我的答案中提供了两个最佳方法。 - OIS

    1

    以上有很多好的答案,但是没有一个提供可以在您的PHP应用程序中使用的工作代码。我已经设置了我的代码,以便根据不同的标识符在数据库表中查找图像的名称。客户端从未设置要下载的文件名,因为这是一种安全风险。

    一旦找到图像名称,我将其分解以获取扩展名。这很重要,以了解基于图像类型(即png、jpg、jpeg、gif等)提供什么类型的头文件。出于安全原因和将jpg转换为jpeg以获得正确的标题名称,我使用switch来执行此操作。我在我的代码中包含了一些额外的头文件,以确保文件不被缓存,需要重新验证,更改名称(否则它将是调用的脚本的名称),最后从服务器读取文件并传输它。

    我喜欢这种方法,因为它从不公开目录或实际文件名。如果您正在尝试以安全方式执行此操作,请确保在运行脚本之前对用户进行身份验证。

    $temp = explode('.', $image_filename);
    $extension = end($temp);    // jpg, jpeg, gif, png - add other flavors based off your use case
    
    switch ($extension) {
        case "jpg":
            header('Content-type: image/jpeg');
            break;
        case "jpeg":
        case "gif":
        case "png":
            header('Content-type: image/'.$extension);
            break;
        default:
            die;    // avoid security issues with prohibited extensions
    }
    
    header('Content-Disposition: filename=photo.'.$extension);
    header('Content-Transfer-Encoding: binary');
    header('Expires: 0');
    header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    readfile('../SECURE_DIRECTORY/'.$image_filename);
    

    PHP 8 允许您使用匹配功能,通过摆脱 switch 和丑陋的嵌套 case 进一步优化代码。


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