首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 图片上传类完整实现:类型校验、大小限制、随机命名、目录自建、错误码规范

     編輯:PHP 綜合知識

核心需求清单

  1. 仅允许图片 MIME:jpeg, png, gif, webp
  2. 文件大小限制 (默认 2MB)
  3. 防止文件名冲突:随机哈希 + 时间戳 + 原后缀
  4. 上传目录按日期自动创建:uploads/2024/01/15/
  5. 移动文件前二次校验 (MIME + 扩展名)
  6. 统一返回结构:['code', 'msg', 'data' => ['path', 'url', 'size', 'mime']]

UploadedFile 封装类

<?php
namespace App\Service;

use Exception;

class UploadedFile
{
    private array $file;
    private array $config;

    public const ERROR_OK = 0;
    public const ERROR_NO_FILE = 1;
    public const ERROR_UPLOAD_FAILED = 2;
    public const ERROR_INVALID_TYPE = 3;
    public const ERROR_EXCEED_SIZE = 4;
    public const ERROR_MOVE_FAILED = 5;
    public const ERROR_DIR_CREATE = 6;

    private static array $errorMessages = [
        self::ERROR_OK => '上传成功',
        self::ERROR_NO_FILE => '未接收到文件',
        self::ERROR_UPLOAD_FAILED => '上传失败 (PHP错误码: %d)',
        self::ERROR_INVALID_TYPE => '不允许的文件类型: %s',
        self::ERROR_EXCEED_SIZE => '文件大小超限: %s 字节 (限制 %s 字节)',
        self::ERROR_MOVE_FAILED => '文件移动失败',
        self::ERROR_DIR_CREATE => '上传目录创建失败',
    ];

    public function __construct(array $file, array $config = [])
    {
        $this->file = $file;
        $this->config = array_merge([
            'max_size' => 2 * 1024 * 1024, // 2MB
            'allowed_mimes' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
            'allowed_exts' => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
            'upload_root' => $_SERVER['DOCUMENT_ROOT'] . '/uploads',
            'url_prefix' => '/uploads',
            'date_dir' => true, // 按 Y/m/d 创建子目录
        ], $config);
    }

    public function save(): array
    {
        // 1. 基础校验
        if (empty($this->file['tmp_name']) || !is_uploaded_file($this->file['tmp_name'])) {
            return $this->error(self::ERROR_NO_FILE);
        }

        // 2. PHP 上传错误码
        if ($this->file['error'] !== UPLOAD_ERR_OK) {
            return $this->error(self::ERROR_UPLOAD_FAILED, $this->file['error']);
        }

        // 3. 大小校验
        if ($this->file['size'] > $this->config['max_size']) {
            return $this->error(self::ERROR_EXCEED_SIZE, $this->file['size'], $this->config['max_size']);
        }

        // 4. MIME 校验 (二次检测,防伪造)
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $realMime = finfo_file($finfo, $this->file['tmp_name']);
        finfo_close($finfo);

        if (!in_array($realMime, $this->config['allowed_mimes'], true)) {
            return $this->error(self::ERROR_INVALID_TYPE, $realMime);
        }

        // 5. 扩展名校验
        $ext = strtolower(pathinfo($this->file['name'], PATHINFO_EXTENSION));
        if (!in_array($ext, $this->config['allowed_exts'], true)) {
            return $this->error(self::ERROR_INVALID_TYPE, $ext);
        }

        // 6. 目标目录
        $dateDir = $this->config['date_dir'] ? date('Y/m/d') : '';
        $saveDir = $this->config['upload_root'] . ($dateDir ? '/' . $dateDir : '');
        $urlDir = $this->config['url_prefix'] . ($dateDir ? '/' . $dateDir : '');

        if (!is_dir($saveDir)) {
            if (!mkdir($saveDir, 0755, true) && !is_dir($saveDir)) {
                return $this->error(self::ERROR_DIR_CREATE);
            }
        }

        // 7. 生成唯一文件名
        $filename = $this->generateFilename($ext);
        $savePath = $saveDir . '/' . $filename;
        $url = $urlDir . '/' . $filename;

        // 8. 移动文件
        if (!move_uploaded_file($this->file['tmp_name'], $savePath)) {
            return $this->error(self::ERROR_MOVE_FAILED);
        }

        // 9. 设置权限 (可选)
        @chmod($savePath, 0644);

        return [
            'code' => self::ERROR_OK,
            'msg' => self::$errorMessages[self::ERROR_OK],
            'data' => [
                'path' => $savePath,
                'url' => $url,
                'size' => $this->file['size'],
                'mime' => $realMime,
                'ext' => $ext,
                'original_name' => $this->file['name'],
            ],
        ];
    }

