文件上传
学习目标
- 掌握PHP文件上传的基本原理
- 学会处理各种文件上传场景
- 理解文件上传的安全风险和防护措施
- 能够构建完整的文件上传系统
文件上传概述
文件上传是Web开发中的常见功能,允许用户将本地文件上传到服务器:
- 用户头像:个人资料图片上传
- 文档管理:上传和分享文档
- 媒体内容:图片、视频、音频上传
- 数据导入:批量数据文件处理
- 系统配置:配置文件上传
PHP文件上传配置
php.ini 配置
; 文件上传相关配置
file_uploads = On ; 启用文件上传
upload_max_filesize = 2M ; 单个文件最大大小
max_file_uploads = 20 ; 同时上传文件数量限制
post_max_size = 8M ; POST数据最大大小
memory_limit = 128M ; 内存限制
max_execution_time = 30 ; 脚本执行时间限制
upload_tmp_dir = /tmp ; 临时文件目录
基本文件上传
HTML表单
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上传</title>
<style>
.upload-form {
max-width: 600px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="file"] {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 3px;
}
button {
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 3px;
cursor: pointer;
}
.progress {
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
margin-top: 10px;
}
.progress-bar {
height: 100%;
background-color: #007bff;
width: 0%;
transition: width 0.3s ease;
}
</style>
</head>
<body>
<h1>文件上传</h1>
<!-- 单文件上传 -->
<div class="upload-form">
<h2>单文件上传</h2>
<form action="upload_single.php" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="file1">选择文件:</label>
<input type="file" id="file1" name="file" accept="image/*" required>
</div>
<div class="form-group">
<label for="description">文件描述:</label>
<input type="text" id="description" name="description" placeholder="输入文件描述">
</div>
<button type="submit">上传文件</button>
</form>
</div>
<!-- 多文件上传 -->
<div class="upload-form">
<h2>多文件上传</h2>
<form action="upload_multiple.php" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="files">选择多个文件:</label>
<input type="file" id="files" name="files[]" multiple accept="image/*,application/pdf">
</div>
<button type="submit">上传文件</button>
</form>
</div>
<!-- 拖拽上传 -->
<div class="upload-form">
<h2>拖拽上传</h2>
<div id="dropZone" style="
border: 2px dashed #ccc;
padding: 40px;
text-align: center;
background-color: #f9f9f9;
cursor: pointer;
">
<p>拖拽文件到这里或点击选择文件</p>
<input type="file" id="dropFile" name="file" multiple style="display: none;">
</div>
<div class="progress">
<div class="progress-bar" id="progressBar"></div>
</div>
</div>
<script>
// 拖拽上传功能
const dropZone = document.getElementById('dropZone');
const dropFile = document.getElementById('dropFile');
const progressBar = document.getElementById('progressBar');
dropZone.addEventListener('click', () => {
dropFile.click();
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.backgroundColor = '#e3f2fd';
});
dropZone.addEventListener('dragleave', () => {
dropZone.style.backgroundColor = '#f9f9f9';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.backgroundColor = '#f9f9f9';
const files = e.dataTransfer.files;
handleFiles(files);
});
function handleFiles(files) {
// 创建FormData对象
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i]);
}
// 创建AJAX请求
const xhr = new XMLHttpRequest();
// 进度事件
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressBar.style.width = percentComplete + '%';
}
});
// 完成事件
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
alert('文件上传成功!');
progressBar.style.width = '0%';
} else {
alert('文件上传失败!');
}
});
// 错误事件
xhr.addEventListener('error', () => {
alert('上传过程中发生错误!');
});
// 发送请求
xhr.open('POST', 'upload_ajax.php', true);
xhr.send(formData);
}
</script>
</body>
</html>
PHP处理代码
单文件上传处理
<?php
// upload_single.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
// 检查是否有文件上传
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
throw new Exception('请选择要上传的文件');
}
$file = $_FILES['file'];
$description = $_POST['description'] ?? '';
// 验证文件
$errors = validateFile($file);
if (!empty($errors)) {
throw new Exception(implode(', ', $errors));
}
// 处理上传
$result = processFileUpload($file, $description);
if ($result) {
echo '<div style="color: green; padding: 10px; border: 1px solid #4CAF50; background-color: #f0f8f0; border-radius: 5px;">';
echo '<h3>文件上传成功!</h3>';
echo '<p>文件名:' . htmlspecialchars($result['filename']) . '</p>';
echo '<p>文件大小:' . formatFileSize($result['size']) . '</p>';
echo '<p>文件类型:' . htmlspecialchars($result['type']) . '</p>';
if ($description) {
echo '<p>描述:' . htmlspecialchars($description) . '</p>';
}
echo '<p><a href="' . htmlspecialchars($result['url']) . '" target="_blank">查看文件</a></p>';
echo '</div>';
}
} catch (Exception $e) {
echo '<div style="color: red; padding: 10px; border: 1px solid #f44336; background-color: #fff0f0; border-radius: 5px;">';
echo '<h3>上传失败</h3>';
echo '<p>' . htmlspecialchars($e->getMessage()) . '</p>';
echo '</div>';
}
}
/**
* 验证上传的文件
*/
function validateFile($file) {
$errors = [];
// 检查文件大小
$maxSize = 5 * 1024 * 1024; // 5MB
if ($file['size'] > $maxSize) {
$errors[] = '文件大小不能超过 5MB';
}
// 检查文件类型
$allowedTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'application/pdf',
'text/plain',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];
if (!in_array($file['type'], $allowedTypes)) {
$errors[] = '不支持的文件类型';
}
// 检查文件扩展名
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'txt', 'doc', 'docx'];
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowedExtensions)) {
$errors[] = '不支持的文件扩展名';
}
// 使用finfo检查真实文件类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
$errors[] = '文件内容与扩展名不匹配';
}
return $errors;
}
/**
* 处理文件上传
*/
function processFileUpload($file, $description = '') {
// 创建上传目录
$uploadDir = 'uploads/' . date('Y/m/d');
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// 生成安全的文件名
$originalName = $file['name'];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$basename = pathinfo($originalName, PATHINFO_FILENAME);
// 清理文件名
$safeBasename = preg_replace('/[^a-zA-Z0-9\-_]/', '', $basename);
$filename = $safeBasename . '_' . uniqid() . '.' . $extension;
$uploadPath = $uploadDir . '/' . $filename;
// 移动文件
if (!move_uploaded_file($file['tmp_name'], $uploadPath)) {
throw new Exception('文件移动失败');
}
// 保存文件信息(这里可以保存到数据库)
$fileInfo = [
'original_name' => $originalName,
'filename' => $filename,
'path' => $uploadPath,
'url' => $uploadPath,
'size' => $file['size'],
'type' => $file['type'],
'description' => $description,
'upload_time' => date('Y-m-d H:i:s')
];
// 保存到数据库示例
// saveFileInfo($fileInfo);
return $fileInfo;
}
/**
* 格式化文件大小
*/
function formatFileSize($bytes) {
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
/**
* 获取文件MIME类型
*/
function getMimeType($filename) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $filename);
finfo_close($finfo);
return $mimeType;
}
/**
* 生成缩略图(仅适用于图片)
*/
function createThumbnail($sourcePath, $targetPath, $width = 200, $height = 200) {
// 获取图片信息
$imageInfo = getimagesize($sourcePath);
if (!$imageInfo) {
return false;
}
$sourceWidth = $imageInfo[0];
$sourceHeight = $imageInfo[1];
$mimeType = $imageInfo['mime'];
// 根据MIME类型创建图片资源
switch ($mimeType) {
case 'image/jpeg':
$sourceImage = imagecreatefromjpeg($sourcePath);
break;
case 'image/png':
$sourceImage = imagecreatefrompng($sourcePath);
break;
case 'image/gif':
$sourceImage = imagecreatefromgif($sourcePath);
break;
case 'image/webp':
$sourceImage = imagecreatefromwebp($sourcePath);
break;
default:
return false;
}
if (!$sourceImage) {
return false;
}
// 计算缩略图尺寸
$ratio = min($width / $sourceWidth, $height / $sourceHeight);
$thumbWidth = (int)($sourceWidth * $ratio);
$thumbHeight = (int)($sourceHeight * $ratio);
// 创建缩略图
$thumbImage = imagecreatetruecolor($thumbWidth, $thumbHeight);
// 处理透明度(PNG和GIF)
if ($mimeType == 'image/png' || $mimeType == 'image/gif') {
imagealphablending($thumbImage, false);
imagesavealpha($thumbImage, true);
$transparent = imagecolorallocatealpha($thumbImage, 255, 255, 255, 127);
imagefilledrectangle($thumbImage, 0, 0, $thumbWidth, $thumbHeight, $transparent);
}
// 复制并调整大小
imagecopyresampled($thumbImage, $sourceImage, 0, 0, 0, 0, $thumbWidth, $thumbHeight, $sourceWidth, $sourceHeight);
// 保存缩略图
$result = false;
switch ($mimeType) {
case 'image/jpeg':
$result = imagejpeg($thumbImage, $targetPath, 90);
break;
case 'image/png':
$result = imagepng($thumbImage, $targetPath, 9);
break;
case 'image/gif':
$result = imagegif($thumbImage, $targetPath);
break;
case 'image/webp':
$result = imagewebp($thumbImage, $targetPath, 90);
break;
}
// 释放内存
imagedestroy($sourceImage);
imagedestroy($thumbImage);
return $result;
}
?>
多文件上传处理
<?php
// upload_multiple.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$uploadedFiles = [];
$errors = [];
if (!isset($_FILES['files'])) {
echo '没有文件被上传';
exit;
}
$files = $_FILES['files'];
// 处理多个文件
$fileCount = count($files['name']);
for ($i = 0; $i < $fileCount; $i++) {
$file = [
'name' => $files['name'][$i],
'type' => $files['type'][$i],
'tmp_name' => $files['tmp_name'][$i],
'error' => $files['error'][$i],
'size' => $files['size'][$i]
];
// 跳过空文件
if ($file['error'] == UPLOAD_ERR_NO_FILE) {
continue;
}
try {
// 验证文件
$fileErrors = validateFile($file);
if (!empty($fileErrors)) {
$errors[$file['name']] = $fileErrors;
continue;
}
// 处理上传
$result = processFileUpload($file);
$uploadedFiles[] = $result;
} catch (Exception $e) {
$errors[$file['name']] = [$e->getMessage()];
}
}
// 显示结果
if (!empty($uploadedFiles)) {
echo '<div style="color: green; padding: 10px; border: 1px solid #4CAF50; background-color: #f0f8f0; border-radius: 5px; margin-bottom: 20px;">';
echo '<h3>成功上传 ' . count($uploadedFiles) . ' 个文件</h3>';
foreach ($uploadedFiles as $file) {
echo '<p>' . htmlspecialchars($file['filename']) . ' (' . formatFileSize($file['size']) . ')</p>';
}
echo '</div>';
}
if (!empty($errors)) {
echo '<div style="color: red; padding: 10px; border: 1px solid #f44336; background-color: #fff0f0; border-radius: 5px;">';
echo '<h3>上传失败的文件</h3>';
foreach ($errors as $filename => $fileErrors) {
echo '<p><strong>' . htmlspecialchars($filename) . ':</strong> ' . implode(', ', $fileErrors) . '</p>';
}
echo '</div>';
}
}
?>
AJAX上传处理
<?php
// upload_ajax.php
header('Content-Type: application/json');
try {
if (!isset($_FILES['files'])) {
throw new Exception('没有文件被上传');
}
$files = $_FILES['files'];
$uploadedFiles = [];
// 处理单个或多个文件
if (is_array($files['name'])) {
// 多文件上传
$fileCount = count($files['name']);
for ($i = 0; $i < $fileCount; $i++) {
$file = [
'name' => $files['name'][$i],
'type' => $files['type'][$i],
'tmp_name' => $files['tmp_name'][$i],
'error' => $files['error'][$i],
'size' => $files['size'][$i]
];
if ($file['error'] == UPLOAD_ERR_OK) {
$result = processFileUpload($file);
$uploadedFiles[] = $result;
}
}
} else {
// 单文件上传
if ($files['error'] == UPLOAD_ERR_OK) {
$result = processFileUpload($files);
$uploadedFiles[] = $result;
}
}
echo json_encode([
'success' => true,
'message' => '文件上传成功',
'files' => $uploadedFiles
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
?>
安全性考虑
1. 文件类型验证
<?php
function validateFileType($file, $allowedTypes) {
// 检查MIME类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
return false;
}
// 检查扩展名
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowedExtensions = array_map(function($type) {
return explode('/', $type)[1];
}, $allowedTypes);
if (!in_array($extension, $allowedExtensions)) {
return false;
}
return true;
}
// 使用示例
$allowedImageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!validateFileType($_FILES['avatar'], $allowedImageTypes)) {
die('只允许上传图片文件');
}
?>
2. 文件大小限制
<?php
function validateFileSize($file, $maxSize) {
return $file['size'] <= $maxSize;
}
// 根据文件类型设置不同的大小限制
function getMaxFileSizeByType($mimeType) {
$limits = [
'image/jpeg' => 5 * 1024 * 1024, // 5MB
'image/png' => 5 * 1024 * 1024, // 5MB
'image/gif' => 2 * 1024 * 1024, // 2MB
'application/pdf' => 10 * 1024 * 1024, // 10MB
'text/plain' => 1 * 1024 * 1024, // 1MB
];
return $limits[$mimeType] ?? 2 * 1024 * 1024; // 默认2MB
}
?>
3. 文件名安全处理
<?php
function generateSafeFilename($originalName) {
// 获取文件信息
$pathInfo = pathinfo($originalName);
$extension = strtolower($pathInfo['extension']);
$basename = $pathInfo['filename'];
// 清理文件名:只保留字母、数字、连字符和下划线
$safeBasename = preg_replace('/[^a-zA-Z0-9\-_]/', '', $basename);
// 如果清理后为空,使用默认名称
if (empty($safeBasename)) {
$safeBasename = 'file';
}
// 限制长度
$safeBasename = substr($safeBasename, 0, 50);
// 添加唯一ID防止文件名冲突
$uniqueId = uniqid();
$timestamp = date('YmdHis');
return sprintf('%s_%s_%s.%s', $safeBasename, $timestamp, $uniqueId, $extension);
}
function isExecutableFile($filename) {
$executableExtensions = ['php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'phps', 'sh', 'bat', 'exe', 'com', 'cmd', 'scr'];
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
return in_array($extension, $executableExtensions);
}
?>
4. 目录权限设置
<?php
function secureUploadDirectory($uploadDir) {
// 创建目录(如果不存在)
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// 设置目录权限
chmod($uploadDir, 0755);
// 创建.htaccess文件防止直接访问(仅适用于Apache)
$htaccessFile = $uploadDir . '/.htaccess';
if (!file_exists($htaccessFile)) {
$htaccessContent = "Order deny,allow\nDeny from all\n";
if (in_array(pathinfo($uploadDir, PATHINFO_BASENAME), ['images', 'avatars', 'photos'])) {
$htaccessContent = "Order allow,deny\nAllow from all\n";
}
file_put_contents($htaccessFile, $htaccessContent);
}
// 创建index.html防止目录列表
$indexFile = $uploadDir . '/index.html';
if (!file_exists($indexFile)) {
file_put_contents($indexFile, '<!DOCTYPE html><html><head><title>Access Denied</title></head><body><h1>Access Denied</h1></body></html>');
}
}
// 使用示例
secureUploadDirectory('uploads/images');
?>
完整的文件上传类
<?php
class FileUploader {
private $uploadDir;
private $allowedTypes;
private $maxFileSize;
private $errors;
public function __construct($uploadDir = 'uploads', $allowedTypes = [], $maxFileSize = 5242880) {
$this->uploadDir = rtrim($uploadDir, '/');
$this->allowedTypes = $allowedTypes;
$this->maxFileSize = $maxFileSize;
$this->errors = [];
$this->initUploadDirectory();
}
private function initUploadDirectory() {
if (!file_exists($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
public function upload($file, $description = '') {
$this->errors = [];
if (!$this->validateFile($file)) {
return false;
}
return $this->processUpload($file, $description);
}
public function uploadMultiple($files) {
$this->errors = [];
$uploadedFiles = [];
foreach ($files['name'] as $key => $name) {
$file = [
'name' => $name,
'type' => $files['type'][$key],
'tmp_name' => $files['tmp_name'][$key],
'error' => $files['error'][$key],
'size' => $files['size'][$key]
];
if ($file['error'] === UPLOAD_ERR_OK) {
$result = $this->upload($file);
if ($result) {
$uploadedFiles[] = $result;
}
}
}
return $uploadedFiles;
}
private function validateFile($file) {
if ($file['error'] !== UPLOAD_ERR_OK) {
$this->errors[] = $this->getUploadErrorMessage($file['error']);
return false;
}
if (!empty($this->allowedTypes) && !in_array($file['type'], $this->allowedTypes)) {
$this->errors[] = '不支持的文件类型';
return false;
}
if ($file['size'] > $this->maxFileSize) {
$this->errors[] = '文件大小超过限制';
return false;
}
// 额外的安全检查
$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->errors[] = '文件类型验证失败';
return false;
}
return true;
}
private function processUpload($file, $description = '') {
$filename = $this->generateSafeFilename($file['name']);
$uploadPath = $this->uploadDir . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $uploadPath)) {
$this->errors[] = '文件移动失败';
return false;
}
return [
'filename' => $filename,
'original_name' => $file['name'],
'path' => $uploadPath,
'size' => $file['size'],
'type' => $file['type'],
'description' => $description,
'upload_time' => date('Y-m-d H:i:s')
];
}
private function generateSafeFilename($originalName) {
$pathInfo = pathinfo($originalName);
$extension = strtolower($pathInfo['extension']);
$basename = preg_replace('/[^a-zA-Z0-9\-_]/', '', $pathInfo['filename']);
if (empty($basename)) {
$basename = 'file';
}
return $basename . '_' . uniqid() . '.' . $extension;
}
private function getUploadErrorMessage($errorCode) {
switch ($errorCode) {
case UPLOAD_ERR_INI_SIZE:
return '文件大小超过服务器限制';
case UPLOAD_ERR_FORM_SIZE:
return '文件大小超过表单限制';
case UPLOAD_ERR_PARTIAL:
return '文件只有部分被上传';
case UPLOAD_ERR_NO_FILE:
return '没有文件被上传';
case UPLOAD_ERR_NO_TMP_DIR:
return '缺少临时文件夹';
case UPLOAD_ERR_CANT_WRITE:
return '文件写入失败';
default:
return '未知上传错误';
}
}
public function getErrors() {
return $this->errors;
}
public function hasErrors() {
return !empty($this->errors);
}
}
// 使用示例
$uploader = new FileUploader('uploads', ['image/jpeg', 'image/png', 'image/gif'], 5 * 1024 * 1024);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = $uploader->upload($_FILES['file']);
if ($result) {
echo '上传成功:' . $result['filename'];
} else {
echo '上传失败:' . implode(', ', $uploader->getErrors());
}
}
?>
总结
文件上传是Web开发中的重要功能,但同时也存在安全风险。通过合理的配置、严格的验证和安全的处理流程,可以构建安全可靠的文件上传系统。记住:始终不要信任用户上传的文件,要进行多重验证和检查。