php 8.3 通过安全存储、签名链接与响应头控制实现文件下载:文件存于 web 不可访问目录,生成带时效签名的唯一下载链接,并在 download.php 中校验后设置正确响应头输出文件。

PHP 8.3 本身不直接“生成下载链接”,而是通过安全的文件存储 + 路由控制 + 响应头设置,实现用户点击链接后触发文件下载。关键在于:不暴露真实路径、限制访问权限、支持断点续传、避免直接暴露 uploads/ 目录。
一、上传文件并存入安全目录
上传时不要把文件放到 Web 可直接访问的目录(如 public/uploads),而应存到项目根目录外或 Web 不可执行的私有目录中,例如:
-
/var/www/myapp/storage/uploads/(Linux) -
C:\inetpub\myapp\storage\uploads\(Windows,确保 IIS/Nginx 不映射该路径)
上传代码示例(使用 PHP 8.3 的类型声明和错误处理):
function handleUpload(): ?array
{
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('文件上传失败');
}
$tmpPath = $_FILES['file']['tmp_name'];
$originalName = basename($_FILES['file']['name']);
$safeName = bin2hex(random_bytes(16)) . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
$storageDir = __DIR__ . '/../storage/uploads/';
$targetPath = $storageDir . $safeName;
if (!is_dir($storageDir)) {
mkdir($storageDir, 0755, true);
}
if (move_uploaded_file($tmpPath, $targetPath)) {
return [
'id' => uniqid('dl_', true),
'original_name' => $originalName,
'stored_name' => $safeName,
'size' => $_FILES['file']['size'],
'uploaded_at' => date('Y-m-d H:i:s'),
];
}
throw new RuntimeException('保存文件失败');
}
二、生成唯一、可验证的下载链接
链接格式建议:/download?id=dl_64a8f2b1c0e9a&token=abc123,其中:
- id 是数据库或缓存中的唯一标识(非文件名,防遍历)
- token 是短期有效签名(如 HMAC-SHA256 + 过期时间),防止链接被滥用
生成 token 示例:
$expires = time() + 3600; // 1小时有效期
$payload = "{$id}|{$expires}";
$token = hash_hmac('sha256', $payload, $_ENV['DOWNLOAD_SECRET_KEY']);
$downloadUrl = "/download?id={$id}&token={$token}&expires={$expires}";
三、编写下载路由(如 download.php 或用框架控制器)
该脚本负责校验、读取、输出,**不输出任何额外 HTML 或空格**:
// download.php
if (!isset($_GET['id'], $_GET['token'], $_GET['expires'])) {
http_response_code(400);
exit('Invalid request');
}
$id = $_GET['id'];
$token = $_GET['token'];
$expires = (int)$_GET['expires'];
// 验证时效
if (time() > $expires) {
http_response_code(410);
exit('Link expired');
}
// 验证签名(重算对比)
$expected = hash_hmac('sha256', "{$id}|{$expires}", $_ENV['DOWNLOAD_SECRET_KEY']);
if (!hash_equals($expected, $token)) {
http_response_code(403);
exit('Forbidden');
}
// 查询数据库或缓存获取文件元信息(此处简化为数组模拟)
$record = getDownloadRecordById($id); // 你自己的查询逻辑
if (!$record || !file_exists(__DIR__ . '/../storage/uploads/' . $record['stored_name'])) {
http_response_code(404);
exit('File not found');
}
$filePath = __DIR__ . '/../storage/uploads/' . $record['stored_name'];
$fileName = $record['original_name'];
// 设置响应头(关键!)
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . rawurlencode($fileName) . '"');
header('Content-Length: ' . filesize($filePath));
header('Accept-Ranges: bytes'); // 支持断点续传
header('Cache-Control: private, must-revalidate, max-age=0');
header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($filePath)) . ' GMT');
readfile($filePath);
exit;
四、链接管理:记录、清理与审计
建议用轻量级 SQLite 或 MySQL 记录每次生成的下载项,字段包括:id、original_name、stored_name、user_id(可选)、created_at、expires_at、download_count、ip_hash(防刷)。
定期清理过期记录和对应文件(可用 CLI 脚本,每天运行一次):
// cleanup_expired_downloads.php
$pdo = new PDO('sqlite:' . __DIR__ . '/../storage/db.sqlite');
$stmt = $pdo->query("SELECT stored_name FROM downloads WHERE expires_at fetch()) {
@unlink(__DIR__ . '/../storage/uploads/' . $row['stored_name']);
}
$pdo->exec("DELETE FROM downloads WHERE expires_at
<p>前端展示下载链接时,可加「复制」按钮 + 「一键过期」操作,提升管理体验。</p>php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











