
通过将文件存储在 web 根目录外,并借助 apache 重写规则将请求路由至受权限控制的 php 脚本,可有效阻止未授权用户直接访问上传文件。
通过将文件存储在 web 根目录外,并借助 apache 重写规则将请求路由至受权限控制的 php 脚本,可有效阻止未授权用户直接访问上传文件。
在 Web 应用中,将用户上传的文件(如 .docx、.pdf)存放在 public_html 或 htdocs 等 Web 可直接访问的目录下,会带来严重的安全风险:即使系统已实现完善的登录与角色权限机制,攻击者一旦获知文件 URL(例如通过数据库泄露、前端日志或猜测路径),即可绕过所有业务层校验,直接下载敏感文档。
根本解决方案是「分离存储与访问」:
✅ 将真实文件保存在 Web 根目录之外(如 /var/www/app-data/uploads/),确保其无法被 HTTP 服务器直接响应;
✅ 所有文件下载请求必须经由受控脚本(如 download.php)处理,该脚本负责身份验证、权限校验与安全响应。
一、配置 Apache 重写规则(.htaccess 或虚拟主机配置)
在 Web 可访问目录(如 ./apps/uploads/)下添加 .htaccess,将匹配的文件请求重写为 PHP 路由:
# .htaccess(置于 uploads 目录内)
RewriteEngine On
RewriteBase /ramseyer/apps/uploads/
# 拦截常见文档后缀,重写为 download.php 控制器
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^(.+)\.(pdf|docx|xlsx|txt|jpg|png)$ /ramseyer/download.php?file=$1&ext=$2 [L,QSA]⚠️ 注意:RewriteBase 必须与实际 URL 路径一致;[QSA] 保留原有查询参数;确保 mod_rewrite 已启用且 AllowOverride All 允许 .htaccess 生效。
二、编写安全的 download.php 控制器
该脚本需完成三重职责:鉴权 → 文件合法性校验 → 安全输出。示例代码如下(含关键防护逻辑):
<?php
// download.php —— 必须置于 Web 可访问路径(如 /ramseyer/),但不存放真实文件
session_start();
// 1. 强制登录校验(可根据框架调整为 JWT、RBAC 等)
if (!isset($_SESSION['user_id']) || !is_numeric($_SESSION['user_id'])) {
http_response_code(403);
die('Access denied: Not authenticated.');
}
// 2. 白名单校验扩展名(防御 MIME 类型混淆 & 任意文件读取)
$allowed_exts = ['pdf', 'docx', 'xlsx', 'txt', 'jpg', 'png'];
$ext = strtolower($_GET['ext'] ?? '');
if (!in_array($ext, $allowed_exts)) {
http_response_code(400);
die('Invalid file type.');
}
// 3. 构建安全文件路径(禁止路径遍历)
$file_base = basename($_GET['file'] ?? ''); // 去除路径分隔符
if (empty($file_base)) {
http_response_code(400);
die('Invalid filename.');
}
$storage_root = '/var/www/app-data/uploads/'; // ✅ 严格位于 Web 根目录外
$full_path = $storage_root . $file_base . '.' . $ext;
// 4. 二次文件存在性 & 权限校验(如:查数据库确认该用户是否有权访问此文件)
if (!file_exists($full_path)) {
http_response_code(404);
die('File not found.');
}
// 示例:根据 file_base 查询数据库,验证当前用户是否拥有该文件访问权限
// $stmt = $pdo->prepare("SELECT 1 FROM uploads WHERE id = ? AND user_id = ?");
// $stmt->execute([$file_base, $_SESSION['user_id']]);
// if (!$stmt->fetch()) { /* 拒绝访问 */ }
// 5. 安全输出(设置正确 Content-Type,避免 XSS 或执行风险)
$mime_types = [
'pdf' => 'application/pdf',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'txt' => 'text/plain',
'jpg' => 'image/jpeg',
'png' => 'image/png',
];
$mime = $mime_types[$ext] ?? 'application/octet-stream';
header('Content-Type: ' . $mime);
header('Content-Disposition: attachment; filename="' . $file_base . '.' . $ext . '"');
header('Content-Length: ' . filesize($full_path));
header('Cache-Control: no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
// 使用 readfile() 替代 file_get_contents(),节省内存(尤其大文件)
readfile($full_path);
exit;三、关键安全注意事项
- ? 永远不要信任客户端输入:对 $_GET['file'] 使用 basename() 是基础,但更推荐使用 UUID 或哈希 ID 存储文件名,彻底规避命名注入;
- ? 物理隔离优于规则拦截:.htaccess 只是辅助手段,核心在于文件存储路径不可被 Web Server 直接解析;
- ? 权限校验不可省略:即使用户已登录,也必须验证其对当前请求文件的具体访问权限(如所属项目、部门、共享状态等);
- ?️ 禁用目录浏览:确保 Options -Indexes 已启用,防止 uploads/ 目录被列目录;
- ? 大文件优化建议:生产环境应使用 X-Sendfile(Apache)或 X-Accel-Redirect(Nginx)交由 Web 服务器高效传输,PHP 仅做鉴权。
遵循以上方案,即可在保障用户体验的同时,彻底关闭“URL 暴力访问”这一高危入口,让文件访问真正受控于你的业务权限体系。










