PHP如何实现后台倒计时 延迟输出下载直链防止审查元素获取
核心认知纠正
你当前的思路存在一个本质错误:只要是最终发送到用户浏览器的内容,不管是PHP执行完一次性输出、还是JS/AJAX延迟加载的,用户都能通过审查元素、网络抓包拿到,不存在「PHP输出的内容用户无法提前查看」的情况。
你写的带sleep(10)的代码之所以达不到预期,是因为PHP作为服务端语言,会把整个脚本执行完成后才把完整响应返回给浏览器,加sleep()只会导致用户打开页面时白屏等待10秒,之后所有内容(包括前面的段落、延迟输出的链接脚本)才会一起渲染,根本做不到先显示提示、延迟出链接的效果,这是HTTP请求响应的基本逻辑,靠前端隐藏、服务端延迟输出的方式藏直链本质都是掩耳盗铃。
正确实现思路
不要试图「藏住固定直链」,要把思路转成让用户提前拿到链接也无法非法使用:
- 真实文件不要放在Web可直接访问的目录下,从根源上杜绝永久直链泄露的可能
- 不输出固定的文件直链,所有下载请求都经过服务端接口校验
- 倒计时结束后才给用户生成绑定身份、短时效、一次性的下载令牌,就算用户拿到令牌链接,提前访问、转发他人都无法正常下载
简化实现代码
1. 前端下载页(index.php)
页面加载时先展示倒计时,纯前端实现,不需要服务端延迟执行,倒计时结束后再向服务端申请下载令牌:
<?php session_start(); // 记录用户进入页面的时间,用于后续校验是否真的等待了足够时长 $_SESSION['page_enter_time'] = time(); ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>文件下载</title> </head> <body> <p>请等待15秒后开始下载</p> <a id="downloadBtn" href="javascript:alert('请等待倒计时结束')">等待中...</a> <script> let waitSeconds = 15; const downloadBtn = document.getElementById('downloadBtn'); const countDown = setInterval(() => { waitSeconds--; if (waitSeconds <= 0) { clearInterval(countDown); downloadBtn.innerText = '立即下载'; // 倒计时结束,向服务端申请一次性下载令牌 fetch('get_token.php') .then(res => { if (!res.ok) throw new Error('获取链接失败'); return res.text(); }) .then(token => { downloadBtn.href = `download.php?token=${token}`; }) .catch(err => alert(err.message)); return; } downloadBtn.innerText = `请等待${waitSeconds}秒后下载`; }, 1000); </script> </body> </html>
2. 令牌生成接口(get_token.php)
校验用户等待时长,生成随机、短时效、一次性的下载令牌,不暴露任何真实文件信息:
<?php session_start(); // 校验是否合法访问、是否等够了15秒 if ( !isset($_SESSION['page_enter_time']) || time() - $_SESSION['page_enter_time'] < 15 ) { http_response_code(403); exit('非法访问'); } // 生成16位随机令牌 $downloadToken = bin2hex(random_bytes(16)); // 把令牌信息存在Session(高并发场景可以换成Redis缓存) $_SESSION['valid_download_token'] = [ 'token_value' => $downloadToken, 'expire_time' => time() + 20, // 令牌20秒后过期 'is_used' => false, // 标记是否已使用 'target_file' => '/your/non-web-dir/target_file.zip' // 真实文件路径,存在服务端不发给前端 ]; echo $downloadToken; ?>
3. 实际下载处理接口(download.php)
校验令牌合法性,校验通过后才输出文件内容给用户:
<?php session_start(); // 基础参数校验 if (!isset($_GET['token']) || !isset($_SESSION['valid_download_token'])) { http_response_code(403); exit('下载链接无效'); } $tokenConfig = $_SESSION['valid_download_token']; // 校验令牌是否匹配、是否过期、是否已使用 if ( $tokenConfig['token_value'] !== $_GET['token'] || $tokenConfig['expire_time'] < time() || $tokenConfig['is_used'] ) { http_response_code(403); exit('下载链接已过期或已被使用'); } // 标记令牌为已使用,防止重复盗用 $tokenConfig['is_used'] = true; $_SESSION['valid_download_token'] = $tokenConfig; // 读取文件输出给浏览器 $filePath = $tokenConfig['target_file']; header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="'.basename($filePath).'"'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); header('Content-Length: ' . filesize($filePath)); readfile($filePath); exit; ?>
额外优化建议
- 真实文件一定要存在网站根目录之外的非Web访问目录,禁止通过静态URL直接访问
- 如果需要更高的防刷强度,可以在令牌校验逻辑里加上IP匹配、单用户下载频率限制、验证码校验等规则
- 不要尝试用PHP flush、前端代码混淆这类方式藏链接,所有浏览器侧的内容对用户都是完全透明的,这类操作没有实际防护效果
内容的提问来源于stack exchange,提问作者Genisys Mythic
相关产品推荐
相关产品推荐

