fix: harden cloud storage security

This commit is contained in:
237899745
2026-06-13 18:45:12 +08:00
parent 7943b04ee2
commit bb6ad01018
28 changed files with 2229 additions and 996 deletions

View File

@@ -163,7 +163,10 @@ createApp({
uploadedBytes: 0,
totalBytes: 0,
uploadingFileName: '',
uploadPhase: '',
isDragging: false,
fileLoadRequestId: 0,
inspectionLoadRequestId: 0,
modalMouseDownTarget: null, // 模态框鼠标按下的目标
// 全局搜索(文件页)
@@ -723,6 +726,36 @@ createApp({
};
},
validateAccountPassword(password) {
const value = String(password || '');
if (value.length < 8) {
return { valid: false, message: '密码至少8个字符' };
}
if (value.length > 128) {
return { valid: false, message: '密码不能超过128个字符' };
}
const typeCount = [
/[a-zA-Z]/.test(value),
/[0-9]/.test(value),
/[!@#$%^&*(),.?":{}|<>_\-+=\[\]\\\/`~]/.test(value)
].filter(Boolean).length;
if (typeCount < 2) {
return { valid: false, message: '密码必须包含字母、数字、特殊字符中的至少两种' };
}
const weakPasswords = [
'password', '12345678', '123456789', 'qwerty123', 'admin123',
'letmein', 'welcome', 'monkey', 'dragon', 'master'
];
if (weakPasswords.includes(value.toLowerCase())) {
return { valid: false, message: '密码过于简单,请使用更复杂的密码' };
}
return { valid: true };
},
// 创建防抖版本的 loadUserProfile延迟2秒避免频繁请求
debouncedLoadUserProfile() {
if (!this._debouncedLoadUserProfile) {
@@ -1001,8 +1034,7 @@ handleDragLeave(e) {
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
await this.uploadFile(file);
await this.uploadFiles(files);
}
},
@@ -1031,6 +1063,27 @@ handleDragLeave(e) {
return generated;
},
getPersistableUser(user) {
if (!user || typeof user !== 'object') return null;
const {
oss_access_key_id,
oss_access_key_secret,
oss_provider,
oss_region,
oss_bucket,
oss_endpoint,
...safeUser
} = user;
return safeUser;
},
persistUser(user = this.user) {
const safeUser = this.getPersistableUser(user);
if (safeUser) {
localStorage.setItem('user', JSON.stringify(safeUser));
}
},
buildLoginClientMeta() {
const platform = navigator.platform || '未知平台';
const name = `${navigator.userAgent?.includes('Mobile') ? '移动端网页' : '网页端'} · ${platform || '未知平台'}`;
@@ -1121,7 +1174,7 @@ handleDragLeave(e) {
// 保存用户信息到localStorage非敏感信息用于页面刷新后恢复
// 注意token 通过 HttpOnly Cookie 传递,不再存储在 localStorage
localStorage.setItem('user', JSON.stringify(this.user));
this.persistUser();
// 启动token自动刷新在过期前5分钟刷新
const expiresIn = response.data.expiresIn || (2 * 60 * 60 * 1000);
@@ -1152,29 +1205,26 @@ handleDragLeave(e) {
this.loadUserTheme();
// 管理员直接跳转到管理后台
if (this.user.is_admin) {
this.currentView = 'admin';
this.switchView('admin', true);
}
// 普通用户:检查存储权限
else {
// 如果用户可以使用本地存储,直接进入文件页面
if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') {
this.currentView = 'files';
this.loadFiles('/');
this.switchView('files', true);
}
// 如果仅OSS模式需要检查是否配置了OSS包括系统级统一配置
else if (this.storagePermission === 'oss_only') {
if (this.user?.oss_config_source !== 'none') {
this.currentView = 'files';
this.loadFiles('/');
this.switchView('files', true);
} else {
this.currentView = 'settings';
this.switchView('settings', true);
this.showToast('info', '欢迎', '请先配置您的OSS服务');
this.openOssConfigModal();
}
} else {
// 默认行为:跳转到文件页面
this.currentView = 'files';
this.loadFiles('/');
this.switchView('files', true);
}
}
}
@@ -1298,6 +1348,13 @@ handleDragLeave(e) {
async handleRegister() {
this.errorMessage = '';
this.successMessage = '';
const passwordCheck = this.validateAccountPassword(this.registerForm.password);
if (!passwordCheck.valid) {
this.errorMessage = passwordCheck.message;
return;
}
this.registerLoading = true;
try {
@@ -1350,7 +1407,7 @@ handleDragLeave(e) {
this.showToast('error', '配置错误', 'Access Key ID 不能为空');
return;
}
if (!this.ossConfigForm.oss_access_key_secret || this.ossConfigForm.oss_access_key_secret.trim() === '') {
if (!this.user?.has_oss_config && (!this.ossConfigForm.oss_access_key_secret || this.ossConfigForm.oss_access_key_secret.trim() === '')) {
this.showToast('error', '配置错误', 'Access Key Secret 不能为空');
return;
}
@@ -1392,8 +1449,7 @@ handleDragLeave(e) {
this.showOssConfigModal = false;
// 刷新到文件页面
this.currentView = 'files';
this.loadFiles('/');
this.switchView('files', true);
// 显示成功提示
this.showToast('success', '配置成功', 'OSS存储配置已保存');
@@ -1470,7 +1526,7 @@ handleDragLeave(e) {
// 更新用户信息(后端已通过 Cookie 更新 token
if (response.data.user) {
this.user = response.data.user;
localStorage.setItem('user', JSON.stringify(response.data.user));
this.persistUser(response.data.user);
}
// 延迟后重新登录
@@ -1487,8 +1543,9 @@ handleDragLeave(e) {
return;
}
if (this.changePasswordForm.new_password.length < 6) {
this.showToast('warning', '提示', '新密码至少6个字符');
const passwordCheck = this.validateAccountPassword(this.changePasswordForm.new_password);
if (!passwordCheck.valid) {
this.showToast('warning', '提示', passwordCheck.message);
return;
}
@@ -1554,7 +1611,7 @@ handleDragLeave(e) {
this.showToast('success', '成功', '用户名修改成功!');
// 更新本地用户信息
this.user.username = this.usernameForm.newUsername;
localStorage.setItem('user', JSON.stringify(this.user));
this.persistUser();
this.usernameForm.newUsername = '';
}
} catch (error) {
@@ -1576,7 +1633,7 @@ handleDragLeave(e) {
// 更新本地用户信息
if (response.data.user) {
this.user = response.data.user;
localStorage.setItem('user', JSON.stringify(this.user));
this.persistUser();
}
}
} catch (error) {
@@ -1637,7 +1694,7 @@ handleDragLeave(e) {
this.isLoggedIn = true;
// 更新localStorage中的用户信息非敏感信息
localStorage.setItem('user', JSON.stringify(this.user));
this.persistUser();
// 从最新的用户信息初始化存储相关字段
this.storagePermission = this.user.storage_permission || 'oss_only';
@@ -1774,16 +1831,22 @@ handleDragLeave(e) {
// ===== 文件管理 =====
async loadFiles(path) {
const requestId = ++this.fileLoadRequestId;
const targetPath = path || '/';
this.loading = true;
// 确保路径不为undefined
this.currentPath = path || '/';
this.currentPath = targetPath;
this.globalSearchVisible = false;
try {
const response = await axios.get(`${this.apiBase}/api/files`, {
params: { path }
params: { path: targetPath }
});
if (requestId !== this.fileLoadRequestId || this.currentPath !== targetPath) {
return;
}
if (response.data.success) {
this.files = response.data.items;
this.thumbnailLoadErrors = {};
@@ -1805,6 +1868,9 @@ handleDragLeave(e) {
}
}
} catch (error) {
if (requestId !== this.fileLoadRequestId) {
return;
}
console.error('加载文件失败:', error);
this.showToast('error', '加载失败', error.response?.data?.message || error.message);
@@ -1812,7 +1878,9 @@ handleDragLeave(e) {
this.logout();
}
} finally {
this.loading = false;
if (requestId === this.fileLoadRequestId) {
this.loading = false;
}
}
},
@@ -2246,11 +2314,17 @@ handleDragLeave(e) {
// 长按取消(移动端)
handleLongPressEnd() {
const wasTriggered = this.longPressTriggered;
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
this.longPressFile = null;
if (wasTriggered) {
setTimeout(() => {
this.longPressTriggered = false;
}, 350);
}
},
// 从菜单执行操作
@@ -2381,14 +2455,7 @@ handleDragLeave(e) {
}
}
// 本地存储模式:返回同步的下载 URL
// OSS 模式下缩略图功能暂不支持(需要预签名 URL建议点击文件预览
if (this.storageType !== 'oss') {
const filePath = this.getCurrentFilePath(file);
return `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`;
}
// OSS 模式暂不支持同步缩略图,返回 null
// 不用下载接口做缩略图,避免浏览网格时消耗下载流量配额。
return null;
},
@@ -2482,6 +2549,8 @@ handleDragLeave(e) {
const link = document.createElement('a');
link.href = this.currentMediaUrl;
link.setAttribute('download', this.currentMediaName);
link.target = '_blank';
link.rel = 'noopener noreferrer';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
@@ -2521,7 +2590,7 @@ handleDragLeave(e) {
openShareFileModal(file) {
this.shareFileForm.fileName = file.name;
this.shareFileForm.filePath = this.currentPath === '/'
? file.name
? `/${file.name}`
: `${this.currentPath}/${file.name}`;
this.shareFileForm.isDirectory = file.isDirectory; // 设置是否为文件夹
this.shareFileForm.enablePassword = false;
@@ -2746,23 +2815,26 @@ handleDragLeave(e) {
// ===== 文件上传 =====
handleFileSelect(event) {
async handleFileSelect(event) {
const files = event.target.files;
if (files && files.length > 0) {
// 支持多文件上传
Array.from(files).forEach(file => {
this.uploadFile(file);
});
await this.uploadFiles(files);
// 清空input允许重复上传相同文件
event.target.value = '';
}
},
handleFileDrop(event) {
async handleFileDrop(event) {
this.isDragging = false;
const file = event.dataTransfer.files[0];
if (file) {
this.uploadFile(file);
await this.uploadFiles(event.dataTransfer.files);
},
async uploadFiles(fileList) {
const files = Array.from(fileList || []).filter(Boolean);
if (files.length === 0) return;
for (const file of files) {
await this.uploadFile(file);
}
},
@@ -2784,6 +2856,7 @@ handleDragLeave(e) {
this.uploadProgress = 0;
this.uploadedBytes = 0;
this.totalBytes = file.size;
this.uploadPhase = '准备上传';
try {
const fileHash = await this.computeQuickFileHash(file);
@@ -2793,6 +2866,7 @@ handleDragLeave(e) {
this.uploadedBytes = 0;
this.totalBytes = 0;
this.uploadingFileName = '';
this.uploadPhase = '';
await this.loadFiles(this.currentPath);
await this.refreshStorageUsage();
return;
@@ -2816,6 +2890,7 @@ handleDragLeave(e) {
this.uploadedBytes = 0;
this.totalBytes = 0;
this.uploadingFileName = '';
this.uploadPhase = '';
this.showToast('error', '上传失败', error.message || '上传失败,请重试');
}
@@ -2934,6 +3009,7 @@ handleDragLeave(e) {
'Content-Type': file.type || 'application/octet-stream'
},
onUploadProgress: (progressEvent) => {
this.uploadPhase = '上传中';
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
this.uploadedBytes = progressEvent.loaded;
this.totalBytes = progressEvent.total;
@@ -2942,6 +3018,7 @@ handleDragLeave(e) {
});
// 3. 通知后端上传完成
this.uploadPhase = '服务端确认中';
await axios.post(`${this.apiBase}/api/files/upload-complete`, {
objectKey: signData.objectKey,
size: file.size,
@@ -2957,6 +3034,7 @@ handleDragLeave(e) {
this.uploadedBytes = 0;
this.totalBytes = 0;
this.uploadingFileName = '';
this.uploadPhase = '';
// 6. 刷新文件列表和空间统计
await this.loadFiles(this.currentPath);
@@ -3006,6 +3084,7 @@ handleDragLeave(e) {
if (uploadedBytes > 0 && file.size > 0) {
this.uploadedBytes = uploadedBytes;
this.uploadProgress = Math.min(100, Math.round((uploadedBytes / file.size) * 100));
this.uploadPhase = '上传中';
}
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) {
@@ -3037,8 +3116,10 @@ handleDragLeave(e) {
this.uploadProgress = file.size > 0
? Math.min(100, Math.round((uploadedBytes / file.size) * 100))
: 0;
this.uploadPhase = this.uploadProgress >= 100 ? '服务端合并中' : '上传中';
}
this.uploadPhase = '服务端合并中';
const completeResp = await axios.post(`${this.apiBase}/api/upload/resumable/complete`, {
session_id: sessionId
});
@@ -3052,6 +3133,7 @@ handleDragLeave(e) {
this.uploadedBytes = 0;
this.totalBytes = 0;
this.uploadingFileName = '';
this.uploadPhase = '';
await this.loadFiles(this.currentPath);
await this.refreshStorageUsage();
return true;
@@ -3078,6 +3160,7 @@ handleDragLeave(e) {
this.uploadedBytes = 0;
this.totalBytes = 0;
this.uploadingFileName = '';
this.uploadPhase = '';
return true;
}
@@ -3092,6 +3175,7 @@ handleDragLeave(e) {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 30 * 60 * 1000,
onUploadProgress: (progressEvent) => {
this.uploadPhase = '上传中';
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
this.uploadedBytes = progressEvent.loaded;
this.totalBytes = progressEvent.total;
@@ -3104,8 +3188,16 @@ handleDragLeave(e) {
this.uploadedBytes = 0;
this.totalBytes = 0;
this.uploadingFileName = '';
this.uploadPhase = '';
await this.loadFiles(this.currentPath);
await this.refreshStorageUsage();
} else {
this.uploadProgress = 0;
this.uploadedBytes = 0;
this.totalBytes = 0;
this.uploadingFileName = '';
this.uploadPhase = '';
throw new Error(response.data.message || '上传失败');
}
return true;
},
@@ -3702,8 +3794,14 @@ handleDragLeave(e) {
},
async submitResetPassword() {
if (!this.resetPasswordForm.token || !this.resetPasswordForm.new_password || this.resetPasswordForm.new_password.length < 6) {
this.showToast('error', '错误', '请输入有效的重置链接和新密码至少6位');
if (!this.resetPasswordForm.token || !this.resetPasswordForm.new_password) {
this.showToast('error', '错误', '请输入有效的重置链接和新密码');
return;
}
const passwordCheck = this.validateAccountPassword(this.resetPasswordForm.new_password);
if (!passwordCheck.valid) {
this.showToast('error', '错误', passwordCheck.message);
return;
}
this.passwordResetting = true;
@@ -3735,25 +3833,36 @@ handleDragLeave(e) {
},
async loadUserFiles(path) {
const requestId = ++this.inspectionLoadRequestId;
const targetPath = path || '/';
this.inspectionLoading = true;
this.inspectionPath = path;
this.inspectionPath = targetPath;
try {
const response = await axios.get(
`${this.apiBase}/api/admin/users/${this.inspectionUser.id}/files`,
{
params: { path }
params: { path: targetPath }
}
);
if (requestId !== this.inspectionLoadRequestId || this.inspectionPath !== targetPath) {
return;
}
if (response.data.success) {
this.inspectionFiles = response.data.items;
}
} catch (error) {
if (requestId !== this.inspectionLoadRequestId) {
return;
}
console.error('加载用户文件失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '加载文件失败');
} finally {
this.inspectionLoading = false;
if (requestId === this.inspectionLoadRequestId) {
this.inspectionLoading = false;
}
}
},
@@ -4023,16 +4132,13 @@ handleDragLeave(e) {
},
openOssConfigModal() {
// 只有管理员才能配置OSS
if (!this.user?.is_admin) {
this.showToast('error', '权限不足', '只有管理员才能配置OSS服务');
if (!this.user) {
this.showToast('error', '请先登录', '登录后才能配置 OSS');
return;
}
this.showOssGuideModal = false;
this.showOssConfigModal = true;
if (this.user && !this.user.is_admin) {
this.loadOssConfig();
}
this.loadOssConfig();
},
closeOssConfigModal() {
@@ -4075,6 +4181,7 @@ handleDragLeave(e) {
// 切换到管理后台时,重新加载用户列表、健康检测和系统日志
if (this.user && this.user.is_admin) {
this.loadUsers();
this.loadSystemSettings();
this.loadServerStorageStats();
if (this.adminTab === 'monitor') {
this.initMonitorTab();
@@ -4087,6 +4194,7 @@ handleDragLeave(e) {
case 'settings':
this.loadOnlineDevices();
if (this.user && !this.user.is_admin) {
this.loadOssConfig();
this.loadDownloadTrafficReport();
}
break;
@@ -4360,11 +4468,25 @@ handleDragLeave(e) {
},
getHighlightedText(value, keyword) {
const text = this.escapeHtml(value || '-');
const rawText = String(value || '-');
const search = String(keyword || '').trim();
if (!search) return text;
const reg = new RegExp(this.escapeRegExp(search), 'ig');
return text.replace(reg, (match) => `<mark class="admin-search-hit">${match}</mark>`);
if (!search) return this.escapeHtml(rawText);
const lowerText = rawText.toLowerCase();
const lowerSearch = search.toLowerCase();
let cursor = 0;
let output = '';
let index = lowerText.indexOf(lowerSearch, cursor);
while (index !== -1) {
output += this.escapeHtml(rawText.slice(cursor, index));
output += `<mark class="admin-search-hit">${this.escapeHtml(rawText.slice(index, index + search.length))}</mark>`;
cursor = index + search.length;
index = lowerText.indexOf(lowerSearch, cursor);
}
output += this.escapeHtml(rawText.slice(cursor));
return output;
},
formatBytes(bytes) {
@@ -4566,9 +4688,14 @@ handleDragLeave(e) {
async updateSystemSettings() {
try {
const maxUploadSize = parseInt(this.systemSettings.maxUploadSizeMB) * 1024 * 1024;
const currentPassword = window.prompt('请输入当前管理员密码以确认修改系统设置');
if (currentPassword === null) {
return;
}
const payload = {
max_upload_size: maxUploadSize,
current_password: currentPassword,
download_security: {
enabled: !!this.systemSettings.downloadSecurity.enabled,
same_ip_same_file: {
@@ -4610,7 +4737,7 @@ handleDragLeave(e) {
}
} catch (error) {
console.error('更新系统设置失败:', error);
this.showToast('error', '错误', '更新系统设置失败');
this.showToast('error', '错误', error.response?.data?.message || '更新系统设置失败');
}
},
@@ -4966,7 +5093,7 @@ handleDragLeave(e) {
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('csrf_token='))
?.split('=')[1];
?.substring('csrf_token='.length);
if (csrfToken && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(config.method?.toUpperCase())) {
config.headers['X-CSRF-Token'] = csrfToken;
@@ -5017,12 +5144,18 @@ handleDragLeave(e) {
// 设置axios响应拦截器处理401错误token过期/失效)
axios.interceptors.response.use(
response => response,
error => {
async error => {
if (error.response && error.response.status === 401) {
// 排除登录接口本身的401密码错误等
const isLoginApi = error.config?.url?.includes('/api/login');
if (!isLoginApi && this.isLoggedIn) {
console.warn('[认证] 收到401响应Token已失效');
const isRefreshApi = error.config?.url?.includes('/api/refresh');
if (!isLoginApi && !isRefreshApi && this.isLoggedIn && !error.config?._retry) {
console.warn('[认证] 收到401响应尝试刷新Token');
error.config._retry = true;
const refreshed = await this.doRefreshToken();
if (refreshed) {
return axios(error.config);
}
this.handleTokenExpired();
this.showToast('warning', '登录已过期', '请重新登录');
}
@@ -5049,21 +5182,6 @@ handleDragLeave(e) {
watch: {
currentView(newView) {
if (newView === 'shares') {
this.refreshShareResources();
} else if (newView === 'admin' && this.user?.is_admin) {
this.loadUsers();
this.loadSystemSettings();
this.loadServerStorageStats();
} else if (newView === 'settings' && this.user && !this.user.is_admin) {
// 普通用户进入设置页面时加载OSS配置
this.loadOssConfig();
this.loadDownloadTrafficReport();
this.loadOnlineDevices();
} else if (newView === 'settings') {
this.loadOnlineDevices();
}
// 记住最后停留的视图(需合法且已登录)
if (this.isLoggedIn && this.isViewAllowed(newView)) {
localStorage.setItem('lastView', newView);