feat: 实现Vue驱动的云存储系统初始功能

- 后端: Node.js + Express + SQLite架构
- 前端: Vue 3 + Axios实现
- 功能: 用户认证、文件上传/下载、分享链接、密码重置
- 安全: 密码加密、分享链接过期机制、缓存一致性
- 部署: Docker + Nginx容器化配置
- 测试: 完整的边界测试、并发测试和状态一致性测试
This commit is contained in:
Dev Team
2026-01-20 23:23:51 +08:00
commit b7b00fff48
45 changed files with 51758 additions and 0 deletions

271
backend/utils/encryption.js Normal file
View File

@@ -0,0 +1,271 @@
/**
* 加密工具模块
*
* 功能:
* - 使用 AES-256-GCM 加密敏感数据OSS Access Key Secret
* - 提供加密和解密函数
* - 自动处理初始化向量(IV)和认证标签
*
* 安全特性:
* - AES-256-GCM 提供认证加密AEAD
* - 每次加密使用随机 IV防止模式泄露
* - 使用认证标签验证数据完整性
* - 密钥从环境变量读取,不存在硬编码
*
* @module utils/encryption
*/
const crypto = require('crypto');
/**
* 从环境变量获取加密密钥
*
* 要求:
* - 必须是 32 字节的十六进制字符串64个字符
* - 如果未设置或格式错误,启动时抛出错误
*
* @returns {Buffer} 32字节的加密密钥
* @throws {Error} 如果密钥未配置或格式错误
*/
function getEncryptionKey() {
const keyHex = process.env.ENCRYPTION_KEY;
if (!keyHex) {
throw new Error(`
╔═══════════════════════════════════════════════════════════════╗
║ ⚠️ 安全错误 ⚠️ ║
╠═══════════════════════════════════════════════════════════════╣
║ ENCRYPTION_KEY 未配置! ║
║ ║
║ 此密钥用于加密 OSS Access Key Secret 等敏感信息 ║
║ ║
║ 生成方法: ║
║ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
║ ║
║ 在 backend/.env 文件中添加: ║
║ ENCRYPTION_KEY=你生成的64位十六进制密钥 ║
╚═══════════════════════════════════════════════════════════════╝
`);
}
// 验证密钥格式必须是64个十六进制字符
if (!/^[0-9a-fA-F]{64}$/.test(keyHex)) {
throw new Error(`
╔═══════════════════════════════════════════════════════════════╗
║ ⚠️ 配置错误 ⚠️ ║
╠═══════════════════════════════════════════════════════════════╣
║ ENCRYPTION_KEY 格式错误! ║
║ ║
║ 要求: 64位十六进制字符串32字节
║ 当前长度: ${keyHex.length} 字符 ║
║ ║
║ 正确的生成方法: ║
║ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
╚═══════════════════════════════════════════════════════════════╝
`);
}
return Buffer.from(keyHex, 'hex');
}
/**
* 加密明文字符串
*
* 使用 AES-256-GCM 算法加密数据,输出格式:
* - Base64(IV + ciphertext + authTag)
* - IV: 12字节随机
* - ciphertext: 加密后的数据
* - authTag: 16字节认证标签
*
* @param {string} plaintext - 要加密的明文字符串
* @returns {string} Base64编码的加密结果包含 IV 和 authTag
* @throws {Error} 如果加密失败
*
* @example
* const encrypted = encryptSecret('my-secret-key');
* // 输出: 'base64-encoded-string-with-iv-and-tag'
*/
function encryptSecret(plaintext) {
try {
// 获取加密密钥
const key = getEncryptionKey();
// 生成随机初始化向量IV
// GCM 模式推荐 12 字节 IV
const iv = crypto.randomBytes(12);
// 创建加密器
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
// 加密数据
let encrypted = cipher.update(plaintext, 'utf8', 'binary');
encrypted += cipher.final('binary');
// 获取认证标签(用于验证数据完整性)
const authTag = cipher.getAuthTag();
// 组合IV + encrypted + authTag
const combined = Buffer.concat([
iv,
Buffer.from(encrypted, 'binary'),
authTag
]);
// 返回 Base64 编码的结果
return combined.toString('base64');
} catch (error) {
console.error('[加密] 加密失败:', error);
throw new Error('数据加密失败: ' + error.message);
}
}
/**
* 解密密文字符串
*
* 解密由 encryptSecret() 加密的数据
* 自动验证认证标签,确保数据完整性
*
* @param {string} ciphertext - Base64编码的密文由 encryptSecret 生成)
* @returns {string} 解密后的明文字符串
* @throws {Error} 如果解密失败或认证标签验证失败
*
* @example
* const decrypted = decryptSecret(encrypted);
* // 输出: 'my-secret-key'
*/
function decryptSecret(ciphertext) {
try {
// 如果是 null 或 undefined直接返回
if (!ciphertext) {
return ciphertext;
}
// 检查是否为加密格式Base64
// 如果不是 Base64可能是旧数据明文直接返回
if (!/^[A-Za-z0-9+/=]+$/.test(ciphertext)) {
console.warn('[加密] 检测到未加密的密钥,建议重新加密');
return ciphertext;
}
// 获取加密密钥
const key = getEncryptionKey();
// 解析 Base64
const combined = Buffer.from(ciphertext, 'base64');
// 提取各部分
// IV: 前 12 字节
const iv = combined.slice(0, 12);
// authTag: 最后 16 字节
const authTag = combined.slice(-16);
// ciphertext: 中间部分
const encrypted = combined.slice(12, -16);
// 创建解密器
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
// 设置认证标签
decipher.setAuthTag(authTag);
// 解密数据
let decrypted = decipher.update(encrypted, 'binary', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
// 如果解密失败,可能是旧数据(明文),直接返回
console.error('[加密] 解密失败,可能是未加密的旧数据:', error.message);
// 在开发环境抛出错误,生产环境尝试返回原值
if (process.env.NODE_ENV === 'production') {
console.error('[加密] 生产环境中解密失败,返回原值(可能导致 OSS 连接失败)');
return ciphertext;
}
throw new Error('数据解密失败: ' + error.message);
}
}
/**
* 验证加密系统是否正常工作
*
* 在应用启动时调用,确保:
* 1. ENCRYPTION_KEY 已配置
* 2. 加密/解密功能正常
*
* @returns {boolean} true 如果验证通过
* @throws {Error} 如果验证失败
*/
function validateEncryption() {
try {
const testData = 'test-secret-123';
// 测试加密
const encrypted = encryptSecret(testData);
// 验证加密结果不为空且不等于原文
if (!encrypted || encrypted === testData) {
throw new Error('加密结果异常');
}
// 测试解密
const decrypted = decryptSecret(encrypted);
// 验证解密结果等于原文
if (decrypted !== testData) {
throw new Error('解密结果不匹配');
}
console.log('[安全] ✓ 加密系统验证通过');
return true;
} catch (error) {
console.error('[安全] ✗ 加密系统验证失败:', error.message);
throw error;
}
}
/**
* 检查字符串是否已加密
*
* 通过格式判断是否为加密数据
* 注意:这不是加密学验证,仅用于提示
*
* @param {string} data - 要检查的数据
* @returns {boolean} true 如果看起来像是加密数据
*/
function isEncrypted(data) {
if (!data || typeof data !== 'string') {
return false;
}
// 加密后的数据特征:
// 1. 是有效的 Base64
// 2. 长度至少为 (12 + 16) * 4/3 = 38 字符IV + authTag 的 Base64
// 3. 通常会比原文长
try {
// 尝试解码 Base64
const buffer = Buffer.from(data, 'base64');
// 检查长度(至少包含 IV + authTag
// AES-GCM: 12字节IV + 至少1字节密文 + 16字节authTag = 29字节
// Base64编码后: 29 * 4/3 ≈ 39 字符
if (buffer.length < 29) {
return false;
}
return true;
} catch (error) {
return false;
}
}
module.exports = {
encryptSecret,
decryptSecret,
validateEncryption,
isEncrypted,
getEncryptionKey
};