文件上传是Web开发中常见的功能,允许用户将文件从本地计算机传输到服务器。PHP提供了强大的文件上传支持,但同时也需要开发者注意安全性和性能问题。
$_FILES超全局数组处理上传的文件,使用move_uploaded_file()函数将临时文件移动到指定位置。
当文件上传时,PHP会自动创建一个$_FILES数组,其中包含上传文件的所有信息:
| 数组元素 | 描述 | 示例值 |
|---|---|---|
$_FILES['file']['name'] |
客户端文件的原始名称 | photo.jpg |
$_FILES['file']['type'] |
文件的MIME类型(由浏览器提供,可能不可靠) | image/jpeg |
$_FILES['file']['size'] |
文件大小(字节) | 204800(200KB) |
$_FILES['file']['tmp_name'] |
服务器上的临时文件路径 | /tmp/php/php4hst32 |
$_FILES['file']['error'] |
错误代码(0表示成功) | 0(无错误) |
$_FILES['file']['full_path'] |
文件的完整路径(PHP 8.1+) | /home/user/photo.jpg |
| 错误代码 | 常量 | 描述 |
|---|---|---|
0 |
UPLOAD_ERR_OK |
文件上传成功 |
1 |
UPLOAD_ERR_INI_SIZE |
文件大小超过了php.ini中的upload_max_filesize设置 |
2 |
UPLOAD_ERR_FORM_SIZE |
文件大小超过了HTML表单中MAX_FILE_SIZE指定的值 |
3 |
UPLOAD_ERR_PARTIAL |
文件只有部分被上传 |
4 |
UPLOAD_ERR_NO_FILE |
没有文件被上传 |
6 |
UPLOAD_ERR_NO_TMP_DIR |
找不到临时文件夹 |
7 |
UPLOAD_ERR_CANT_WRITE |
文件写入失败 |
8 |
UPLOAD_ERR_EXTENSION |
PHP扩展阻止了文件上传 |
<!DOCTYPE html>
<html>
<head>
<title>文件上传示例</title>
<style>
.upload-form {
max-width: 500px;
margin: 50px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="file"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 3px;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 3px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="upload-form">
<h2>上传文件</h2>
<form action="upload.php" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="file">选择文件:</label>
<input type="file" name="file" id="file" required>
</div>
<div class="form-group">
<label for="description">文件描述:</label>
<input type="text" name="description" id="description" placeholder="可选">
</div>
<button type="submit" name="submit">上传文件</button>
<!-- 隐藏字段限制文件大小(单位为字节) -->
<input type="hidden" name="MAX_FILE_SIZE" value="10485760"> <!-- 10MB -->
</form>
<div class="upload-info">
<p><strong>支持的文件类型:</strong> JPG, PNG, GIF, PDF, DOC, DOCX</p>
<p><strong>最大文件大小:</strong> 10MB</p>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>多文件上传</title>
<style>
.upload-form {
max-width: 600px;
margin: 50px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
.file-input-wrapper {
margin-bottom: 15px;
}
.add-file-btn {
background-color: #2196F3;
color: white;
padding: 8px 16px;
border: none;
border-radius: 3px;
cursor: pointer;
margin-bottom: 15px;
}
.remove-file-btn {
background-color: #f44336;
color: white;
padding: 5px 10px;
border: none;
border-radius: 3px;
cursor: pointer;
margin-left: 10px;
}
</style>
<script>
let fileCount = 1;
function addFileInput() {
fileCount++;
const container = document.getElementById('fileInputs');
const div = document.createElement('div');
div.className = 'file-input-wrapper';
div.innerHTML = `
<input type="file" name="files[]" required>
<button type="button" class="remove-file-btn" onclick="removeFileInput(this)">删除</button>
`;
container.appendChild(div);
}
function removeFileInput(button) {
if (fileCount > 1) {
button.parentElement.remove();
fileCount--;
}
}
</script>
</head>
<body>
<div class="upload-form">
<h2>多文件上传</h2>
<form action="multi_upload.php" method="POST" enctype="multipart/form-data">
<div id="fileInputs">
<div class="file-input-wrapper">
<input type="file" name="files[]" required>
</div>
</div>
<button type="button" class="add-file-btn" onclick="addFileInput()">添加更多文件</button>
<br><br>
<button type="submit" name="submit">上传所有文件</button>
<!-- 隐藏字段限制文件大小 -->
<input type="hidden" name="MAX_FILE_SIZE" value="5242880"> <!-- 5MB -->
</form>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>AJAX文件上传</title>
<style>
.upload-area {
width: 400px;
height: 200px;
border: 2px dashed #ccc;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
margin: 20px auto;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
border-color: #4CAF50;
background-color: #f9f9f9;
}
.upload-area.dragover {
border-color: #2196F3;
background-color: #e3f2fd;
}
.progress-container {
width: 400px;
margin: 20px auto;
display: none;
}
.progress-bar {
width: 0%;
height: 20px;
background-color: #4CAF50;
border-radius: 5px;
transition: width 0.3s;
}
#preview {
max-width: 200px;
max-height: 200px;
display: none;
margin: 20px auto;
}
</style>
</head>
<body>
<h2 style="text-align: center;">AJAX文件上传(支持拖放)</h2>
<div class="upload-area" id="uploadArea">
<div>
<p><strong>点击选择文件或拖放到此处</strong></p>
<p>支持 JPG, PNG, GIF 图片</p>
<p>最大大小: 2MB</p>
</div>
</div>
<input type="file" id="fileInput" accept="image/*" style="display: none;">
<div class="progress-container" id="progressContainer">
<p>上传进度: <span id="progressPercent">0%</span></p>
<div class="progress-bar" id="progressBar"></div>
</div>
<img id="preview" src="" alt="预览">
<div id="message" style="text-align: center; margin-top: 20px;"></div>
<script>
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const preview = document.getElementById('preview');
const progressContainer = document.getElementById('progressContainer');
const progressBar = document.getElementById('progressBar');
const progressPercent = document.getElementById('progressPercent');
const messageDiv = document.getElementById('message');
// 点击上传区域触发文件选择
uploadArea.addEventListener('click', () => {
fileInput.click();
});
// 文件选择变化
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileUpload(e.target.files[0]);
}
});
// 拖放功能
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
handleFileUpload(e.dataTransfer.files[0]);
}
});
// 处理文件上传
function handleFileUpload(file) {
// 验证文件类型
const validTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!validTypes.includes(file.type)) {
showMessage('错误:只支持 JPG, PNG, GIF 格式', 'error');
return;
}
// 验证文件大小(2MB)
if (file.size > 2 * 1024 * 1024) {
showMessage('错误:文件大小不能超过2MB', 'error');
return;
}
// 显示预览
const reader = new FileReader();
reader.onload = function(e) {
preview.src = e.target.result;
preview.style.display = 'block';
}
reader.readAsDataURL(file);
// 上传文件
uploadFile(file);
}
// 上传文件到服务器
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('ajax_upload', 'true');
const xhr = new XMLHttpRequest();
// 显示进度条
progressContainer.style.display = 'block';
progressBar.style.width = '0%';
progressPercent.textContent = '0%';
// 上传进度事件
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressBar.style.width = percentComplete + '%';
progressPercent.textContent = Math.round(percentComplete) + '%';
}
});
// 上传完成事件
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
showMessage('文件上传成功!', 'success');
} else {
showMessage('上传失败:' + response.message, 'error');
}
} else {
showMessage('上传失败,服务器错误', 'error');
}
// 隐藏进度条
setTimeout(() => {
progressContainer.style.display = 'none';
}, 2000);
});
// 上传错误事件
xhr.addEventListener('error', () => {
showMessage('上传失败,网络错误', 'error');
progressContainer.style.display = 'none';
});
// 发送请求
xhr.open('POST', 'upload.php');
xhr.send(formData);
}
// 显示消息
function showMessage(text, type) {
messageDiv.textContent = text;
messageDiv.style.color = type === 'error' ? '#f44336' : '#4CAF50';
messageDiv.style.fontWeight = 'bold';
}
</script>
</body>
</html>
<?php
// upload.php - 基础文件上传处理
// 检查是否有文件上传
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$uploadDir = 'uploads/';
// 确保上传目录存在
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$file = $_FILES['file'];
$fileName = basename($file['name']);
$fileTmpName = $file['tmp_name'];
$fileSize = $file['size'];
$fileError = $file['error'];
// 检查上传是否成功
if ($fileError === UPLOAD_ERR_OK) {
// 生成安全的文件名(防止文件名攻击)
$safeFileName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $fileName);
$safeFileName = time() . '_' . $safeFileName;
$destination = $uploadDir . $safeFileName;
// 移动临时文件到目标位置
if (move_uploaded_file($fileTmpName, $destination)) {
echo "<div style='padding: 20px; background-color: #d4edda; border: 1px solid #c3e6cb; border-radius: 5px;'>";
echo "<h3>文件上传成功!</h3>";
echo "<p><strong>文件名:</strong> " . htmlspecialchars($fileName) . "</p>";
echo "<p><strong>保存为:</strong> " . htmlspecialchars($safeFileName) . "</p>";
echo "<p><strong>文件大小:</strong> " . formatBytes($fileSize) . "</p>";
echo "<p><strong>文件路径:</strong> " . htmlspecialchars($destination) . "</p>";
// 如果是图片,显示缩略图
if (strpos($file['type'], 'image/') === 0) {
echo "<p><strong>图片预览:</strong></p>";
echo "<img src='" . htmlspecialchars($destination) . "' alt='预览' style='max-width: 200px; max-height: 200px;'>";
}
echo "</div>";
} else {
echo "<div style='padding: 20px; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 5px;'>";
echo "<h3>文件上传失败!</h3>";
echo "<p>无法将文件移动到目标位置。请检查目录权限。</p>";
echo "</div>";
}
} else {
echo "<div style='padding: 20px; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 5px;'>";
echo "<h3>文件上传失败!</h3>";
echo "<p><strong>错误代码:</strong> " . $fileError . "</p>";
echo "<p><strong>错误描述:</strong> " . getUploadError($fileError) . "</p>";
echo "</div>";
}
} else {
echo "<div style='padding: 20px; background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 5px;'>";
echo "<h3>没有文件被上传</h3>";
echo "<p>请通过表单上传文件。</p>";
echo "</div>";
}
/**
* 格式化字节大小
*/
function formatBytes($bytes, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$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, $precision) . ' ' . $units[$pow];
}
/**
* 获取上传错误描述
*/
function getUploadError($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 '文件写入失败。';
case UPLOAD_ERR_EXTENSION:
return 'PHP扩展阻止了文件上传。';
default:
return '未知错误。';
}
}
?>
<?php
/**
* 安全的文件上传处理类
*/
class SecureFileUploader {
private $allowedExtensions = [];
private $allowedMimeTypes = [];
private $maxFileSize = 0;
private $uploadDir = '';
private $errors = [];
public function __construct($uploadDir = 'uploads/') {
$this->uploadDir = rtrim($uploadDir, '/') . '/';
// 创建上传目录(如果不存在)
if (!file_exists($this->uploadDir)) {
if (!mkdir($this->uploadDir, 0755, true)) {
$this->addError('无法创建上传目录');
}
}
// 检查目录是否可写
if (!is_writable($this->uploadDir)) {
$this->addError('上传目录不可写');
}
}
/**
* 设置允许的文件扩展名
*/
public function setAllowedExtensions($extensions) {
$this->allowedExtensions = array_map('strtolower', $extensions);
return $this;
}
/**
* 设置允许的MIME类型
*/
public function setAllowedMimeTypes($mimeTypes) {
$this->allowedMimeTypes = $mimeTypes;
return $this;
}
/**
* 设置最大文件大小(字节)
*/
public function setMaxFileSize($sizeInBytes) {
$this->maxFileSize = $sizeInBytes;
return $this;
}
/**
* 处理单个文件上传
*/
public function upload($fileInputName, $customName = null) {
$this->errors = [];
// 检查是否有文件上传
if (!isset($_FILES[$fileInputName]) || $_FILES[$fileInputName]['error'] === UPLOAD_ERR_NO_FILE) {
$this->addError('没有文件被上传');
return false;
}
$file = $_FILES[$fileInputName];
// 检查上传错误
if ($file['error'] !== UPLOAD_ERR_OK) {
$this->addError($this->getUploadErrorMessage($file['error']));
return false;
}
// 验证文件大小
if ($this->maxFileSize > 0 && $file['size'] > $this->maxFileSize) {
$this->addError('文件大小超过限制');
return false;
}
// 获取文件信息
$originalName = $file['name'];
$tmpName = $file['tmp_name'];
$fileSize = $file['size'];
// 获取文件扩展名
$fileExtension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
// 验证文件扩展名
if (!empty($this->allowedExtensions) && !in_array($fileExtension, $this->allowedExtensions)) {
$this->addError('不支持的文件类型');
return false;
}
// 验证MIME类型(使用finfo进行更可靠的检测)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$detectedMimeType = finfo_file($finfo, $tmpName);
finfo_close($finfo);
if (!empty($this->allowedMimeTypes) && !in_array($detectedMimeType, $this->allowedMimeTypes)) {
$this->addError('不支持的文件格式');
return false;
}
// 生成安全的文件名
if ($customName) {
$safeFileName = $this->sanitizeFileName($customName . '.' . $fileExtension);
} else {
$safeFileName = $this->generateSafeFileName($originalName);
}
// 检查文件名是否已存在(避免覆盖)
$destination = $this->uploadDir . $safeFileName;
$counter = 1;
while (file_exists($destination)) {
$safeFileName = $this->generateSafeFileName($originalName, $counter);
$destination = $this->uploadDir . $safeFileName;
$counter++;
}
// 移动文件
if (move_uploaded_file($tmpName, $destination)) {
return [
'success' => true,
'original_name' => $originalName,
'saved_name' => $safeFileName,
'file_path' => $destination,
'file_size' => $fileSize,
'file_extension' => $fileExtension,
'mime_type' => $detectedMimeType
];
} else {
$this->addError('文件移动失败');
return false;
}
}
/**
* 处理多文件上传
*/
public function uploadMultiple($fileInputName) {
$results = [];
// 检查是否是多文件上传
if (!isset($_FILES[$fileInputName]['name']) || !is_array($_FILES[$fileInputName]['name'])) {
$this->addError('不是多文件上传');
return false;
}
$files = $_FILES[$fileInputName];
$fileCount = count($files['name']);
for ($i = 0; $i < $fileCount; $i++) {
// 如果没有选择文件,跳过
if ($files['error'][$i] === UPLOAD_ERR_NO_FILE) {
continue;
}
// 创建临时文件数组
$file = [
'name' => $files['name'][$i],
'type' => $files['type'][$i],
'tmp_name' => $files['tmp_name'][$i],
'error' => $files['error'][$i],
'size' => $files['size'][$i]
];
// 保存到临时$_FILES数组以便重用upload方法
$_FILES['temp_file'] = $file;
// 使用upload方法处理单个文件
$result = $this->upload('temp_file');
if ($result) {
$results[] = $result;
}
// 清理临时文件数组
unset($_FILES['temp_file']);
}
return $results;
}
/**
* 生成安全的文件名
*/
private function generateSafeFileName($originalName, $counter = null) {
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$nameWithoutExt = pathinfo($originalName, PATHINFO_FILENAME);
// 清理文件名
$safeName = $this->sanitizeFileName($nameWithoutExt);
// 添加时间戳和随机字符串
$timestamp = time();
$random = bin2hex(random_bytes(4));
if ($counter) {
$fileName = "{$timestamp}_{$random}_{$counter}.{$extension}";
} else {
$fileName = "{$timestamp}_{$random}.{$extension}";
}
return $fileName;
}
/**
* 清理文件名(移除特殊字符)
*/
private function sanitizeFileName($fileName) {
// 移除路径信息
$fileName = basename($fileName);
// 替换空格为下划线
$fileName = str_replace(' ', '_', $fileName);
// 只保留字母、数字、下划线、连字符和点
$fileName = preg_replace('/[^a-zA-Z0-9._-]/', '', $fileName);
// 移除多个连续的点
$fileName = preg_replace('/\.{2,}/', '.', $fileName);
return $fileName;
}
/**
* 获取上传错误信息
*/
private function getUploadErrorMessage($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 => 'PHP扩展阻止了文件上传'
];
return $messages[$errorCode] ?? '未知上传错误';
}
/**
* 添加错误信息
*/
private function addError($message) {
$this->errors[] = $message;
}
/**
* 获取所有错误信息
*/
public function getErrors() {
return $this->errors;
}
/**
* 获取最后一个错误信息
*/
public function getLastError() {
return end($this->errors) ?: '';
}
/**
* 是否有错误
*/
public function hasErrors() {
return !empty($this->errors);
}
}
// 使用示例
$uploader = new SecureFileUploader('uploads/');
// 配置上传设置
$uploader
->setAllowedExtensions(['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'])
->setAllowedMimeTypes([
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
])
->setMaxFileSize(5 * 1024 * 1024); // 5MB
// 处理单个文件上传
if (isset($_FILES['single_file'])) {
$result = $uploader->upload('single_file');
if ($result) {
echo "<div style='color: green;'>文件上传成功!</div>";
echo "<pre>" . print_r($result, true) . "</pre>";
} else {
echo "<div style='color: red;'>文件上传失败!</div>";
foreach ($uploader->getErrors() as $error) {
echo "<p>错误:$error</p>";
}
}
}
// 处理多文件上传
if (isset($_FILES['multiple_files'])) {
$results = $uploader->uploadMultiple('multiple_files');
if ($results) {
echo "<div style='color: green;'>上传了 " . count($results) . " 个文件</div>";
foreach ($results as $result) {
echo "<p>{$result['original_name']} -> {$result['saved_name']}</p>";
}
} else {
echo "<div style='color: red;'>多文件上传失败!</div>";
foreach ($uploader->getErrors() as $error) {
echo "<p>错误:$error</p>";
}
}
}
?>
<?php
/**
* 图片上传处理类
*/
class ImageUploader {
private $maxWidth = 1920;
private $maxHeight = 1080;
private $quality = 85;
private $createThumbnail = true;
private $thumbnailWidth = 200;
private $thumbnailHeight = 200;
public function uploadImage($fileInputName) {
// 验证是否是图片文件
if (!isset($_FILES[$fileInputName])) {
return ['success' => false, 'message' => '没有文件上传'];
}
$file = $_FILES[$fileInputName];
// 检查上传错误
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'message' => '上传错误:' . $file['error']];
}
// 验证文件类型
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$detectedType = mime_content_type($file['tmp_name']);
if (!in_array($detectedType, $allowedTypes)) {
return ['success' => false, 'message' => '不支持的文件类型'];
}
// 验证文件扩展名
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array($extension, $allowedExtensions)) {
return ['success' => false, 'message' => '不支持的文件格式'];
}
// 获取图片信息
$imageInfo = @getimagesize($file['tmp_name']);
if (!$imageInfo) {
return ['success' => false, 'message' => '不是有效的图片文件'];
}
list($width, $height, $type) = $imageInfo;
// 检查图片尺寸
if ($width > $this->maxWidth || $height > $this->maxHeight) {
return ['success' => false, 'message' => "图片尺寸过大(最大 {$this->maxWidth}x{$this->maxHeight})"];
}
// 创建上传目录
$uploadDir = 'uploads/images/';
$thumbDir = $uploadDir . 'thumbs/';
if (!file_exists($uploadDir)) mkdir($uploadDir, 0755, true);
if ($this->createThumbnail && !file_exists($thumbDir)) mkdir($thumbDir, 0755, true);
// 生成安全的文件名
$timestamp = time();
$random = bin2hex(random_bytes(8));
$safeFileName = "{$timestamp}_{$random}.{$extension}";
$destination = $uploadDir . $safeFileName;
// 移动文件
if (!move_uploaded_file($file['tmp_name'], $destination)) {
return ['success' => false, 'message' => '文件移动失败'];
}
$result = [
'success' => true,
'original_name' => $file['name'],
'saved_name' => $safeFileName,
'file_path' => $destination,
'width' => $width,
'height' => $height,
'mime_type' => $detectedType,
'thumbnail' => null
];
// 创建缩略图
if ($this->createThumbnail) {
$thumbnailPath = $this->createThumbnail($destination, $thumbDir . $safeFileName);
if ($thumbnailPath) {
$result['thumbnail'] = $thumbnailPath;
}
}
return $result;
}
/**
* 创建缩略图
*/
private function createThumbnail($sourcePath, $destinationPath) {
$imageInfo = getimagesize($sourcePath);
if (!$imageInfo) return false;
list($width, $height, $type) = $imageInfo;
// 计算缩略图尺寸(保持纵横比)
$srcRatio = $width / $height;
$dstRatio = $this->thumbnailWidth / $this->thumbnailHeight;
if ($srcRatio > $dstRatio) {
// 源图片更宽
$newWidth = $this->thumbnailWidth;
$newHeight = $this->thumbnailWidth / $srcRatio;
} else {
// 源图片更高或相等
$newHeight = $this->thumbnailHeight;
$newWidth = $this->thumbnailHeight * $srcRatio;
}
// 创建源图片资源
switch ($type) {
case IMAGETYPE_JPEG:
$source = imagecreatefromjpeg($sourcePath);
break;
case IMAGETYPE_PNG:
$source = imagecreatefrompng($sourcePath);
break;
case IMAGETYPE_GIF:
$source = imagecreatefromgif($sourcePath);
break;
case IMAGETYPE_WEBP:
$source = imagecreatefromwebp($sourcePath);
break;
default:
return false;
}
if (!$source) return false;
// 创建缩略图画布
$thumbnail = imagecreatetruecolor($this->thumbnailWidth, $this->thumbnailHeight);
// 设置透明背景(针对PNG和GIF)
if ($type === IMAGETYPE_PNG || $type === IMAGETYPE_GIF) {
imagealphablending($thumbnail, false);
imagesavealpha($thumbnail, true);
$transparent = imagecolorallocatealpha($thumbnail, 0, 0, 0, 127);
imagefill($thumbnail, 0, 0, $transparent);
}
// 计算居中位置
$dstX = ($this->thumbnailWidth - $newWidth) / 2;
$dstY = ($this->thumbnailHeight - $newHeight) / 2;
// 调整大小并居中
imagecopyresampled(
$thumbnail, $source,
$dstX, $dstY, 0, 0,
$newWidth, $newHeight, $width, $height
);
// 保存缩略图
switch ($type) {
case IMAGETYPE_JPEG:
imagejpeg($thumbnail, $destinationPath, $this->quality);
break;
case IMAGETYPE_PNG:
imagepng($thumbnail, $destinationPath);
break;
case IMAGETYPE_GIF:
imagegif($thumbnail, $destinationPath);
break;
case IMAGETYPE_WEBP:
imagewebp($thumbnail, $destinationPath, $this->quality);
break;
}
// 释放内存
imagedestroy($source);
imagedestroy($thumbnail);
return $destinationPath;
}
// 设置方法
public function setMaxDimensions($width, $height) {
$this->maxWidth = $width;
$this->maxHeight = $height;
return $this;
}
public function setQuality($quality) {
$this->quality = max(0, min(100, $quality));
return $this;
}
public function setCreateThumbnail($create) {
$this->createThumbnail = $create;
return $this;
}
public function setThumbnailDimensions($width, $height) {
$this->thumbnailWidth = $width;
$this->thumbnailHeight = $height;
return $this;
}
}
// 使用示例
$imageUploader = new ImageUploader();
$imageUploader
->setMaxDimensions(4000, 4000)
->setQuality(90)
->setThumbnailDimensions(300, 300);
if (isset($_FILES['image'])) {
$result = $imageUploader->uploadImage('image');
if ($result['success']) {
echo "<div style='border: 2px solid green; padding: 20px; margin: 20px;'>";
echo "<h3>图片上传成功!</h3>";
echo "<p><strong>原始名称:</strong> {$result['original_name']}</p>";
echo "<p><strong>保存名称:</strong> {$result['saved_name']}</p>";
echo "<p><strong>图片尺寸:</strong> {$result['width']} x {$result['height']}</p>";
// 显示原图
echo "<p><strong>原图预览:</strong></p>";
echo "<img src='{$result['file_path']}' style='max-width: 300px; max-height: 300px;'>";
// 显示缩略图
if ($result['thumbnail']) {
echo "<p><strong>缩略图:</strong></p>";
echo "<img src='{$result['thumbnail']}'>";
}
echo "</div>";
} else {
echo "<div style='border: 2px solid red; padding: 20px; margin: 20px;'>";
echo "<h3>图片上传失败!</h3>";
echo "<p>错误信息:{$result['message']}</p>";
echo "</div>";
}
}
?>
<?php
/**
* PDF文件上传处理类
*/
class PdfUploader {
private $maxPages = 50;
private $maxSize = 10 * 1024 * 1024; // 10MB
private $scanForViruses = false; // 在实际应用中应该启用
public function uploadPdf($fileInputName) {
// 验证PDF文件
if (!isset($_FILES[$fileInputName])) {
return ['success' => false, 'message' => '没有文件上传'];
}
$file = $_FILES[$fileInputName];
// 检查上传错误
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'message' => '上传错误:' . $file['error']];
}
// 验证文件大小
if ($file['size'] > $this->maxSize) {
return ['success' => false, 'message' => '文件大小超过限制'];
}
// 验证文件类型
$allowedTypes = ['application/pdf', 'application/x-pdf'];
$detectedType = mime_content_type($file['tmp_name']);
if (!in_array($detectedType, $allowedTypes)) {
return ['success' => false, 'message' => '只支持PDF文件'];
}
// 验证文件扩展名
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if ($extension !== 'pdf') {
return ['success' => false, 'message' => '文件扩展名必须是.pdf'];
}
// 检查PDF文件结构(基本验证)
if (!$this->isValidPdf($file['tmp_name'])) {
return ['success' => false, 'message' => '不是有效的PDF文件'];
}
// 检查PDF页数
$pageCount = $this->getPdfPageCount($file['tmp_name']);
if ($pageCount > $this->maxPages) {
return ['success' => false, 'message' => "PDF页数超过限制(最多{$this->maxPages}页)"];
}
// 创建上传目录
$uploadDir = 'uploads/documents/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// 生成安全的文件名
$timestamp = time();
$random = bin2hex(random_bytes(8));
$safeFileName = "{$timestamp}_{$random}.pdf";
$destination = $uploadDir . $safeFileName;
// 移动文件
if (!move_uploaded_file($file['tmp_name'], $destination)) {
return ['success' => false, 'message' => '文件移动失败'];
}
return [
'success' => true,
'original_name' => $file['name'],
'saved_name' => $safeFileName,
'file_path' => $destination,
'file_size' => $file['size'],
'page_count' => $pageCount,
'upload_time' => date('Y-m-d H:i:s')
];
}
/**
* 验证PDF文件
*/
private function isValidPdf($filePath) {
// 检查文件是否以PDF标记开头
$handle = fopen($filePath, 'r');
$header = fread($handle, 5);
fclose($handle);
return $header === '%PDF-';
}
/**
* 获取PDF页数
*/
private function getPdfPageCount($filePath) {
// 简单的方法:使用正则表达式匹配页面标记
$content = file_get_contents($filePath);
$pattern = '/\/Type\s*\/Page[^s]/';
preg_match_all($pattern, $content, $matches);
return count($matches[0]);
}
/**
* 提取PDF文本内容(需要安装pdftotext工具)
*/
public function extractPdfText($filePath) {
if (!function_exists('shell_exec')) {
return false;
}
// 检查是否安装了pdftotext
$output = shell_exec('which pdftotext 2>&1');
if (empty($output)) {
return false;
}
$tempFile = tempnam(sys_get_temp_dir(), 'pdf_text_');
$command = "pdftotext '{$filePath}' '{$tempFile}' 2>&1";
shell_exec($command);
if (file_exists($tempFile)) {
$text = file_get_contents($tempFile);
unlink($tempFile);
return $text;
}
return false;
}
// 设置方法
public function setMaxPages($pages) {
$this->maxPages = $pages;
return $this;
}
public function setMaxSize($sizeInBytes) {
$this->maxSize = $sizeInBytes;
return $this;
}
}
// 使用示例
$pdfUploader = new PdfUploader();
$pdfUploader
->setMaxPages(100)
->setMaxSize(20 * 1024 * 1024); // 20MB
if (isset($_FILES['pdf_document'])) {
$result = $pdfUploader->uploadPdf('pdf_document');
if ($result['success']) {
echo "<div style='border: 2px solid green; padding: 20px; margin: 20px;'>";
echo "<h3>PDF文件上传成功!</h3>";
echo "<p><strong>文件名称:</strong> {$result['original_name']}</p>";
echo "<p><strong>保存名称:</strong> {$result['saved_name']}</p>";
echo "<p><strong>文件大小:</strong> " . formatBytes($result['file_size']) . "</p>";
echo "<p><strong>页数:</strong> {$result['page_count']}</p>";
echo "<p><strong>上传时间:</strong> {$result['upload_time']}</p>";
// 提取文本内容
$text = $pdfUploader->extractPdfText($result['file_path']);
if ($text) {
echo "<p><strong>文本预览(前500字符):</strong></p>";
echo "<div style='border: 1px solid #ddd; padding: 10px; max-height: 200px; overflow-y: auto;'>";
echo htmlspecialchars(substr($text, 0, 500)) . "...";
echo "</div>";
}
echo "</div>";
} else {
echo "<div style='border: 2px solid red; padding: 20px; margin: 20px;'>";
echo "<h3>PDF文件上传失败!</h3>";
echo "<p>错误信息:{$result['message']}</p>";
echo "</div>";
}
}
function formatBytes($bytes, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$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, $precision) . ' ' . $units[$pow];
}
?>
| 配置项 | 默认值 | 推荐值 | 描述 |
|---|---|---|---|
file_uploads |
On |
On |
是否允许HTTP文件上传 |
upload_max_filesize |
2M |
10M 或更大 |
允许上传的文件最大尺寸 |
post_max_size |
8M |
12M(比upload_max_filesize大) |
POST数据最大尺寸(必须大于upload_max_filesize) |
max_file_uploads |
20 |
50 |
一次请求中最多上传的文件数 |
upload_tmp_dir |
系统默认 | 自定义目录 | 上传临时文件的目录 |
max_input_time |
60 |
300 |
脚本接收输入数据的最大时间(秒) |
max_execution_time |
30 |
300 |
脚本最大执行时间(秒) |
memory_limit |
128M |
256M 或更大 |
脚本内存限制 |
<?php
/**
* 在运行时调整文件上传配置
*/
class UploadConfig {
public static function adjustForLargeFiles() {
// 增加上传文件大小限制
ini_set('upload_max_filesize', '50M');
ini_set('post_max_size', '55M');
// 增加脚本执行时间(大文件需要更长时间)
ini_set('max_execution_time', '600'); // 10分钟
ini_set('max_input_time', '600');
// 增加内存限制
ini_set('memory_limit', '512M');
// 设置自定义临时目录(确保可写)
$tempDir = sys_get_temp_dir() . '/custom_uploads/';
if (!file_exists($tempDir)) {
mkdir($tempDir, 0755, true);
}
ini_set('upload_tmp_dir', $tempDir);
}
public static function getCurrentConfig() {
return [
'file_uploads' => ini_get('file_uploads'),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'max_file_uploads' => ini_get('max_file_uploads'),
'upload_tmp_dir' => ini_get('upload_tmp_dir'),
'max_execution_time' => ini_get('max_execution_time'),
'max_input_time' => ini_get('max_input_time'),
'memory_limit' => ini_get('memory_limit')
];
}
public static function checkRequirements() {
$errors = [];
$config = self::getCurrentConfig();
// 检查文件上传是否启用
if ($config['file_uploads'] !== '1') {
$errors[] = '文件上传功能被禁用';
}
// 检查临时目录是否可写
$tempDir = $config['upload_tmp_dir'] ?: sys_get_temp_dir();
if (!is_writable($tempDir)) {
$errors[] = "临时目录不可写:{$tempDir}";
}
// 检查上传大小限制
$uploadMax = self::convertToBytes($config['upload_max_filesize']);
$postMax = self::convertToBytes($config['post_max_size']);
if ($postMax < $uploadMax) {
$errors[] = "post_max_size 必须大于 upload_max_filesize";
}
return [
'config' => $config,
'errors' => $errors,
'passed' => empty($errors)
];
}
/**
* 将配置文件中的大小字符串转换为字节
*/
private static function convertToBytes($sizeStr) {
$unit = strtoupper(substr($sizeStr, -1));
$size = (int)substr($sizeStr, 0, -1);
switch ($unit) {
case 'G': $size *= 1024;
case 'M': $size *= 1024;
case 'K': $size *= 1024;
}
return $size;
}
}
// 使用示例
echo "<h3>当前上传配置:</h3>";
$currentConfig = UploadConfig::getCurrentConfig();
foreach ($currentConfig as $key => $value) {
echo "<p><strong>$key:</strong> $value</p>";
}
echo "<h3>配置检查:</h3>";
$check = UploadConfig::checkRequirements();
if ($check['passed']) {
echo "<div style='color: green;'>✓ 所有配置检查通过</div>";
} else {
echo "<div style='color: red;'>✗ 发现配置问题:</div>";
foreach ($check['errors'] as $error) {
echo "<p>• $error</p>";
}
}
// 为大文件上传调整配置
UploadConfig::adjustForLargeFiles();
echo "<h3>已为大文件上传调整配置</h3>";
?>
finfo_file()或getimagesize())# 禁止上传目录执行PHP
<FilesMatch "\.(php|php5|phtml)$">
Order Allow,Deny
Deny from all
</FilesMatch>
# 限制直接访问某些文件类型
<FilesMatch "\.(htaccess|htpasswd|ini|log|sh|sql)$">
Order Allow,Deny
Deny from all
</FilesMatch>
# 禁用目录浏览
Options -Indexes
# 设置文件缓存头
<FilesMatch "\.(jpg|jpeg|png|gif|pdf)$">
Header set Cache-Control "max-age=2592000, public"
</FilesMatch>
# 限制文件大小(服务器端)
php_value upload_max_filesize 10M
php_value post_max_size 12M
# 添加安全头
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
<?php
// 生成CSRF令牌
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// 处理AJAX上传请求
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax_upload'])) {
// 验证CSRF令牌
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'CSRF令牌验证失败']);
exit;
}
// 验证文件上传
if (!isset($_FILES['file'])) {
echo json_encode(['success' => false, 'message' => '没有文件被上传']);
exit;
}
$file = $_FILES['file'];
// 安全文件上传处理
$uploader = new SecureFileUploader('uploads/');
$uploader
->setAllowedExtensions(['jpg', 'jpeg', 'png', 'gif'])
->setAllowedMimeTypes(['image/jpeg', 'image/png', 'image/gif'])
->setMaxFileSize(2 * 1024 * 1024);
$result = $uploader->upload('file');
if ($result) {
echo json_encode([
'success' => true,
'message' => '文件上传成功',
'file_name' => $result['original_name'],
'file_url' => $result['file_path'],
'file_size' => $result['file_size']
]);
} else {
echo json_encode([
'success' => false,
'message' => '文件上传失败',
'errors' => $uploader->getErrors()
]);
}
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<title>安全的AJAX文件上传</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<form id="uploadForm" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
<input type="hidden" name="ajax_upload" value="1">
<input type="file" name="file" id="fileInput" required>
<button type="submit">上传文件</button>
</form>
<div id="result"></div>
<script>
$(document).ready(function() {
$('#uploadForm').on('submit', function(e) {
e.preventDefault();
var formData = new FormData(this);
$.ajax({
url: 'upload.php',
type: 'POST',
data: formData,
processData: false,
contentType: false,
dataType: 'json',
success: function(response) {
if (response.success) {
$('#result').html('<div style="color: green;">' + response.message + '</div>');
$('#result').append('<p>文件名:' + response.file_name + '</p>');
$('#result').append('<p>文件大小:' + (response.file_size / 1024).toFixed(2) + ' KB</p>');
} else {
$('#result').html('<div style="color: red;">' + response.message + '</div>');
if (response.errors) {
$('#result').append('<ul>');
response.errors.forEach(function(error) {
$('#result').append('<li>' + error + '</li>');
});
$('#result').append('</ul>');
}
}
},
error: function() {
$('#result').html('<div style="color: red;">上传失败,请重试</div>');
}
});
});
});
</script>
</body>
</html>
需要调整以下配置:
upload_max_filesize = 10Mpost_max_size = 12M
php_value upload_max_filesize 10Mphp_value post_max_size 12M
ini_set('upload_max_filesize', '10M');ini_set('post_max_size', '12M');
post_max_size必须大于upload_max_filesize,因为POST数据包含文件和其他表单数据。
多层次防御策略:
finfo_file()检测真实MIME类型getimagesize()验证<?php
// 大文件分片上传示例
class ChunkedFileUpload {
private $tempDir = 'uploads/temp/';
private $finalDir = 'uploads/final/';
public function __construct() {
if (!file_exists($this->tempDir)) mkdir($this->tempDir, 0755, true);
if (!file_exists($this->finalDir)) mkdir($this->finalDir, 0755, true);
}
public function uploadChunk($fileInputName, $chunkNumber, $totalChunks, $fileName) {
if (!isset($_FILES[$fileInputName])) {
return ['success' => false, 'message' => '没有文件被上传'];
}
$file = $_FILES[$fileInputName];
$tempFilePath = $this->tempDir . $fileName . '.part' . $chunkNumber;
// 保存分片
if (!move_uploaded_file($file['tmp_name'], $tempFilePath)) {
return ['success' => false, 'message' => '分片保存失败'];
}
// 检查是否所有分片都已上传
$uploadedChunks = 0;
for ($i = 1; $i <= $totalChunks; $i++) {
if (file_exists($this->tempDir . $fileName . '.part' . $i)) {
$uploadedChunks++;
}
}
// 如果所有分片都已上传,合并文件
if ($uploadedChunks == $totalChunks) {
$finalFilePath = $this->finalDir . $fileName;
$finalFile = fopen($finalFilePath, 'wb');
for ($i = 1; $i <= $totalChunks; $i++) {
$chunkFile = $this->tempDir . $fileName . '.part' . $i;
$chunkContent = file_get_contents($chunkFile);
fwrite($finalFile, $chunkContent);
unlink($chunkFile); // 删除分片文件
}
fclose($finalFile);
return [
'success' => true,
'message' => '文件上传完成',
'file_path' => $finalFilePath,
'file_size' => filesize($finalFilePath)
];
}
return [
'success' => true,
'message' => "分片 {$chunkNumber}/{$totalChunks} 上传成功",
'progress' => round(($uploadedChunks / $totalChunks) * 100, 2)
];
}
}
// 使用示例
if (isset($_POST['chunked_upload'])) {
$uploader = new ChunkedFileUpload();
$result = $uploader->uploadChunk(
$_POST['file_input'] ?? 'file',
$_POST['chunk_number'] ?? 1,
$_POST['total_chunks'] ?? 1,
$_POST['file_name'] ?? 'uploaded_file'
);
echo json_encode($result);
}
?>
性能优化建议: