Files
qingdao-employment-service/utils/fileValidator.js
2025-12-19 10:25:10 +08:00

181 lines
6.1 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
* 封装好的文件安全校验类 (ES Module)
*/
// ==========================================
// 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] 允许的扩展名列表 (如 ['jpg', 'png']),默认允许全部已知类型
*/
version = '1.0.0';
constructor(options = {}) {
// 配置大小 (默认 10MB)
this.maxSizeMB = options.maxSizeMB || 10;
// 配置允许的类型
// 如果传入了 allowedExtensions则只使用传入的否则使用全部 KNOWN_SIGNATURES
if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) {
this.allowedConfig = {};
options.allowedExtensions.forEach((ext) => {
const key = ext.toLowerCase();
if (KNOWN_SIGNATURES[key]) {
this.allowedConfig[key] = KNOWN_SIGNATURES[key];
} else {
console.warn(`[FileValidator] 未知的文件类型: .${key},已忽略`);
}
});
} else {
this.allowedConfig = {
...KNOWN_SIGNATURES
};
}
}
/**
* 辅助ArrayBuffer 转 Hex 字符串
*/
_bufferToHex(buffer) {
return Array.prototype.map
.call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2))
.join('')
.toUpperCase();
}
/**
* 辅助:纯文本抽样检测
*/
_isCleanText(buffer) {
const bytes = new Uint8Array(buffer);
const checkLen = Math.min(bytes.length, 1000);
let suspiciousCount = 0;
for (let i = 0; i < checkLen; i++) {
const byte = bytes[i];
// 允许常见控制符: 9(Tab), 10(LF), 13(CR)
// 0-31 范围内其他的通常是二进制控制符
if (byte < 32 && ![9, 10, 13].includes(byte)) {
suspiciousCount++;
}
}
// 如果可疑字符占比 < 5%,认为是纯文本
return suspiciousCount / checkLen < 0.05;
}
/**
* 执行校验
* @param {File} file 文件对象
* @returns {Promise<boolean>}
*/
validate(file) {
return new Promise((resolve, reject) => {
// 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._isCleanText(buffer)) {
isSafe = true;
} else {
return reject(`文件异常:.${extension} 文件包含非法二进制内容`);
}
} else {
// 获取文件头 Hex (读取足够长的字节以覆盖最长的魔数PNG需8字节)
const fileHeader = this._bufferToHex(buffer.slice(0, 8));
// 使用 startsWith 匹配 (兼容 4字节或8字节魔数)
if (fileHeader.startsWith(expectedMagic)) {
isSafe = true;
} else {
return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`);
}
}
if (isSafe) resolve(true);
};
reader.onerror = () => reject('文件读取失败,无法校验');
// 读取前 1KB 足够进行判断
reader.readAsArrayBuffer(file.slice(0, 1024));
});
}
}
// 【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';
// });