文件上传,是Web开发中非常常见的需求。用户头像上传、文章配图上传、附件上传、视频上传……这些都需要用到文件上传功能。
PHP对文件上传有很好的支持,通过$FILES超全局变量和moveuploaded_file()函数,就能实现基本的文件上传。但文件上传,也是安全风险很高的功能,如果处理不当,可能会导致文件上传漏洞、服务器被入侵、数据泄露等严重问题。
今天,我们来详细学习PHP文件上传,从基础用法到安全加固,从单文件上传到多文件上传,从图片处理到大文件上传,帮你掌握这个常用但需要小心的功能。
文件上传基础
表单设置
要实现文件上传,HTML表单需要做特殊设置:
- method必须是POST
- enctype必须是multipart/form-data
- 表单中要有type="file"的input元素
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="avatar">
<input type="submit" value="上传">
</form>enctype="multipart/form-data"是必须的,它告诉浏览器,表单数据包含文件,需要用multipart格式编码。如果不设置这个属性,文件上传不会生效。
$_FILES超全局变量
PHP通过$_FILES超全局变量获取上传的文件信息。
print_r($_FILES['avatar']);
// Array (
// [name] => photo.jpg // 原始文件名
// [type] => image/jpeg // MIME类型
// [tmp_name] => /tmp/phpXXX.tmp // 临时文件路径
// [error] => 0 // 错误码
// [size] => 12345 // 文件大小(字节)
// )每个上传的文件,都有这5个字段:
- name:原始文件名(用户电脑上的文件名)
- type:MIME类型(浏览器提供,不可信)
- tmp_name:服务器上的临时文件路径
- error:错误码(0表示成功,其他表示错误)
- size:文件大小(字节)
错误码
$_FILES['file']['error']的可能值:
- UPLOADERROK (0):上传成功
- UPLOADERRINISIZE (1):文件超过php.ini中uploadmax_filesize的限制
- UPLOADERRFORMSIZE (2):文件超过表单中MAXFILE_SIZE的限制
- UPLOADERRPARTIAL (3):文件只有部分被上传
- UPLOADERRNO_FILE (4):没有文件被上传
- UPLOADERRNOTMPDIR (6):找不到临时文件夹
- UPLOADERRCANT_WRITE (7):文件写入失败
- UPLOADERREXTENSION (8):PHP扩展停止了文件上传
if ($_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
switch ($_FILES['avatar']['error']) {
case UPLOAD_ERR_INI_SIZE:
echo '文件超过服务器限制';
break;
case UPLOAD_ERR_FORM_SIZE:
echo '文件超过表单限制';
break;
case UPLOAD_ERR_PARTIAL:
echo '文件只有部分被上传';
break;
case UPLOAD_ERR_NO_FILE:
echo '没有文件被上传';
break;
default:
echo '上传失败';
}
exit;
}moveuploadedfile()
上传的文件,先保存在临时目录中,脚本执行结束后会自动删除。要保存文件,需要用moveuploadedfile()函数把临时文件移动到目标位置。
$tmpFile = $_FILES['avatar']['tmp_name'];
$destFile = 'uploads/' . $_FILES['avatar']['name'];
if (move_uploaded_file($tmpFile, $destFile)) {
echo '上传成功';
} else {
echo '上传失败';
}moveuploadedfile()会检查文件是否是通过HTTP POST上传的,确保安全。不要用copy()或rename()来移动上传文件,因为它们不会做安全检查。
基础文件上传示例
<?php
// upload.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 检查是否有文件上传
if (!isset($_FILES['avatar']) || $_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
echo '请选择要上传的文件';
exit;
}
$file = $_FILES['avatar'];
// 基本配置
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
$maxSize = 2 * 1024 * 1024; // 2MB
$uploadDir = 'uploads/';
// 检查文件大小
if ($file['size'] > $maxSize) {
echo '文件大小不能超过2MB';
exit;
}
// 检查文件类型(注意:type不可信,需要进一步验证)
if (!in_array($file['type'], $allowedTypes)) {
echo '只支持JPG、PNG、GIF格式';
exit;
}
// 生成安全的文件名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = uniqid() . '.' . $ext;
$destPath = $uploadDir . $newName;
// 确保上传目录存在
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// 移动文件
if (move_uploaded_file($file['tmp_name'], $destPath)) {
echo '上传成功:' . $newName;
} else {
echo '上传失败';
}
}
?>
<form method="post" enctype="multipart/form-data">
<input type="file" name="avatar">
<input type="submit" value="上传">
</form>这是一个基础的文件上传示例,但还不够安全。下面我们来详细讲解安全加固。
文件上传安全加固
文件上传,是Web安全中风险很高的功能。攻击者可以通过上传恶意文件(如PHP脚本、WebShell),获取服务器权限。所以,文件上传的安全加固非常重要。
1. 验证文件类型
不要信任$_FILES['file']['type'],这个值是浏览器提供的,可以被伪造。需要用更可靠的方式验证文件类型。
方法1:用finfo验证MIME类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $_FILES['avatar']['tmp_name']);
finfo_close($finfo);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mimeType, $allowedTypes)) {
echo '文件类型不允许';
exit;
}finfo通过读取文件内容来判断MIME类型,比浏览器提供的type更可靠。
方法2:用getimagesize验证图片
如果是图片上传,可以用getimagesize()验证:
$imageInfo = getimagesize($_FILES['avatar']['tmp_name']);
if ($imageInfo === false) {
echo '不是有效的图片文件';
exit;
}
// $imageInfo[2] 是图片类型常量(IMAGETYPE_JPEG, IMAGETYPE_PNG等)
$allowedTypes = [IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_GIF];
if (!in_array($imageInfo[2], $allowedTypes)) {
echo '只支持JPG、PNG、GIF格式';
exit;
}方法3:验证文件头(魔数)
每种文件类型,都有特定的文件头(魔数)。可以读取文件的前几个字节,验证文件头。
// JPEG文件头:FF D8 FF
// PNG文件头:89 50 4E 47
// GIF文件头:47 49 46 38
$handle = fopen($_FILES['avatar']['tmp_name'], 'rb');
$header = fread($handle, 4);
fclose($handle);
// 验证JPEG
if (bin2hex($header) === 'ffd8ffe0' || bin2hex($header) === 'ffd8ffe1') {
// 是JPEG
}2. 重命名文件
不要使用用户提供的原始文件名,因为:
- 原始文件名可能包含特殊字符,导致路径遍历漏洞
- 原始文件名可能是中文,编码问题可能导致乱码
- 原始文件名可能重复,导致覆盖
应该生成随机的、唯一的文件名:
// 方法1:uniqid + 扩展名
$ext = pathinfo($_FILES['avatar']['name'], PATHINFO_EXTENSION);
$newName = uniqid() . '.' . $ext;
// 方法2:md5(uniqid + 随机数)
$newName = md5(uniqid() . mt_rand()) . '.' . $ext;
// 方法3:日期 + 随机数
$newName = date('YmdHis') . '_' . mt_rand(1000, 9999) . '.' . $ext;注意:扩展名也不能完全信任,需要验证。可以从MIME类型推断扩展名,而不是从原始文件名获取。
3. 存储目录安全
上传文件的存储目录,需要做好安全设置:
- 上传目录不可执行:在Nginx/Apache配置中,设置上传目录不执行PHP脚本。
``nginx # Nginx配置 location /uploads/ { location ~ \.php$ { deny all; # 禁止执行uploads目录下的PHP } } ``
- 上传目录不在Web根目录:把上传文件存储在Web根目录之外,通过PHP脚本读取和输出。
``php // 文件存储在 /var/www/uploads/ (Web根目录之外) // 通过 download.php 读取文件 $file = '/var/www/uploads/' . $filename; header('Content-Type: image/jpeg'); readfile($file); ``
- 设置目录权限:上传目录设置为755,文件设置为644,不要设置为777。
- 用.htaccess保护(Apache):在上传目录创建.htaccess文件,禁止执行脚本。
``apache <Files *.php> deny from all </Files> ``
4. 防止路径遍历
用户提供的文件名,可能包含../等路径遍历字符,导致文件被上传到非预期的目录。
// 危险:直接用用户文件名
$destPath = 'uploads/' . $_FILES['avatar']['name'];
// 安全:只取文件名部分,过滤路径
$filename = basename($_FILES['avatar']['name']);
$destPath = 'uploads/' . $filename;
// 更安全:完全生成新文件名,不用用户文件名
$newName = uniqid() . '.jpg';
$destPath = 'uploads/' . $newName;5. 验证文件大小
在PHP端验证文件大小,不要只依赖前端或php.ini的限制。
$maxSize = 2 * 1024 * 1024; // 2MB
if ($_FILES['avatar']['size'] > $maxSize) {
echo '文件大小不能超过2MB';
exit;
}同时,在php.ini中设置:
upload_max_filesize = 2M
post_max_size = 8M6. 图片二次渲染(图片上传专用)
如果是图片上传,可以对图片进行二次渲染(重新生成图片),彻底清除图片中可能包含的恶意代码(如图片马)。
// 用GD库重新生成图片
$srcImage = imagecreatefromjpeg($_FILES['avatar']['tmp_name']);
$width = imagesx($srcImage);
$height = imagesy($srcImage);
// 创建新图片
$dstImage = imagecreatetruecolor($width, $height);
imagecopyresampled($dstImage, $srcImage, 0, 0, 0, 0, $width, $height, $width, $height);
// 保存新图片
imagejpeg($dstImage, 'uploads/' . $newName, 90);
// 释放内存
imagedestroy($srcImage);
imagedestroy($dstImage);二次渲染,会重新生成图片数据,原来图片中隐藏的PHP代码(图片马)会被清除,非常安全。
7. 防重复上传
可以通过文件哈希(MD5/SHA1)判断文件是否已经上传过,避免重复存储。
$fileHash = md5_file($_FILES['avatar']['tmp_name']);
$existingFile = findFileByHash($fileHash); // 从数据库查询
if ($existingFile) {
echo '文件已存在:' . $existingFile;
} else {
// 保存新文件
move_uploaded_file($_FILES['avatar']['tmp_name'], 'uploads/' . $newName);
// 保存文件哈希到数据库
saveFileHash($fileHash, $newName);
}多文件上传
PHP支持同时上传多个文件。
表单设置
<form method="post" enctype="multipart/form-data">
<input type="file" name="photos[]" multiple>
<input type="submit" value="上传">
</form>注意:input的name要加[],表示数组;multiple属性允许选择多个文件。
处理多文件
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$files = $_FILES['photos'];
$uploadDir = 'uploads/';
$results = [];
for ($i = 0; $i < count($files['name']); $i++) {
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
$results[] = ['name' => $files['name'][$i], 'status' => 'error'];
continue;
}
$tmpName = $files['tmp_name'][$i];
$ext = pathinfo($files['name'][$i], PATHINFO_EXTENSION);
$newName = uniqid() . '.' . $ext;
$destPath = $uploadDir . $newName;
if (move_uploaded_file($tmpName, $destPath)) {
$results[] = ['name' => $newName, 'status' => 'success'];
} else {
$results[] = ['name' => $files['name'][$i], 'status' => 'error'];
}
}
print_r($results);
}多文件上传时,$_FILES的结构是二维数组,需要按索引遍历。
大文件上传
大文件上传,需要特殊处理,因为PHP默认的上传限制较小。
1. 修改php.ini配置
; 允许上传的最大文件大小
upload_max_filesize = 100M
; POST数据的最大大小(必须大于upload_max_filesize)
post_max_size = 110M
; 脚本最大执行时间
max_execution_time = 300
; 脚本最大输入时间
max_input_time = 300
; 内存限制
memory_limit = 256M2. 分片上传
对于超大文件(如视频),推荐用分片上传,把大文件分成多个小块,逐个上传,最后在服务器合并。
分片上传的优点:
- 可以上传超过PHP限制的大文件
- 支持断点续传
- 上传失败可以重试某个分片,不用重新上传整个文件
前端可以用Plupload、Resumable.js、Dropzone.js等库实现分片上传。
3. 进度条
大文件上传,用户需要知道上传进度。可以用HTML5的XMLHttpRequest获取上传进度:
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
var percent = (e.loaded / e.total) * 100;
console.log('上传进度:' + percent + '%');
}
});也可以用APC扩展或Session实现上传进度(PHP 5.4+有内置的上传进度支持)。
图片处理
上传图片后,通常需要做一些处理,如生成缩略图、裁剪、加水印等。
生成缩略图
function createThumbnail($srcPath, $dstPath, $thumbWidth = 200) {
$imageInfo = getimagesize($srcPath);
$width = $imageInfo[0];
$height = $imageInfo[1];
$mime = $imageInfo['mime'];
// 计算缩略图尺寸
$thumbHeight = intval($height * ($thumbWidth / $width));
// 创建源图像
switch ($mime) {
case 'image/jpeg':
$srcImage = imagecreatefromjpeg($srcPath);
break;
case 'image/png':
$srcImage = imagecreatefrompng($srcPath);
break;
case 'image/gif':
$srcImage = imagecreatefromgif($srcPath);
break;
default:
return false;
}
// 创建缩略图
$dstImage = imagecreatetruecolor($thumbWidth, $thumbHeight);
// PNG透明背景处理
if ($mime === 'image/png') {
imagealphablending($dstImage, false);
imagesavealpha($dstImage, true);
}
// 复制并缩放
imagecopyresampled($dstImage, $srcImage, 0, 0, 0, 0, $thumbWidth, $thumbHeight, $width, $height);
// 保存缩略图
switch ($mime) {
case 'image/jpeg':
imagejpeg($dstImage, $dstPath, 85);
break;
case 'image/png':
imagepng($dstImage, $dstPath);
break;
case 'image/gif':
imagegif($dstImage, $dstPath);
break;
}
// 释放内存
imagedestroy($srcImage);
imagedestroy($dstImage);
return true;
}图片裁剪
function cropImage($srcPath, $dstPath, $x, $y, $width, $height) {
$imageInfo = getimagesize($srcPath);
$mime = $imageInfo['mime'];
switch ($mime) {
case 'image/jpeg':
$srcImage = imagecreatefromjpeg($srcPath);
break;
case 'image/png':
$srcImage = imagecreatefrompng($srcPath);
break;
default:
return false;
}
$dstImage = imagecreatetruecolor($width, $height);
imagecopy($dstImage, $srcImage, 0, 0, $x, $y, $width, $height);
imagejpeg($dstImage, $dstPath, 90);
imagedestroy($srcImage);
imagedestroy($dstImage);
return true;
}加水印
function addWatermark($srcPath, $dstPath, $watermarkText = 'Copyright') {
$imageInfo = getimagesize($srcPath);
$mime = $imageInfo['mime'];
$width = $imageInfo[0];
$height = $imageInfo[1];
$srcImage = imagecreatefromjpeg($srcPath);
// 创建水印颜色
$watermarkColor = imagecolorallocatealpha($srcImage, 255, 255, 255, 60);
// 设置字体大小
$fontSize = 5; // 内置字体大小1-5
// 计算水印位置(右下角)
$textWidth = imagefontwidth($fontSize) * strlen($watermarkText);
$textHeight = imagefontheight($fontSize);
$x = $width - $textWidth - 10;
$y = $height - $textHeight - 10;
// 添加文字水印
imagestring($srcImage, $fontSize, $x, $y, $watermarkText, $watermarkColor);
imagejpeg($srcImage, $dstPath, 90);
imagedestroy($srcImage);
return true;
}完整的安全文件上传类
下面是一个完整的、安全的文件上传类:
class FileUpload {
private $allowedTypes = [];
private $maxSize = 2097152; // 2MB
private $uploadDir = 'uploads/';
private $error = '';
public function __construct($config = []) {
if (isset($config['allowed_types'])) {
$this->allowedTypes = $config['allowed_types'];
}
if (isset($config['max_size'])) {
$this->maxSize = $config['max_size'];
}
if (isset($config['upload_dir'])) {
$this->uploadDir = $config['upload_dir'];
}
}
public function upload($fileField) {
// 检查文件是否存在
if (!isset($_FILES[$fileField])) {
$this->error = '没有文件被上传';
return false;
}
$file = $_FILES[$fileField];
// 检查上传错误
if ($file['error'] !== UPLOAD_ERR_OK) {
$this->error = $this->getErrorMessage($file['error']);
return false;
}
// 检查文件大小
if ($file['size'] > $this->maxSize) {
$this->error = '文件大小超过限制';
return false;
}
// 验证文件类型(用finfo)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!empty($this->allowedTypes) && !in_array($mimeType, $this->allowedTypes)) {
$this->error = '文件类型不允许';
return false;
}
// 从MIME类型推断扩展名
$ext = $this->getExtensionFromMime($mimeType);
// 生成唯一文件名
$newName = date('YmdHis') . '_' . uniqid() . '.' . $ext;
$destPath = $this->uploadDir . $newName;
// 确保目录存在
if (!is_dir($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
// 移动文件
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
$this->error = '文件保存失败';
return false;
}
// 设置文件权限
chmod($destPath, 0644);
return [
'name' => $newName,
'path' => $destPath,
'size' => $file['size'],
'mime' => $mimeType,
'ext' => $ext,
];
}
private function getErrorMessage($errorCode) {
$messages = [
UPLOAD_ERR_INI_SIZE => '文件超过服务器限制',
UPLOAD_ERR_FORM_SIZE => '文件超过表单限制',
UPLOAD_ERR_PARTIAL => '文件只有部分被上传',
UPLOAD_ERR_NO_FILE => '没有文件被上传',
UPLOAD_ERR_NO_TMP_DIR => '找不到临时文件夹',
UPLOAD_ERR_CANT_WRITE => '文件写入失败',
UPLOAD_ERR_EXTENSION => '扩展停止了文件上传',
];
return $messages[$errorCode] ?? '未知错误';
}
private function getExtensionFromMime($mime) {
$map = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'application/pdf' => 'pdf',
'text/plain' => 'txt',
'application/zip' => 'zip',
];
return $map[$mime] ?? 'bin';
}
public function getError() {
return $this->error;
}
}
// 使用示例
$uploader = new FileUpload([
'allowed_types' => ['image/jpeg', 'image/png', 'image/gif'],
'max_size' => 2 * 1024 * 1024,
'upload_dir' => 'uploads/',
]);
$result = $uploader->upload('avatar');
if ($result) {
echo '上传成功:' . $result['name'];
} else {
echo '上传失败:' . $uploader->getError();
}常见问题和坑
1. 上传大文件失败
原因:php.ini的uploadmaxfilesize和postmaxsize设置太小。 解决:修改php.ini配置,或用分片上传。
2. moveuploadedfile()失败
原因:目标目录不存在或没有写入权限。 解决:确保目录存在,权限设置正确(755)。
3. $_FILES为空
原因:表单没有设置enctype="multipart/form-data",或method不是POST。 解决:检查表单设置。
4. 中文文件名乱码
原因:文件名编码问题。 解决:不要用原始文件名,生成随机文件名;或用iconv/mbconvertencoding转换编码。
5. 上传后文件找不到
原因:上传目录路径错误,或文件被自动删除(临时文件)。 解决:用绝对路径,确保moveuploadedfile()成功执行。
6. 图片上传后不能显示
原因:文件权限问题,或路径错误,或Nginx/Apache配置问题。 解决:检查文件权限(644),检查路径,检查Web服务器配置。
总结
PHP文件上传,是Web开发中常用但需要小心的功能。
核心要点:
- 基础:表单设置enctype="multipart/form-data",$FILES获取文件信息,moveuploaded_file()保存文件
- 安全加固:
- 验证文件类型(finfo/getimagesize/文件头,不要信任$_FILES['type']) - 重命名文件(不要用原始文件名) - 存储目录安全(禁止执行PHP/目录不在Web根/权限设置) - 防止路径遍历(basename/完全生成新文件名) - 验证文件大小 - 图片二次渲染(清除图片马) - 防重复上传(文件哈希)
- 多文件上传:name加[],multiple属性,按索引遍历$_FILES
- 大文件上传:修改php.ini配置,分片上传,进度条
- 图片处理:生成缩略图、裁剪、加水印(GD库)
- 常见问题:大文件失败、move失败、$_FILES为空、中文乱码、文件找不到、图片不显示
文件上传的安全,怎么强调都不为过。一个不安全的文件上传功能,可能导致服务器被入侵,数据泄露,甚至整个网站被控制。所以,实现文件上传功能时,一定要做好安全加固,不要掉以轻心。
希望这篇文章,能帮你实现一个安全、稳定的文件上传功能。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录