PHP: 文件上传
用户头像、简历附件、图片分享——每个真正的 Web 应用都要处理文件上传。PHP 让文件上传变得直观,但安全细节稍不注意就是大坑。
1. 前置条件
(1) HTML 表单要求
HTML
<!-- 文件上传表单的三个必须 -->
<form method="POST" enctype="multipart/form-data">
<input type="file" name="avatar">
<button type="submit">上传</button>
</form>
| 要求 | 说明 |
|---|---|
method="POST" |
文件必须走 POST |
enctype="multipart/form-data" |
必须写! 否则只上传文件名 |
type="file" |
文件选择器 |
🔥 易错: 忘记
enctype="multipart/form-data" 是最常见的文件上传错误——PHP 只会收到文件名,收不到文件内容。
(2) 创建上传目录
在 myphp/ 下创建 uploads/ 文件夹,并确保 PHP 有写入权限:
PHP
<?php
// 在脚本中自动创建上传目录
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
?>
2. $_FILES 详解
PHP
<?php
// 上传一个文件后,$_FILES 的结构:
print_r($_FILES['avatar']);
/*
Array (
[name] => photo.jpg // 原始文件名
[type] => image/jpeg // 文件 MIME 类型
[tmp_name] => C:\xampp\tmp\php123.tmp // 临时文件路径
[error] => 0 // 错误码(0=成功)
[size] => 85469 // 文件大小(字节)
)
*/
?>
| 键 | 含义 | 注意 |
|---|---|---|
name |
原始文件名 | 不可信,用户可伪造 |
type |
MIME 类型 | 浏览器报告,不可信 |
tmp_name |
服务器临时文件 | 脚本结束自动删除 |
error |
错误码 | 0=成功,其他=出错 |
size |
文件大小(字节) | 可用来做大小限制 |
(1) error 错误码
| 码 | 常量 | 含义 |
|---|---|---|
| 0 | UPLOAD_ERR_OK |
✅ 上传成功 |
| 1 | UPLOAD_ERR_INI_SIZE |
超过 php.ini 的 upload_max_filesize |
| 2 | UPLOAD_ERR_FORM_SIZE |
超过表单 MAX_FILE_SIZE |
| 3 | UPLOAD_ERR_PARTIAL |
文件部分上传 |
| 4 | UPLOAD_ERR_NO_FILE |
没选文件 |
| 6 | UPLOAD_ERR_NO_TMP_DIR |
没有临时文件夹 |
| 7 | UPLOAD_ERR_CANT_WRITE |
磁盘写入失败 |
3. move_uploaded_file() — 核心函数
PHP 收到文件后存在临时目录,必须显式移动到目标位置,否则脚本结束就丢了:
PHP
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$file = $_FILES['avatar'];
$uploadDir = __DIR__ . '/uploads/';
if ($file['error'] !== UPLOAD_ERR_OK) {
echo "上传失败,错误码:" . $file['error'];
} else {
$targetPath = $uploadDir . $file['name'];
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
echo "上传成功!文件保存在:{$targetPath}";
} else {
echo "移动文件失败";
}
}
}
?>
▶ 示例:基本文件上传(带错误处理)
PHP
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$file = $_FILES['avatar'];
$uploadDir = __DIR__ . '/uploads/';
if ($file['error'] === UPLOAD_ERR_OK) {
$targetPath = $uploadDir . $file['name'];
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
echo "<p style='color:green'>上传成功!</p>";
}
} elseif ($file['error'] === UPLOAD_ERR_NO_FILE) {
echo "<p style='color:red'>请选择文件</p>";
} else {
echo "<p style='color:red'>上传失败,错误码:{$file['error']}</p>";
}
}
?>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="avatar">
<button type="submit">上传</button>
</form>
4. 安全验证(关键!)
(1) 4.1 文件大小限制
PHP
<?php
$maxSize = 2 * 1024 * 1024; // 2MB
if ($file['size'] > $maxSize) {
die("文件太大,不能超过2MB");
}
?>
(2) 4.2 文件扩展名白名单
PHP
<?php
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed)) {
die("不允许的文件类型,仅支持:" . implode(', ', $allowed));
}
?>
💡 提示: 用白名单(只允许已知安全的类型),不要用黑名单(禁止 .php, .exe 等)。攻击者总能找到黑名单漏掉的危险扩展名。
(3) 4.3 生成安全文件名
PHP
<?php
// ❌ 直接使用用户上传的文件名是危险的
// $targetPath = $uploadDir . $file['name'];
// ✅ 生成唯一安全的文件名
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$safeName = uniqid('img_') . '.' . $ext;
$targetPath = $uploadDir . $safeName;
?>
▶ 示例:完整的安全上传
PHP
<?php
function handleUpload(array $file, string $uploadDir): array {
$maxSize = 2 * 1024 * 1024; // 2MB
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
// 1. 检查错误
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'message' => '上传失败'];
}
// 2. 检查大小
if ($file['size'] > $maxSize) {
return ['success' => false, 'message' => '文件不能超过2MB'];
}
// 3. 检查扩展名(白名单)
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed)) {
return ['success' => false, 'message' => '仅支持图片文件'];
}
// 4. 检查文件真实类型(Magic Bytes,比扩展名可靠)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mimeType, $allowedMimes)) {
return ['success' => false, 'message' => '文件类型不匹配'];
}
// 5. 生成安全文件名
$safeName = uniqid('upload_') . '.' . $ext;
$targetPath = $uploadDir . $safeName;
// 6. 移动文件
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
return [
'success' => true,
'message' => '上传成功',
'path' => $targetPath,
'name' => $safeName,
];
}
return ['success' => false, 'message' => '保存文件失败'];
}
// 使用
$result = handleUpload($_FILES['avatar'], __DIR__ . '/uploads/');
echo $result['message'];
?>
5. 多文件上传
PHP
<form method="POST" enctype="multipart/form-data">
<input type="file" name="photos[]" multiple>
<button type="submit">上传多张</button>
</form>
PHP
<?php
$uploadDir = __DIR__ . '/uploads/';
$results = [];
foreach ($_FILES['photos']['name'] as $i => $name) {
// 重新组装单文件数组
$singleFile = [
'name' => $_FILES['photos']['name'][$i],
'type' => $_FILES['photos']['type'][$i],
'tmp_name' => $_FILES['photos']['tmp_name'][$i],
'error' => $_FILES['photos']['error'][$i],
'size' => $_FILES['photos']['size'][$i],
];
$results[] = handleUpload($singleFile, $uploadDir);
}
foreach ($results as $r) {
echo $r['message'] . "<br>";
}
?>
💡 提示: PHP 多文件上传的
$_FILES 结构是"列式"的——所有文件的 name 在一个数组,所有 size 在另一个数组。需要手动重组为每个文件的数组。
6. 图片上传后显示
PHP
<?php
// 列出 uploads 目录中的图片
$images = glob(__DIR__ . '/uploads/*.{jpg,jpeg,png,gif,webp}', GLOB_BRACE);
?>
<h3>已上传的图片</h3>
<div style="display:flex;flex-wrap:wrap;gap:10px">
<?php foreach ($images as $img): ?>
<img src="uploads/<?= basename($img) ?>" width="150">
<?php endforeach; ?>
</div>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="avatar">
<button type="submit">上传</button>
</form>
❓ 常见问题
Q
move_uploaded_file 和 copy 有什么区别?A
move_uploaded_file 会检查文件是否是通过 HTTP POST 上传的(安全),普通 copy() 可以被利用来复制服务器上的系统文件。文件上传永远用 move_uploaded_file。Q 上传大文件总是失败?
A 检查三处限制:(1) php.ini 中的
upload_max_filesize(默认 2M);(2) post_max_size(需 > upload_max_filesize);(3) 表单中的 MAX_FILE_SIZE(可选)。修改 php.ini 后要重启 Apache/Nginx。Q
$_FILES['avatar']['type'] 可靠吗?A 不可靠,这是浏览器报告的 MIME 类型,可以伪造。用
finfo_file() 检测文件的真实 Magic Bytes 才准确。📖 小节
- 表单必加
enctype="multipart/form-data" $_FILES['field']['tmp_name']是临时文件,用move_uploaded_file()移动- 验证四步:大小限制 → 扩展名白名单 → MIME 验证 → 安全重命名
finfo_file()检查文件真实类型(Magic Bytes),比扩展名可靠- 多文件上传需重组
$_FILES结构 - 安全规则:白名单 > 黑名单、生成新文件名、限制文件大小
📝 作业
- 写一个头像上传功能:限制 1MB 以内、仅允许 jpg/png/gif、上传后在页面显示头像预览。
- 给上传功能添加完整的错误提示(文件太大 / 类型不对 / 没有选文件),并在失败时保留表单状态。
- 实现批量上传5张图片:上传后在页面以缩略图网格展示。