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

@@ -1040,6 +1040,13 @@
<span v-else> | 有效期: <strong class="share-expire-time valid">永久有效</strong></span>
</p>
<div v-if="shareInfo.share_type !== 'file'" class="share-meta-bar" style="justify-content: space-between;">
<span>当前位置:/{{ currentPath || '' }}</span>
<button v-if="currentPath" class="btn btn-secondary" style="width: auto; padding: 8px 14px;" @click="goParentDirectory">
<i class="fas fa-arrow-left"></i> 返回上级
</button>
</div>
<!-- 视图切换按钮 (多文件时才显示) -->
<div v-if="files.length > 1" class="view-controls">
<button class="btn" :class="viewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="viewMode = 'grid'">
@@ -1061,6 +1068,9 @@
<i class="single-file-icon fas" :class="getFileIcon(viewingFile || files[0])" :style="getIconColor(viewingFile || files[0])"></i>
<div class="single-file-name">{{ (viewingFile || files[0]).name }}</div>
<div class="single-file-size">{{ (viewingFile || files[0]).sizeFormatted }}</div>
<button v-if="(viewingFile || files[0]).isDirectory" class="btn single-file-download" @click="enterDirectory(viewingFile || files[0])">
<i class="fas fa-folder-open"></i> 进入文件夹
</button>
<button v-if="!(viewingFile || files[0]).isDirectory" class="btn single-file-download" @click="downloadFile(viewingFile || files[0])">
<i class="fas fa-download"></i> 下载文件
</button>
@@ -1083,7 +1093,7 @@
<!-- 列表视图 -->
<ul v-else-if="!viewingFile" class="file-list">
<li v-for="file in files" :key="file.name" class="file-item">
<li v-for="file in files" :key="file.name" class="file-item" @click="handleFileClick(file)" style="cursor: pointer;">
<div class="file-info">
<i class="file-icon fas" :class="getFileIcon(file)" :style="getIconColor(file)"></i>
<div class="file-name-container">
@@ -1126,6 +1136,7 @@
shareNotFound: false,
shareInfo: null,
files: [],
currentPath: '',
loading: true,
errorMessage: '',
downloadAlertMessage: '',
@@ -1196,6 +1207,8 @@
async verifyShare() {
this.errorMessage = '';
this.downloadAlertMessage = '';
this.currentPath = '';
this.viewingFile = null;
this.loading = true;
try {
@@ -1210,40 +1223,45 @@
// 如果是单文件分享且后端已返回文件信息,直接使用,无需再次请求
if (response.data.file) {
this.files = [response.data.file];
this.loading = false;
} else {
// 目录分享,需要加载文件列表
await this.loadFiles();
}
} else {
this.errorMessage = response.data.message || '验证失败';
}
} catch (error) {
// 404错误 - 分享不存在
if (error.response?.status === 404) {
this.shareNotFound = true;
this.loading = false;
}
// 需要密码
else if (error.response?.data?.needPassword) {
this.needPassword = true;
this.loading = false;
}
// 其他错误
else {
this.errorMessage = error.response?.data?.message || '验证失败';
this.loading = false;
}
} finally {
this.loading = false;
}
},
async loadFiles() {
async loadFiles(path = this.currentPath || '') {
this.loading = true;
this.currentPath = String(path || '').replace(/^\/+|\/+$/g, '');
this.viewingFile = null;
try {
const response = await axios.post(`${this.apiBase}/api/share/${this.shareCode}/list`, {
password: this.password,
path: ''
path: this.currentPath
});
if (response.data.success) {
this.files = response.data.items;
} else {
this.errorMessage = response.data.message || '加载文件失败';
}
} catch (error) {
console.error('加载文件失败:', error);
@@ -1255,10 +1273,32 @@
// 处理文件点击 - 显示文件详情页面
handleFileClick(file) {
// 所有文件类型都显示详情页面(分享页面不提供媒体预览)
if (file?.isDirectory) {
this.enterDirectory(file);
return;
}
this.viewFileDetail(file);
},
joinSharePath(basePath, name) {
return [basePath, name]
.map(part => String(part || '').replace(/^\/+|\/+$/g, ''))
.filter(Boolean)
.join('/');
},
enterDirectory(file) {
if (!file?.isDirectory) return;
this.loadFiles(this.joinSharePath(this.currentPath, file.name));
},
goParentDirectory() {
if (!this.currentPath) return;
const parts = this.currentPath.split('/').filter(Boolean);
parts.pop();
this.loadFiles(parts.join('/'));
},
// 查看文件详情(放大显示)
viewFileDetail(file) {
this.viewingFile = file;
@@ -1279,8 +1319,9 @@
filePath = this.shareInfo.share_path;
} else {
// 目录分享,组合路径
const basePath = this.shareInfo.share_path;
filePath = basePath === '/' ? `/${file.name}` : `${basePath}/${file.name}`;
const basePath = String(this.shareInfo.share_path || '/').replace(/\/+$/g, '') || '/';
const relativePath = this.joinSharePath(this.currentPath, file.name);
filePath = basePath === '/' ? `/${relativePath}` : `${basePath}/${relativePath}`;
}
try {
@@ -1295,11 +1336,10 @@
// OSS 直连下载:新窗口打开
console.log("[分享下载] OSS 直连下载");
// 仅直连下载需要单独记录下载次数(本地代理下载在后端接口内已计数)
axios.post(`${this.apiBase}/api/share/${this.shareCode}/download`)
.catch(err => console.error('记录下载次数失败:', err));
window.open(data.downloadUrl, '_blank');
const downloadWindow = window.open(data.downloadUrl, '_blank', 'noopener,noreferrer');
if (downloadWindow) {
downloadWindow.opener = null;
}
} else {
// 本地存储:通过后端下载
console.log("[分享下载] 后端代理下载");
@@ -1474,7 +1514,7 @@
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;