Skip to content

PHP 文件上传

PHP 提供了内置功能来处理通过 HTML 表单上传到服务器的文件。

警告:允许文件上传会带来重大的安全风险。务必始终实施严格的检查和验证,以防止漏洞。

在处理上传之前,请确保您的 PHP 配置允许文件上传。检查您的 php.ini 文件中的以下指令:

  • file_uploads = On:必须启用此选项以允许 HTTP 文件上传。
  • upload_tmp_dir:指定上传过程中用于存储文件的临时目录(确保服务器在此目录有写入权限)。如果未设置,PHP 会使用系统默认值。
  • upload_max_filesize:单个上传文件的最大大小(例如,8M 表示 8 兆字节)。
  • post_max_size:PHP 将接受的 POST 数据的最大大小。此值必须大于 upload_max_filesize,并且足够大以容纳所有表单数据以及文件(例如,10M)。
  • max_file_uploads:一次请求中可以同时上传的文件最大数量(默认值通常为 20)。

示例 php.ini 设置:

; 确保文件上传已启用
file_uploads = On
; 设置单个文件的最大大小(例如,10 兆字节)
upload_max_filesize = 10M
; 设置整个 POST 请求的最大大小(必须 >= upload_max_filesize)
post_max_size = 12M

注意:更改 php.ini 后,通常需要重启您的 Web 服务器(例如,Apache、Nginx)或 PHP-FPM 进程才能使更改生效。

创建一个 HTML 表单,允许用户选择文件进行上传。此表单必须满足特定要求:

<!DOCTYPE html>
<html>
<head>
<title>文件上传示例</title>
</head>
<body>
<form action="upload_handler.php" method="post" enctype="multipart/form-data">
<h2>选择要上传的图片:</h2>
<input type="file" name="userFile" id="userFile">
<input type="submit" value="Upload Image" name="submit">
</form>
</body>
</html>

HTML 表单的关键要求:

  • 方法必须是 POST (method="post")。文件上传不能使用 GET。
  • 编码类型必须是 multipart/form-data (enctype="multipart/form-data")。这允许发送二进制文件数据。
  • 使用 <input type="file"> 元素提供文件选择界面。其 name 属性(示例中为 userFile)对于在 PHP 中访问文件至关重要。

如果没有这些属性,文件上传将会失败。

安全提示:在实际应用中,考虑在表单中添加 CSRF(跨站请求伪造)保护 tokens。

此表单会将选定的文件及其他数据发送到 upload_handler.php,我们接下来将创建此文件。

PHP 脚本(在我们的示例中为 upload_handler.php)处理上传的文件。关于上传文件的信息可在 $_FILES 超全局数组中获取。

$_FILES['userFile'] 数组(其中 userFile 与输入字段的 name 匹配)包含以下信息:

  • $_FILES['userFile']['name']:客户端机器上的原始文件名。
  • $_FILES['userFile']['type']:文件的 mime 类型,如果浏览器提供了此信息(例如,‘image/jpeg’)。警告:此值由客户端控制,不可完全信任。
  • $_FILES['userFile']['size']:上传文件的大小,以字节为单位。
  • $_FILES['userFile']['tmp_name']:存储在服务器上的临时文件名。
  • $_FILES['userFile']['error']:与此文件上传相关的错误代码。UPLOAD_ERR_OK(值为 0)表示成功。

基本上传逻辑:

