POST 方法上传
概述
文件上传是 Web 开发中最常见的功能之一。PHP 通过 $_FILES 超全局数组获取上传的文件信息,使用 move_uploaded_file() 将文件从临时目录移动到目标位置。is_uploaded_file() 用于验证文件是否通过 HTTP POST 上传。PHP 配置中的 upload_max_filesize 和 post_max_size 决定了上传限制。
适用场景
- 用户头像上传
- 文档/图片管理
- CSV 数据导入
- 附件上传功能
基础概念
$_FILES 数组结构
php
<?php
// 单文件上传:HTML <input type="file" name="avatar">
$_FILES = [
'avatar' => [
'name' => 'photo.jpg', // 原始文件名
'full_path' => '/path/to/photo.jpg', // 完整路径(PHP 8.1+)
'type' => 'image/jpeg', // MIME 类型(客户端提供)
'tmp_name' => '/tmp/phpXXXXXX', // 临时文件路径
'error' => UPLOAD_ERR_OK, // 错误码
'size' => 123456, // 文件大小(字节)
],
];核心函数
| 函数 | 功能 | 返回值 |
|---|---|---|
move_uploaded_file() | 移动上传的文件 | bool |
is_uploaded_file() | 验证上传文件 | bool |
相关配置
| 配置项 | 默认值 | 说明 |
|---|---|---|
file_uploads | On | 是否允许 HTTP 上传 |
upload_max_filesize | 2M | 单个文件最大大小 |
post_max_size | 8M | POST 数据最大大小 |
max_file_uploads | 20 | 同时上传文件数上限 |
upload_tmp_dir | 系统临时目录 | 上传临时目录 |
PHP 8.1 新增
$_FILES 新增 full_path 键,包含文件的完整路径。仅当浏览器提供时才有值。
语法与代码示例
HTML 表单
html
<!-- 单文件上传 -->
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/*">
<button type="submit">上传</button>
</form>
<!-- 限制文件大小(HTML5,单位字节) -->
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="1048576"> <!-- 1MB -->
<input type="file" name="document" required>
<button type="submit">上传</button>
</form>php
<?php
// upload.php - 基本文件上传处理
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Method Not Allowed');
}
$uploadDir = '/var/www/html/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (!isset($_FILES['avatar']) || $_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
die('上传失败');
}
$file = $_FILES['avatar'];
// 验证是上传的文件
if (!is_uploaded_file($file['tmp_name'])) {
die('非法上传');
}
// 生成安全文件名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$destPath = $uploadDir . $newName;
// 移动文件
if (move_uploaded_file($file['tmp_name'], $destPath)) {
echo "上传成功: {$newName}\n";
echo "文件大小: {$file['size']} 字节\n";
} else {
die('文件移动失败');
}完整上传处理
php
<?php
declare(strict_types=1);
class FileUploader
{
private string $uploadDir;
private int $maxSize;
private array $allowedMimes;
private int $maxNameLength;
public function __construct(
string $uploadDir,
int $maxSize = 10485760, // 10MB
array $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
int $maxNameLength = 255
) {
$this->uploadDir = rtrim($uploadDir, '/\\');
$this->maxSize = $maxSize;
$this->allowedMimes = $allowedMimes;
$this->maxNameLength = $maxNameLength;
if (!is_dir($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
public function upload(array $file): array
{
// 检查错误
if (!isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException($this->getErrorMessage($file['error'] ?? UPLOAD_ERR_NO_FILE));
}
// 检查上传
if (!is_uploaded_file($file['tmp_name'])) {
throw new RuntimeException('非法上传文件');
}
// 检查文件大小
if ($file['size'] > $this->maxSize) {
throw new RuntimeException("文件大小超出限制: {$this->maxSize} 字节");
}
// 检查 MIME 类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $this->allowedMimes, true)) {
throw new RuntimeException("不允许的文件类型: {$mime}");
}
// 生成安全文件名
$ext = $this->getExtension($mime);
$filename = $this->generateFilename() . '.' . $ext;
$destPath = $this->uploadDir . DIRECTORY_SEPARATOR . $filename;
// 移动文件
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
throw new RuntimeException('文件保存失败');
}
// 设置权限
chmod($destPath, 0644);
return [
'filename' => $filename,
'path' => $destPath,
'mime_type' => $mime,
'size' => $file['size'],
'original' => $this->sanitizeName($file['name']),
];
}
private function getExtension(string $mime): string
{
$map = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'application/pdf' => 'pdf',
];
return $map[$mime] ?? 'bin';
}
private function generateFilename(): string
{
return date('Ymd_His') . '_' . bin2hex(random_bytes(8));
}
private function sanitizeName(string $name): string
{
$name = basename($name);
$name = preg_replace('/[^a-zA-Z0-9._-]/', '_', $name);
return substr($name, 0, $this->maxNameLength);
}
private function getErrorMessage(int $code): string
{
$messages = [
UPLOAD_ERR_INI_SIZE => '文件超过 upload_max_filesize 限制',
UPLOAD_ERR_FORM_SIZE => '文件超过表单 MAX_FILE_SIZE 限制',
UPLOAD_ERR_PARTIAL => '文件上传不完整',
UPLOAD_ERR_NO_FILE => '没有文件被上传',
UPLOAD_ERR_NO_TMP_DIR => '找不到临时目录',
UPLOAD_ERR_CANT_WRITE => '写入磁盘失败',
UPLOAD_ERR_EXTENSION => '上传被扩展阻止',
];
return $messages[$code] ?? '未知上传错误';
}
}
// 使用示例
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
$uploader = new FileUploader('/var/www/html/uploads');
try {
$result = $uploader->upload($_FILES['avatar']);
echo "上传成功: {$result['filename']}\n";
} catch (RuntimeException $e) {
echo "上传失败: {$e->getMessage()}\n";
}
}is_uploaded_file 验证
php
<?php
// is_uploaded_file 检查文件是否通过 HTTP POST 上传
// 这是安全措施,防止攻击者通过构造 $_FILES 数组来利用文件路径
$tmpName = $_FILES['document']['tmp_name'];
if (!is_uploaded_file($tmpName)) {
http_response_code(400);
die('非法上传请求');
}
// 安全移动
$destDir = '/var/www/html/uploads/documents/';
$newName = md5_file($tmpName) . '.' . pathinfo($_FILES['document']['name'], PATHINFO_EXTENSION);
move_uploaded_file($tmpName, $destDir . $newName);实战示例
配置文件上传限制
bash
# php.ini 文件上传相关配置ini
; 启用文件上传
file_uploads = On
; 单个文件最大 20MB
upload_max_filesize = 20M
; POST 数据最大 50MB(应大于 upload_max_filesize * 最大文件数量)
post_max_size = 50M
; 最大同时上传文件数
max_file_uploads = 20
; 临时上传目录
upload_tmp_dir = /tmp
; 脚本最大执行时间(秒)
max_execution_time = 60
; 脚本最大内存
memory_limit = 256Mphp
<?php
// 运行时获取配置
function getUploadLimits(): array
{
return [
'uploadMaxFilesize' => ini_get('upload_max_filesize'),
'postMaxSize' => ini_get('post_max_size'),
'maxFileUploads' => ini_get('max_file_uploads'),
'uploadTmpDir' => ini_get('upload_tmp_dir'),
'maxExecutionTime' => ini_get('max_execution_time'),
'memoryLimit' => ini_get('memory_limit'),
];
}
// 转为字节数
function parseSizeString(string $size): int
{
$unit = strtolower($size[-1]);
$value = (int)$size;
return match ($unit) {
'g' => $value * 1073741824,
'm' => $value * 1048576,
'k' => $value * 1024,
default => $value,
};
}
// 前端友好的限制信息
$limits = getUploadLimits();
echo "最大文件大小: {$limits['uploadMaxFilesize']}\n";
echo "最大 POST 大小: {$limits['postMaxSize']}\n";分块上传处理(前端配合)
php
<?php
declare(strict_types=1);
/**
* 处理分块上传
* 前端将大文件分为多个块上传
*/
class ChunkUploader
{
private string $chunkDir;
public function __construct(string $baseDir)
{
$this->chunkDir = $baseDir . DIRECTORY_SEPARATOR . 'chunks';
if (!is_dir($this->chunkDir)) {
mkdir($this->chunkDir, 0755, true);
}
}
public function uploadChunk(array $file, string $fileId, int $chunkIndex, int $totalChunks): array
{
// 保存分块
$chunkPath = $this->chunkDir . DIRECTORY_SEPARATOR . "{$fileId}_{$chunkIndex}";
move_uploaded_file($file['tmp_name'], $chunkPath);
// 检查是否所有分块都已上传
$uploadedChunks = glob($this->chunkDir . DIRECTORY_SEPARATOR . "{$fileId}_*");
$uploadedCount = count($uploadedChunks);
if ($uploadedCount < $totalChunks) {
return ['status' => 'partial', 'progress' => $uploadedCount / $totalChunks];
}
// 合并分块
$finalPath = $this->mergeChunks($fileId, $totalChunks);
$this->cleanupChunks($fileId);
return [
'status' => 'complete',
'path' => $finalPath,
'size' => filesize($finalPath),
];
}
private function mergeChunks(string $fileId, int $totalChunks): string
{
$finalDir = dirname($this->chunkDir) . DIRECTORY_SEPARATOR . 'completed';
if (!is_dir($finalDir)) {
mkdir($finalDir, 0755, true);
}
$finalPath = $finalDir . DIRECTORY_SEPARATOR . $fileId;
$destHandle = fopen($finalPath, 'wb');
if ($destHandle === false) {
throw new RuntimeException('无法创建最终文件');
}
for ($i = 0; $i < $totalChunks; $i++) {
$chunkPath = $this->chunkDir . DIRECTORY_SEPARATOR . "{$fileId}_{$i}";
$chunkData = file_get_contents($chunkPath);
fwrite($destHandle, $chunkData);
}
fclose($destHandle);
return $finalPath;
}
private function cleanupChunks(string $fileId): void
{
$chunks = glob($this->chunkDir . DIRECTORY_SEPARATOR . "{$fileId}_*");
foreach ($chunks as $chunk) {
unlink($chunk);
}
}
}注意事项
move_uploaded_file 安全性
php
<?php
// move_uploaded_file 会自动检查:
// 1. 文件是否由 PHP 上传机制创建
// 2. 源文件是否为合法的上传临时文件
// 不要用 rename() 或 copy() 替代 move_uploaded_file()
// rename() 没有安全检查,可能导致任意文件操作漏洞
// 不好
rename($_FILES['avatar']['tmp_name'], '/var/www/html/uploads/photo.jpg');
// 好
move_uploaded_file($_FILES['avatar']['tmp_name'], '/var/www/html/uploads/photo.jpg');安全警告
始终使用 move_uploaded_file() 而非 copy() 或 rename() 来处理上传文件。move_uploaded_file() 内置了安全检查,确保文件确实是上传的。
临时文件清理
php
<?php
// PHP 会在请求结束后自动清理上传临时文件
// 但长时间运行的脚本需要手动清理
// 检查临时文件是否存在
$tmpFile = $_FILES['document']['tmp_name'];
if (!file_exists($tmpFile)) {
die('临时文件已被清理');
}
// 请求结束后临时文件会自动删除
// 不要依赖临时文件长时间存在最佳实践
1. 完整的上传验证流程
php
<?php
function handleUpload(array $file): string
{
// 1. 验证上传
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('上传错误: ' . $file['error']);
}
// 2. 验证存在
if (!is_uploaded_file($file['tmp_name'])) {
throw new RuntimeException('非法上传');
}
// 3. 验证大小
$maxSize = 10 * 1024 * 1024; // 10MB
if ($file['size'] > $maxSize || $file['size'] === 0) {
throw new RuntimeException('文件大小无效');
}
// 4. 验证 MIME
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, ['image/jpeg', 'image/png'], true)) {
throw new RuntimeException('不支持的文件类型');
}
// 5. 移动文件
$destDir = '/var/www/html/uploads/' . date('Y/m/d');
if (!is_dir($destDir)) {
mkdir($destDir, 0755, true);
}
$newName = bin2hex(random_bytes(16)) . '.' . pathinfo($file['name'], PATHINFO_EXTENSION);
$destPath = $destDir . DIRECTORY_SEPARATOR . $newName;
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
throw new RuntimeException('文件保存失败');
}
chmod($destPath, 0644);
return $destPath;
}2. 上传目录禁止执行 PHP
apache
# Apache .htaccess
<FilesMatch "\.php$">
Deny from all
</FilesMatch>
# nginx 配置
location ~* ^/uploads/ {
location ~ \.php$ {
deny all;
}
}