脚本托管文件缓存无法正常更新问题排查
问题:PHP私有文件缓存逻辑导致修改后浏览器不更新图片
我开发了一款PHP脚本,用于从域名目录外提供私有文件,核心是验证访问权限。为避免重复请求已下载文件,添加了缓存规则头,但目前遇到以下异常:
- 请求新文件时返回200 OK,正常
- 请求已加载文件时返回304 Not Modified,正常
- 修改图片后,请求本应返回新文件并携带200 OK状态,但浏览器始终不更新,仅在按下Ctrl+F5强制刷新后才显示新图
- 直接输出文件原始数据时能正常看到更新,但添加Content-type头后就失效
缓存处理示例代码
<?php $filename = 'test.jpg'; $path = '/0001/user/'; $filesize = 300000; $filetype = 'image/jpeg'; $created = '2023-10-17 12:00:00'; $modified = '2023-10-17 13:00:00'; $etag = md5($filename.$modified); // Set expiration date to 30 days from the current date $expiration = strtotime('+30 days'); $expiration = gmdate('D, d M Y H:i:s \G\M\T', $expiration); // Set the "Cache-Control" header to specify private caching for 30 days header('Cache-Control: private, max-age=2592000'); header('Expires: '.$expiration); if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) { if ($_SERVER['HTTP_IF_NONE_MATCH'] === $etag) { // The client's cached version matches the current version, send a 304 Not Modified response $serverLastModified = max(strtotime($modified), strtotime($created)); header('Last-Modified: '.gmdate('D, d M Y H:i:s \G\M\T', $serverLastModified)); header('ETag: '.$etag); header('HTTP/1.1 304 Not Modified'); die; } } else if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { // If the browser has a cached version, check if it's up to date $clientLastModified = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); $serverLastModified = max(strtotime($modified), strtotime($created)); if ($clientLastModified >= $serverLastModified) { // The client's cached version is up to date, send a 304 Not Modified response header('Last-Modified: '.gmdate('D, d M Y H:i:s \G\M\T', $serverLastModified)); // Add an ETag header for cache validation header('ETag: '.$etag); header('HTTP/1.1 304 Not Modified'); die; } } // Add an ETag header for cache validation header('ETag: '.$etag); // Set the "Last-Modified" header using the most recent date $lastModifiedDate = max(strtotime($modified), strtotime($created)); $lastModifiedDateFormatted = gmdate('D, d M Y H:i:s \G\M\T', $lastModifiedDate); header('Last-Modified: '.$lastModifiedDateFormatted); // Output file to browser header('Content-type: '.$filetype); // Flush output buffer and send headers clearstatcache(); if (ob_get_length()) { ob_clean(); ob_end_clean(); } flush(); readfile(__DIR__.'/../../private/'.$path.$filename);
redbot.org抓取的响应头信息
HTTP/1.1 200 OK Connection: Keep-Alive Cache-Control: private, max-age=2592000 Expires: Fri, 17 Nov 2023 13:09:12 GMT Vary: Accept-Encoding,User-Agent ETag: "92951e93c30b48ff39298b48877a0ccb" Last-Modified: Thu, 12 Oct 2023 18:09:23 GMT Content-Type: image/jpeg Transfer-Encoding: chunked Date: Wed, 18 Oct 2023 12:09:12 GMT X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Alt-Svc: quic=":443"; ma=2592000; v="43,46", h3-Q043=":443"; ma=2592000, h3-Q046=":443";; ma=2592000, h3-Q050=":443"; ma=2592000, h3-25=":443"; ma=2592000, h3-27=":443";; ma=2592000
问题分析
- 缓存优先级过高:设置了
max-age=2592000(30天),浏览器在缓存有效期内会直接使用本地缓存,不会向服务器发起缓存验证请求(即不会发送If-None-Match或If-Modified-Since头)。即使文件修改了,浏览器也不会主动检查更新。 - 修改时间硬编码:代码里的
$modified是固定字符串,不是动态获取的文件实际修改时间。就算文件真的改了,ETag和Last-Modified还是旧值,就算浏览器发起验证,服务器也会返回304。 - Content-Type的影响:直接输出原始数据时,浏览器无法识别文件类型,不会应用严格的缓存规则;添加Content-Type后,浏览器会按照标准缓存策略处理,触发max-age的本地缓存逻辑。
解决方案
1. 调整缓存策略,强制验证
把Cache-Control改为短期max-age+must-revalidate,这样浏览器在缓存有效期内每次请求都会先向服务器验证缓存有效性:
// 改成1小时有效期,且必须验证缓存 header('Cache-Control: private, max-age=3600, must-revalidate');
must-revalidate会强制浏览器在缓存过期前,每次请求都发送验证头,服务器就能根据ETag/Last-Modified判断是否返回新文件。
2. 动态获取文件修改时间
替换硬编码的$created和$modified,用文件系统的实际时间:
$filePath = __DIR__.'/../../private/'.$path.$filename; // 获取文件修改时间(GMT格式) $modified = gmdate('Y-m-d H:i:s', filemtime($filePath)); // 获取文件创建时间(GMT格式) $created = gmdate('Y-m-d H:i:s', filectime($filePath));
这样文件修改后,$modified会自动更新,ETag和Last-Modified也会同步变化,浏览器验证时就能识别缓存失效。
3. 临时应急方案(不推荐长期使用)
如果需要立即让所有用户看到新文件,可以在请求URL后加版本参数,比如test.jpg?v=2。浏览器会把带不同参数的URL视为不同资源,直接请求新文件,但这会绕过缓存,增加服务器压力,适合临时更新场景。
内容的提问来源于stack exchange,提问作者Chaosxmk
相关产品推荐
相关产品推荐

