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_filecopy 有什么区别?
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 才准确。

📖 小节

📝 作业

  1. 写一个头像上传功能:限制 1MB 以内、仅允许 jpg/png/gif、上传后在页面显示头像预览。
  2. 给上传功能添加完整的错误提示(文件太大 / 类型不对 / 没有选文件),并在失败时保留表单状态。
  3. 实现批量上传5张图片:上传后在页面以缩略图网格展示。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