    private function generateFilename(string $ext): string
    {
        // 随机 16 字符 + 时间戳毫秒 + 后缀
        $random = bin2hex(random_bytes(8));
        $time = (int)(microtime(true) * 1000);
        return "{$random}_{$time}.{$ext}";
    }

    private function error(int $code, ...$params): array
    {
        $msg = self::$errorMessages[$code] ?? '未知错误';
        if ($params) $msg = vsprintf($msg, $params);
        return ['code' => $code, 'msg' => $msg, 'data' => null];
    }
}

控制器使用示例

<?php
namespace app\controller;

use App\Service\UploadedFile;

class UploadController
{
    public function avatar()
    {
        $file = $_FILES['avatar'] ?? null;
        if (!$file) {
            return json(['code' => 1, 'msg' => '请选择文件']);
        }

        $uploader = new UploadedFile($file, [
            'max_size' => 5 * 1024 * 1024, // 头像放宽 5MB
            'upload_root' => '/var/www/uploads',
            'url_prefix' => 'https://cdn.example.com/uploads',
        ]);

        $result = $uploader->save();
        return json($result);
    }

    public function editorImage()
    {
        // 富文本编辑器上传 (如 WangEditor, TinyMCE)
        $file = $_FILES['file'] ?? null;
        $uploader = new UploadedFile($file, [
            'allowed_mimes' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'],
            'max_size' => 10 * 1024 * 1024,
        ]);
        $result = $uploader->save();

        // 编辑器要求特定格式
        if ($result['code'] === 0) {
            return json(['errno' => 0, 'data' => ['url' => $result['data']['url']]]);
        }
        return json(['errno' => 1, 'message' => $result['msg']]);
    }
}

前端表单规范

<form action="/upload/avatar" method="post" enctype="multipart/form-data">
    <input type="file" name="avatar" accept="image/jpeg,image/png,image/gif,image/webp" required>
    <button type="submit">上传头像</button>
</form>

<script>
// AJAX 上传示例
document.querySelector('form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const res = await fetch(e.target.action, {method: 'POST', body: formData});
    const data = await res.json();
    if (data.code === 0) {
        alert('上传成功: ' + data.data.url);
    } else {
        alert('失败: ' + data.msg);
    }
});
</script>

安全加固清单

风险对策
脚本伪装成图片 (.php.jpg)1. 仅信任 finfo_file() 检测 MIME 2. 存储目录配置 Nginx location ~ \.php$ { deny all; }
文件名路径穿越完全忽略原文件名,生成随机名
大文件撑爆内存/磁盘Nginx client_max_body_size 10M; PHP upload_max_filesize/post_max_size
并发同名覆盖random_bytes(8) + 毫秒时间戳,冲突概率极低
图片含恶意代码 (Exif)可选:imagick 重新编码 $img->stripImage() 移除元数据

结语

图片上传看似简单,**安全细节决定成败**。上述类封装了:MIME 二次校验、随机命名、日期分目录、错误码规范、前端对接格式。生产环境建议:上传后异步推送到对象存储 (S3/OSS/COS),Web 服务器仅作临时中转,实现无状态水平扩展。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved