PHP实现验证文件信息的四六级照片上传维护

前言


我们都知道,在大学英语四六级报名时,照片是不可以自行修改的,但是,所使用的高中拍摄的照片,会因为受到效率等的影响,不符合本人的正常特征。或太暗、或角度不对、或长大以后面相改变等原因,需要修改。这时,一般学校的教务处,一般采用下面的方法,且存在问题:

  • 让学生发邮件到负责老师处。这个时候,会出现,有的学生不按照要求修改照片的文件名,不按照要求设置照片格式,或者忘记附上身份证号码,拖慢工作。而且老师公开邮箱到互联网,也存在黑客钓鱼的风险。
  • 让学生发到年级责任人处。这个时候,会出现,一些年级责任人不理解要求,传达错误;或者不按要求收集文件。影响整体的工作进度,或者年级责任人泄露考生身份信息的问题。
  • 使用老旧的PHP上传系统。一般这些系统很老旧,既不做文件名检查,也不会按安全规范保存文件,存在注入木马或者爆破的风险。而且不检查文件名也会拖慢老师的工作。

这个时候,基于PHP的轻量化设计的四六级照片维护系统孕育而生。

项目地址


GitHub:https://github.com/little-gt/TOOLS-jwcsiliujiimgweihu

需要解决的问题


为了提高用户体验以及节省服务器资源,在实际工程项目中,我们一般采用前端基础验证,加上后端再次验证的方式进行安全设计。我们也从这两方面着手,解决技术问题。

  1. 限制上传的文件格式;
  2. 限制上传的文件数量;
  3. 限制上传的文件名为“身份证号码.jpg”;
  4. 有安全保护措施,避免注入等攻击。

前端逻辑


示例
当我们随意打开一个前端上传文件的界面,无外乎有上面的几个要求

在开始之前,我们不妨创建一个上传文件的点选框input,如下所示:

之所以这样写,是出于扩展功能考虑:

  1. 使用点选框,就可以限定拖拽上传时,接受拖拽文件的消息的部分;
  2. 出现错误时,不使用弹窗通知,而是转为在选择框内直接通知,优化用户体验的同时,又符合最新的浏览器页面技术要求,尽可能少的弹窗打扰用户。
<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(大写)的校验码。因此我们可以考虑通过先判断身份号码内容是否符合要求,以及计算得到的校验码是否正确来就行判断。

简单的来说,可以画出下面的验证流程图,然后根据流程图验证:

判断前17位是否是数字
判断第18位是否是数字或者字母X
计算校验码
原始文件名
未知第18位校验码情况
格式正确但内容不确定
完成验证
/**
 * 验证身份证号码是否正确
 *
 * @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 许可协议发布,但并不保证:

  1. 代码或其他材料没有错误或者不会导致任何损害。
  2. 任何使用本项目代码或相关资源所产生的结果准确性或可靠性。
  3. 本项目能够满足特定用户的所有需求。

使用者应当自行承担使用本项目所带来的风险。对于因使用本项目所引起的任何形式的损失,作者及贡献者概不负责。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值