<?php
// 定义文件将要保存的目标目录
$target_dir = "uploads/";
// 检查目标目录是否存在,如果不存在则创建
if (!is_dir($target_dir)) {
mkdir($target_dir, 0755, true); // 递归创建目录并设置权限
}
// 检查表单是否已提交且文件输入字段是否存在
if (isset($_POST["submit"]) && isset($_FILES["userFile"])) {
// --- 1. 检查上传错误 ---
if ($_FILES["userFile"]["error"] !== UPLOAD_ERR_OK) {
echo "Upload Error Code: " . $_FILES["userFile"]["error"];
// 处理特定错误(例如,UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE)
// 提供用户友好的消息
exit; // 停止脚本执行
}
// --- 2. 生成安全的文件名 ---
// 永远不要直接信任原始文件名!
$original_filename = basename($_FILES["userFile"]["name"]); // 基本的清理
$file_extension = strtolower(pathinfo($original_filename, PATHINFO_EXTENSION));
// 创建唯一文件名以防止覆盖和命名冲突
$safe_filename = uniqid('file_', true) . '.' . $file_extension;
$target_file = $target_dir . $safe_filename;
// --- 3. 验证文件大小 ---
$max_filesize = 5 * 1024 * 1024; // 5 MB 限制(示例)
if ($_FILES["userFile"]["size"] > $max_filesize) {
echo "Error: File is too large (Max: " . ($max_filesize / 1024 / 1024) . " MB).";
exit;
}
// --- 4. 验证文件类型(更安全的方式) ---
// 不要仅仅依赖文件扩展名或客户端提供的 mime 类型。
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$file_mime_type = $finfo->file($_FILES["userFile"]["tmp_name"]);
if (!in_array($file_mime_type, $allowed_mime_types)) {
echo "Error: Invalid file type ('" . htmlspecialchars($file_mime_type) ."'). Allowed types: JPG, PNG, GIF, PDF.";
exit;
}
// 可选:使用 getimagesize 函数对图片文件进行更严格的检查
if (strpos($file_mime_type, 'image/') === 0) {
if (getimagesize($_FILES["userFile"]["tmp_name"]) === false) {
echo "Error: File is not a valid image.";
exit;
}
}
// --- 5. 移动上传的文件 ---
// 出于安全考虑,请使用 move_uploaded_file()
if (move_uploaded_file($_FILES["userFile"]["tmp_name"], $target_file)) {
echo "The file " . htmlspecialchars($original_filename) . " has been uploaded successfully as " . htmlspecialchars($safe_filename) . ".";
} else {
echo "Error: There was a problem moving the uploaded file. Check permissions and paths.";
}
} else {
// 处理表单未正确提交或文件未上传的情况
echo "Invalid request or no file uploaded.";
}
?>

安全措施解释:

  • 检查错误: 始终首先检查 $_FILES['userFile']['error']。
  • 安全文件名: 切勿直接使用 $_FILES['userFile']['name'] 创建目标路径。它可能包含恶意字符(../),用于目录遍历攻击。使用 basename() 是一个基本步骤,但生成一个唯一、随机的文件名(例如使用 uniqid() 或散列)要安全得多。
  • 验证大小: 根据脚本中定义的合理限制检查文件大小(并确保它在 php.ini 设置的限制内)。
  • 验证类型: 不要信任文件扩展名(pathinfo)或客户端提供的 MIME 类型($_FILES['userFile']['type'])。使用服务器端方法,如 finfo(Fileinfo 扩展)或 mime_content_type(),从文件内容中确定实际的 MIME 类型。对于图片,getimagesize() 可以提供额外的额外验证。
  • 使用 move_uploaded_file(): 此函数专门用于处理上传文件。它会检查文件是否确实通过 HTTP POST 上传,防止攻击者诱骗脚本移动服务器上已有的任意文件。
  • 安全目录: 如果可能,将上传的文件存储在 Web 根目录之外,或者如果存储在内部,请配置 Web 服务器以防止直接执行上传目录中的文件(例如,使用 .htaccess 或 Nginx 配置)。

注意:确保目标目录(本例中的 uploads/)存在,并且 Web 服务器进程对此目录拥有写入权限。

要处理使用相同输入名称上传的多个文件(例如,<input type="file" name="userFiles[]" multiple>),PHP 会以不同的方式组织 $_FILES 数组。您需要遍历结构化的 name、type、tmp_name、error 和 size 数组。

查阅官方 PHP 文档以获取更多详细信息: