前言
我们都知道,在大学英语四六级报名时,照片是不可以自行修改的,但是,所使用的高中拍摄的照片,会因为受到效率等的影响,不符合本人的正常特征。或太暗、或角度不对、或长大以后面相改变等原因,需要修改。这时,一般学校的教务处,一般采用下面的方法,且存在问题:
- 让学生发邮件到负责老师处。这个时候,会出现,有的学生不按照要求修改照片的文件名,不按照要求设置照片格式,或者忘记附上身份证号码,拖慢工作。而且老师公开邮箱到互联网,也存在黑客钓鱼的风险。
- 让学生发到年级责任人处。这个时候,会出现,一些年级责任人不理解要求,传达错误;或者不按要求收集文件。影响整体的工作进度,或者年级责任人泄露考生身份信息的问题。
- 使用老旧的PHP上传系统。一般这些系统很老旧,既不做文件名检查,也不会按安全规范保存文件,存在注入木马或者爆破的风险。而且不检查文件名也会拖慢老师的工作。
这个时候,基于PHP的轻量化设计的四六级照片维护系统孕育而生。
项目地址
GitHub:https://github.com/little-gt/TOOLS-jwcsiliujiimgweihu
需要解决的问题
为了提高用户体验以及节省服务器资源,在实际工程项目中,我们一般采用前端基础验证,加上后端再次验证的方式进行安全设计。我们也从这两方面着手,解决技术问题。
- 限制上传的文件格式;
- 限制上传的文件数量;
- 限制上传的文件名为“
身份证号码.jpg
”; - 有安全保护措施,避免注入等攻击。
前端逻辑
当我们随意打开一个前端上传文件的界面,无外乎有上面的几个要求
在开始之前,我们不妨创建一个上传文件的点选框input
,如下所示:
之所以这样写,是出于扩展功能考虑:
- 使用点选框,就可以限定拖拽上传时,接受拖拽文件的消息的部分;
- 出现错误时,不使用弹窗通知,而是转为在选择框内直接通知,优化用户体验的同时,又符合最新的浏览器页面技术要求,尽可能少的弹窗打扰用户。
<div class="drop-zone" id="dropZone">
<div class="message" id="message">将照片拖放到这里或点击选择文件</div>
<input type="file" id="photo" name="photo" accept="image/jpeg" required>
<div class="error-message" id="errorMessage"></div>
</div>
通过JavaScript知识可知,浏览器中已经自带了拖拽上传和点选上传的方法,我们只需要通过JavaScript绑定与之对应的元素即可,此时很显然,这个div
元素就是id="dropZone"
。
首先定义几个元素的句柄:
const dropZone = document.getElementById('dropZone');
const message = document.getElementById('message');
const errorMessage = document.getElementById('errorMessage');
const photoInput = document.getElementById('photo');
为上传区域,绑定拖拽上传和点选上传的方法(注意:因为拖拽上传支持同时拖拽多个文件,所以需要检查拖入文件的数量是否为1,如果不是1就应该提醒):
function handleDragEvents(event) {
event.preventDefault();
event.stopPropagation();
}
function handleDrop(event) {
handleDragEvents(event);
let files = event.dataTransfer.files;
if (files.length = 1) {
photoInput.files = files;
validatePhoto(files[0]);
}
if (files.length > 1) {
message.textContent = '';
errorMessage.textContent = ''; // 清空错误消息
errorMessage.textContent = "只允许选择一个文件,请重新操作。";
submitBtn.disabled = true;
}
dropZone.classList.remove('active');
}
document.addEventListener("DOMContentLoaded", function () {
dropZone.addEventListener("dragenter", handleDragEvents);
dropZone.addEventListener("dragover", handleDragEvents);
dropZone.addEventListener("drop", handleDrop);
// 确保点击选择文件后也能进行验证
dropZone.addEventListener("click", function() {
photoInput.click();
});
// 监听文件输入变化
photoInput.addEventListener("change", function() {
validatePhoto(photoInput.files[0]);
});
const form = document.querySelector("form");
form.addEventListener("submit", function (event) {
if (errorMessage.textContent) {
event.preventDefault();
}
});
});
此时再对选择的文件进行验证,也就是验证文件格式和文件的大小,文件名因为涉及到身份照号码的验证,可以留到后端:
function validatePhoto(file) {
message.textContent = '';
errorMessage.textContent = ''; // 清空错误消息
if (!file) {
errorMessage.textContent = "还没有选择任何文件,请选择文件";
submitBtn.disabled = true;
return;
}
if (file.type !== "image/jpeg") {
errorMessage.textContent = "只接受JPG格式的照片,请重新选择";
submitBtn.disabled = true;
return;
}
if (file.size > 180 * 1024) {
errorMessage.textContent = "照片大小不能超过180KB,请重新选择";
submitBtn.disabled = true;
return;
}
// 如果文件通过验证,则显示文件名并启用上传按钮
message.textContent = `你已选中文件: ${file.name}`;
submitBtn.disabled = false;
}
之所以再次验证是否为空,是因为点选文件以后,是允许取消选择的,这个时候会清空选择文件的句柄,直接post空内容到后端,引起报错,当然这个也需要在后端再次验证,但是显然会影响用户体验。
我们再简单进行一下UI设计,就可以得到美观的上传页面了
后端逻辑
首先,为了方便维护和复用,我们把一些固定的参数先进行定义,也就是:
// 目标文件夹
$target_dir = "../upload/";
// 文件名
$target_file = $target_dir . basename($_FILES["photo"]["name"]);
// 允许的文件类型
$allowed_types = array("jpg", "jpeg");
// 文件大小限制
$max_file_size_kb = 180;
第一,有时候老师们会忘记创建上传文件保存为的路径,这个时候就需要提前中断流程,避免文件因为PHP设置在另存为失败以后,潜在的保存到其他意外位置的可能:
// 检查目标文件夹是否存在
if (!is_dir($target_dir)) {
session_start(); // 启动会话
$_SESSION['err_message'] = "目标文件夹不存在,请联系系统管理员处理此问题";
header("Location: result.php?error=" . urlencode("目标文件夹不存在,请联系系统管理员处理此问题"));
exit;
}
第二,就算前端JavaScrip限制了上传的文件数量,但是也不派出出错或者注入攻击的可能,这个时候需要检查POST中文件的数量:
// 检查上传的文件数量
if (count($_FILES["photo"]["name"]) > 1) {
session_start(); // 启动会话
$_SESSION['err_message'] = "只能上传一个文件,请确认后再试";
header("Location: result.php?error=" . urlencode("只能上传一个文件,请确认后再试"));
exit;
}
到这里,一些基本的准备工作已经完成了,但是后面的验证,需要检查文件名和文件类型,就需要对上传的文件在进行一些处理。也就是分离文件名和后缀,以及判断大小。
// 获取文件信息
$file_name = basename($_FILES["photo"]["name"]);
$file_type = pathinfo($file_name, PATHINFO_EXTENSION);
$file_size = $_FILES["photo"]["size"] / 1024; // 转换为KB
// 分离文件名和扩展名
$file_base_name = pathinfo($file_name, PATHINFO_FILENAME);
此时,我们再检查文件的大小是否满足要求,以及文件的后缀名(注意,因为早期Windows只支持三个字符的后缀名,所以是.jpg;之后Windows及Linux支持多位字符后缀名,才又有了.jpeg)是否符合要求:
if (!in_array($file_type, $allowed_types)) {
$errorMessage = "只允许JPG类型的文件";
} elseif ($file_size > $max_file_size_kb) { // 验证文件大小
$errorMessage = "照片文件的大小不允许超过180KB";
}
身份证号码规则是:18位,前17位为数字,最后一位为数字或字母X(大写)的校验码。因此我们可以考虑通过先判断身份号码内容是否符合要求,以及计算得到的校验码是否正确来就行判断。
简单的来说,可以画出下面的验证流程图,然后根据流程图验证:
/**
* 验证身份证号码是否正确
*
* @param string $idCard 身份证号码
* @return bool 是否有效
*/
function validateIDCard($idCard) {
// 验证身份证号码长度
if (strlen($idCard) !== 18) {
return false;
}
// 验证前17位是否全部为数字
if (!ctype_digit(substr($idCard, 0, 17))) {
return false;
}
// 验证第18位是否为数字或X/x
if (!ctype_digit(substr($idCard, 17, 1)) && !in_array(strtoupper(substr($idCard, 17, 1)), ['X'])) {
return false;
}
// 定义校验码
$checkCode = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
// 定义系数数组
$factorArray = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
// 计算校验码
$sum = 0;
for ($i = 0; $i < 17; $i++) {
$sum += substr($idCard, $i, 1) * $factorArray[$i];
}
$mod = $sum % 11;
$checkDigit = $checkCode[$mod];
// 比较校验码
if (strtoupper(substr($idCard, 17, 1)) != $checkDigit) {
return false;
}
return true;
}
完成验证以后,就可以进行保存了,不过考虑到保存过程中可能出现异常,导致移动失败等情况,还需要对是否成功进行验证。
因为我们之前定义了目标位置位置和文件名为$target_file = $target_dir . basename($_FILES["photo"]["name"]);
就可以有:
// 移动上传的文件
if (move_uploaded_file($_FILES["photo"]["tmp_name"], $target_file)) {
// 检查文件是否成功移动到指定位置
if (file_exists($target_file)) {
session_start(); // 启动会话
$_SESSION['suc_message'] = "文件 ".htmlspecialchars(basename($_FILES["photo"]["name"])). " 已上传";
header("Location: result.php?success=文件 ".htmlspecialchars(basename($_FILES["photo"]["name"])). " 已上传");
exit;
} else {
session_start(); // 启动会话
$_SESSION['err_message'] = "上传过程中发生了错误";
header("Location: result.php?error=" . urlencode("上传过程中发生了错误"));
}
}
总结
此时,解决了重要的问题以后,剩下的其实就没有很难了,对于传输结果的方式,实际上就是替换占位符,在之前的文章就已经运用过了,这里就不进行赘述了,只不过整合了错误信息和成功信息的传递而已。
消息传递页面展示
技术交流文档声明
本技术交流文档旨在促进参与者之间的信息共享和技术讨论,并仅用于非商业目的的技术研究与学习。本文档中的观点、结论及任何技术建议均不代表文中提及的任何单位、组织或个人的意见或立场。所有内容均基于撰写时可获得的信息,并可能随时更新而不另行通知。
请注意,本项目采用 MIT 许可协议发布,但并不保证:
- 代码或其他材料没有错误或者不会导致任何损害。
- 任何使用本项目代码或相关资源所产生的结果准确性或可靠性。
- 本项目能够满足特定用户的所有需求。
使用者应当自行承担使用本项目所带来的风险。对于因使用本项目所引起的任何形式的损失,作者及贡献者概不负责。