Files
qingdao-employment-service/utils/fileValidator.js

302 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* FileValidator.js
* 封装好的文件安全校验类
*/
// ==========================================
// 1. 预定义:已知文件类型的魔数 (Signature Database)
// ==========================================
const KNOWN_SIGNATURES = {
// === 图片 ===
png: '89504E470D0A1A0A',
jpg: 'FFD8FF',
jpeg: 'FFD8FF',
gif: '47494638',
webp: '52494646', // RIFF Header
// === 文档 (Office 新版 - ZIP 格式) ===
docx: '504B0304',
xlsx: '504B0304',
pptx: '504B0304',
// === 文档 (Office 旧版 - OLECF 格式) ===
doc: 'D0CF11E0',
xls: 'D0CF11E0',
ppt: 'D0CF11E0',
// === 其他 ===
pdf: '25504446',
// === 纯文本 (无固定魔数,需特殊算法检测) ===
txt: 'TYPE_TEXT',
csv: 'TYPE_TEXT',
md: 'TYPE_TEXT',
json: 'TYPE_TEXT',
};
// ==========================================
// 2. 核心类定义
// ==========================================
export class FileValidator {
/**
* 构造函数
* @param {Object} options 配置项
* @param {number} [options.maxSizeMB=10] 最大文件大小 (MB)
* @param {string[]} [options.allowedExtensions = ['png','jpg','jpeg','gif','webp','svg','bmp','ico','pdf','doc','docx','xls','xlsx','csv','ppt','pptx','txt','md']] 允许的扩展名列表 (如 ['jpg', 'png']),默认允许全部已知类型
*/
version = '1.0.0';
constructor(options = {}) {
// 配置大小 (默认 10MB)
this.maxSizeMB = options.maxSizeMB || 10;
// 扩展名到 MIME 的映射(用于反向查找)
this.extToMime = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
webp: 'image/webp',
pdf: 'application/pdf',
txt: 'text/plain',
md: 'text/markdown',
json: 'application/json',
csv: 'text/csv',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
};
// 如果传入的是 MIME 类型,转换为扩展名
let allowedExtensions = options.allowedExtensions || Object.keys(KNOWN_SIGNATURES);
// 配置允许的类型
this.allowedConfig = {};
allowedExtensions.forEach((extOrMime) => {
const key = extOrMime.toLowerCase();
// 如果是 MIME 类型,尝试转换为扩展名
let ext = key;
if (key.includes('/')) {
// 查找对应的扩展名
for (const [e, mime] of Object.entries(this.extToMime)) {
if (mime === key) {
ext = e;
break;
}
}
}
if (KNOWN_SIGNATURES[ext]) {
this.allowedConfig[ext] = KNOWN_SIGNATURES[ext];
} else {
console.warn(`[FileValidator] 未知的文件类型: ${key},已忽略`);
}
});
}
/**
* 改进版:检查是否为有效的 UTF-8 文本
*/
_isValidUTF8(buffer) {
try {
// fatal: true 会在遇到无效编码时抛出错误,而不是用 替换
const decoder = new TextDecoder('utf-8', {
fatal: true,
});
decoder.decode(buffer);
return true;
} catch (e) {
return false;
}
}
/**
* 辅助ArrayBuffer 转 Hex 字符串
*/
_bufferToHex(buffer) {
return Array.prototype.map
.call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2))
.join('')
.toUpperCase();
}
/**
* 【新增】统计 CSV 行数(严谨版:忽略引号内的换行符)
* 性能:对于 10MB 文件,现代浏览器处理通常在 100ms 以内
*/
_countCSVRows(buffer) {
const decoder = new TextDecoder('utf-8');
const text = decoder.decode(buffer);
let rowCount = 0;
let inQuote = false;
let len = text.length;
// 遍历每一个字符
for (let i = 0; i < len; i++) {
const char = text[i];
// 切换引号状态
if (char === '"') {
inQuote = !inQuote;
}
// 只有在非引号状态下的换行符,才算作一行结束
else if (char === '\n' && !inQuote) {
rowCount++;
}
}
// 处理最后一行没有换行符的情况(且文件不为空)
if (len > 0 && text[len - 1] !== '\n') {
rowCount++;
}
return rowCount;
}
/**
* 【核心】:校验纯文本内容
* 1. 检查是否包含乱码 (非 UTF-8)
* 2. 针对特定格式 (JSON) 进行语法解析
*/
_validateTextContent(buffer, extension) {
// 1. 尝试解码为 UTF-8
let contentStr = '';
try {
const decoder = new TextDecoder('utf-8', {
fatal: true,
});
contentStr = decoder.decode(buffer);
} catch (e) {
// 如果解码失败,说明包含非文本的二进制数据
console.warn('UTF-8 解码失败', e);
return false;
}
// 2. 检查是否存在过多的空字符 (二进制文件特征)
// 某些二进制文件可能勉强通过 UTF-8 解码,但会包含大量 \0
if (contentStr.includes('\u0000')) {
return false;
}
// 3. 针对特定后缀进行语法校验 (可选,更严格)
if (extension === 'json') {
try {
JSON.parse(contentStr);
} catch (e) {
console.warn('无效的 JSON 格式');
return false;
}
}
// 如果是 CSV可以简单检查行数可选
// if (extension === 'csv') { ... }
return true;
}
/**
* 执行校验
* @param {File} file 文件对象
* @returns {Promise<boolean>}
*/
validate(file) {
return new Promise((resolve, reject) => {
console.log('开始校验文件');
// 1. 基础对象检查
if (!file || !file.name) return reject('无效的文件对象');
// 2. 大小检查
if (file.size > this.maxSizeMB * 1024 * 1024) {
return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`);
}
// 3. 后缀名检查
const fileName = file.name.toLowerCase();
const extension = fileName.substring(fileName.lastIndexOf('.') + 1);
// 检查是否在配置的白名单中
const expectedMagic = this.allowedConfig[extension];
if (!expectedMagic) {
return reject(`不支持的文件格式: .${extension}`);
}
// 4. 读取二进制头进行魔数校验
const reader = new FileReader();
reader.onload = (e) => {
const buffer = e.target.result;
let isSafe = false;
// 分支处理:纯文本 vs 二进制
if (expectedMagic === 'TYPE_TEXT') {
if (this._validateTextContent(buffer, extension)) {
isSafe = true;
} else {
// 细化报错信息
if (extension === 'json') {
return reject(`文件异常:不是有效的 JSON 文件`);
}
return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`);
}
// 【新增】专门针对 CSV 的行数检查
if (extension === 'csv' && this.csvMaxRows > 0) {
const rows = this._countCSVRows(buffer);
// 注意:这里通常把表头也算作 1 行,如果不算表头可以将 limit + 1
if (rows > this.csvMaxRows) {
return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`);
}
}
} else {
// 获取文件头 Hex (读取足够长的字节以覆盖最长的魔数PNG需8字节)
const fileHeader = this._bufferToHex(buffer.slice(0, 8));
// 使用 startsWith 匹配
if (fileHeader.startsWith(expectedMagic)) {
isSafe = true;
} else {
return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`);
}
}
if (isSafe) resolve(true);
};
reader.onerror = () => reject('文件读取失败,无法校验');
// 读取前 1KB 进行判断
if (expectedMagic === 'TYPE_TEXT' && extension === 'json') {
// JSON 必须读全量才能 parse建议限制 JSON 文件大小
reader.readAsArrayBuffer(file);
} else {
// 图片/普通文本 读取前 2KB 足够判断头部和编码特征
reader.readAsArrayBuffer(file.slice(0, 2048));
}
});
}
}
// 【demo】
// 如果传入了 allowedExtensions则只使用传入的否则使用全部 KNOWN_SIGNATURES
// const imageValidator = new FileValidator({
// maxSizeMB: 5,
// allowedExtensions: ['png', 'jpg', 'jpeg'],
// });
// imageValidator
// .validate(file)
// .then(() => {
// statusDiv.textContent = `检测通过: ${file.name}`;
// statusDiv.style.color = 'green';
// console.log('图片校验通过,开始上传...');
// // upload(file)...
// })
// .catch((err) => {
// statusDiv.textContent = `检测失败: ${err}`;
// statusDiv.style.color = 'red';
// });