feat:日志敏感信息过滤
build / task (16.x, npm, macOS-latest) (push) Canceled after 0s
build / task (16.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, npm, windows-latest) (push) Canceled after 0s
build / task (16.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (16.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (16.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (16.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, yarn, windows-latest) (push) Canceled after 0s
build / task (18.x, npm, macOS-latest) (push) Canceled after 0s
build / task (18.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, npm, windows-latest) (push) Canceled after 0s
build / task (18.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (18.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (18.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (18.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, yarn, windows-latest) (push) Canceled after 0s
build / task (20.x, npm, macOS-latest) (push) Canceled after 0s
build / task (20.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, npm, windows-latest) (push) Canceled after 0s
build / task (20.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (20.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (20.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (20.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, yarn, windows-latest) (push) Canceled after 0s

This commit is contained in:
dzc committed 2026-10-29 15:46:24 +08:00
1 parent 3e744b63f1
commit 1fa2f11057
10 files changed
+362 -14

No files matched your search

+12
View File
@@ -22,6 +22,7 @@ import { stripFileHtml, resolveFileUrl } from '/@/utils/file-access';
import { bindFileAccessDom } from '/@/utils/file-access-dom';
import { Session } from '/@/utils/storage';
import other from '/@/utils/other';
import request from '/@/utils/request';
const { proxy } = getCurrentInstance();
// 定义父组件传过来的值
@@ -75,6 +76,17 @@ const headers = computed(() => {
// 定义上传需要的字段信息
const uploadAttr = reactive({
async customUpload(file: File, insertFn: (url: string) => void) {
const data = new FormData();
data.append('file', file);
try {
const response = await request<any, { code: number; data?: { url: string }; msg?: string }>({ url: props.uploadFileUrl, method: 'post', data });
if (response.code !== 0 || !response.data?.url) throw new Error(response.msg || '文件上传失败');
insertFn(resolveFileUrl(response.data.url));
} catch (error: any) {
throw new Error(error?.msg || error?.message || '文件上传失败');
}
},
fieldName: 'file',
server: proxy.baseURL + props.uploadFileUrl,
headers: headers,
+21 -5
View File
@@ -2,6 +2,7 @@
<template>
<div class="w-full upload-file">
<el-upload ref="fileUpload" v-if="props.type === 'default'"
:http-request="handleHttpUpload"
:action="baseURL + other.adaptationUrl(props.uploadFileUrl)" :before-upload="handleBeforeUpload"
:file-list="fileList" :headers="headers" :limit="limit" :on-error="handleUploadError"
:on-remove="handleRemove" :on-preview="handlePreview" :on-exceed="handleExceed" :data="formData"
@@ -24,6 +25,7 @@
</template>
</el-upload>
<el-upload ref="fileUpload" v-if="props.type === 'simple'"
:http-request="handleHttpUpload"
:action="baseURL + other.adaptationUrl(props.uploadFileUrl)" :before-upload="handleBeforeUpload"
:file-list="fileList" :headers="headers" :limit="limit" :auto-upload="autoUpload"
:on-error="handleUploadError" :on-remove="handleRemove" :on-exceed="handleExceed" :data="formData"
@@ -38,6 +40,8 @@ import { useMessage } from '/@/hooks/message';
import { Session } from '/@/utils/storage';
import other from '/@/utils/other';
import { useI18n } from 'vue-i18n';
import request from '/@/utils/request';
import type { UploadRequestOptions } from 'element-plus';
const props = defineProps({
modelValue: [String, Array],
@@ -53,7 +57,7 @@ const props = defineProps({
},
fileType: {
type: Array,
default: () => ['png', 'jpg', 'jpeg', 'doc', 'xls', 'ppt', 'txt', 'pdf', 'docx', 'xlsx', 'pptx', 'apk'],
default: () => ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'svg', 'pdf', 'apk', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'zip', 'mp4', 'mov', 'webm', 'avi', 'mp3', 'wav', 'm4a', 'aac', 'ogg', 'amr', 'flac'],
},
// 是否显示提示
isShowTip: {
@@ -93,6 +97,16 @@ const uploadList = ref<{ name: string; url: string; fileUrl: string; fileSize: n
const fileUpload = ref();
const { t } = useI18n();
// Axios applies the gateway prefix, architecture adaptation and fresh signature once.
const handleHttpUpload = (options: UploadRequestOptions) => {
const data = new FormData();
for (const [name, value] of Object.entries(options.data || {})) {
if (value != null) data.append(name, value instanceof Blob ? value : String(value));
}
data.append(options.filename || 'file', options.file);
return request({ url: props.uploadFileUrl, method: 'post', data, timeout: 0 });
};
// 请求头处理
const headers = computed(() => {
return {
@@ -111,8 +125,8 @@ const handleBeforeUpload = (file: File) => {
// 校检文件类型
if (props.fileType.length) {
const fileName = file.name.split('.');
const fileExt = fileName[fileName.length - 1];
const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
const fileExt = fileName[fileName.length - 1].toLowerCase();
const isTypeOk = props.fileType.some(value => String(value).toLowerCase() === fileExt);
if (!isTypeOk) {
useMessage().error(`${t('excel.typeErrorText')} ${props.fileType.join('/')}!`);
return false;
@@ -193,8 +207,10 @@ const listToString = (list: { url: string }[], separator = ','): string => {
return strs !== '' ? strs.substr(0, strs.length - 1) : '';
};
const handleUploadError = () => {
useMessage().error('上传文件失败');
const handleUploadError = (error: any) => {
number.value = Math.max(0, number.value - 1);
useMessage().error(error?.msg || error?.message || '上传文件失败');
uploadedSuccessfully();
};
/**
+33 -1
View File
@@ -21,10 +21,19 @@ export function fileAccessSignature(file: string, app: string, kid: string, ts:
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(key) || CryptoJS.enc.Base64.parse(key).sigBytes < 32) throw new Error('Invalid file access key')
return CryptoJS.HmacSHA256(fileAccessCanonical(file, app, kid, ts), CryptoJS.enc.Base64.parse(key)).toString(CryptoJS.enc.Hex)
}
export function fileUploadCanonical(target: string, app: string, kid: string, ts: number, nonce: string): string {
if (!['upload', 'inner-upload', 'qn-upload'].includes(target)) throw new Error('Invalid file upload target')
return ['v1', 'oss-file-upload', 'POST', target, app, kid, String(ts), nonce].join('\n')
}
export function fileUploadSignature(target: string, app: string, kid: string, ts: number, nonce: string, key: string): string {
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(key) || CryptoJS.enc.Base64.parse(key).sigBytes < 32) throw new Error('Invalid file upload key')
return CryptoJS.HmacSHA256(fileUploadCanonical(target, app, kid, ts, nonce), CryptoJS.enc.Base64.parse(key)).toString(CryptoJS.enc.Hex)
}
export function createFileAccess(options: FileAccessOptions) {
const now = options.now || Date.now
let offset = 0, ttl = 1800
const cache = new Map<string, { url: string; ts: number }>()
let uploadSequence = 0
const base = () => (options.baseUrl() || '').replace(/\/+$/, '')
const allowed = () => [origin(base()), ...(options.allowedOrigins || []).map(origin)].filter(Boolean)
function resolve(source?: string | null): string {
@@ -88,6 +97,29 @@ export function createFileAccess(options: FileAccessOptions) {
const query = head.slice(at + 1).split('&').filter(p => !FILE_ACCESS_FIELDS.includes(decode(p.split('=')[0])))
return head.slice(0, at) + (query.length ? '?' + query.join('&') : '') + hash
}
function uploadTarget(source: string): string | null {
const resolved = resolve(source), host = origin(resolved)
if ((host && !allowed().includes(host)) || (/^[a-z][a-z0-9+.-]*:/i.test(resolved) && !host)) return null
const path = (host ? resolved.replace(/^https?:\/\/[^/?#]+/i, '') : resolved).split(/[?#]/)[0]
const prefix = base().replace(/^https?:\/\/[^/?#]+/i, '')
for (const [target, suffix] of [['upload', 'upload'], ['inner-upload', 'inner-upload'], ['qn-upload', 'qn/upload']]) {
if (['/sys-file/', '/admin/sys-file/', '/api/sys-file/', '/api/admin/sys-file/', prefix + '/admin/sys-file/', prefix + '/sys-file/']
.some(start => path === start + suffix)) return target
}
return null
}
function uploadHeaders(source: string, fixedNonce?: string): Record<string, string> {
const target = uploadTarget(source)
if (!target) return {}
const seconds = Math.floor((now() + offset) / 1000)
// The nonce is a unique request identifier, not a secret; HMAC provides authorization.
const nonce = fixedNonce || Math.floor(now()).toString(36) + '-' + (++uploadSequence).toString(36) + '-'
+ Array.from({ length: 4 }, () => Math.floor(Math.random() * 0x100000000).toString(16).padStart(8, '0')).join('')
return {
'X-File-App-Id': options.appId, 'X-File-Key-Id': options.kid, 'X-File-Timestamp': String(seconds), 'X-File-Nonce': nonce,
'X-File-Signature': fileUploadSignature(target, options.appId, options.kid, seconds, nonce, options.key),
}
}
function synchronize(serverTime: number, ttlSeconds = ttl, roundTripMs = 0) {
if (!Number.isFinite(serverTime) || serverTime <= 0) return
offset = serverTime + roundTripMs / 2 - now()
@@ -99,7 +131,7 @@ export function createFileAccess(options: FileAccessOptions) {
const parsed = parse(source)
if (parsed) cache.delete(parsed.unsigned)
}
return { url, resolve, strip, protected: (s?: string | null) => !!parse(s), synchronize, invalidate, clear: () => cache.clear(), ttl: () => ttl }
return { uploadHeaders, uploadTarget, url, resolve, strip, protected: (s?: string | null) => !!parse(s), synchronize, invalidate, clear: () => cache.clear(), ttl: () => ttl }
}
const unescapeAttribute = (s: string) => s.replace(/&amp;/gi, '&').replace(/&quot;/gi, '"').replace(/&#(?:39|x27);/gi, "'").replace(/&#(x[0-9a-f]+|\d+);/gi, (_, n) => { const code = n[0].toLowerCase() === 'x' ? parseInt(n.slice(1), 16) : Number(n); return code <= 0x10ffff ? String.fromCodePoint(code) : ''; })
+1
View File
@@ -23,6 +23,7 @@ export const accessFileUrl = (source?: string | null) => isProtectedFile(source)
export const fileUrls = (sources: any) => Array.isArray(sources) ? sources.map(accessFileUrl) : []
export const originalFileUrl = (source?: string | null) => client.strip(source)
export const resolveFileUrl = (source?: string | null) => client.resolve(source)
export const fileUploadHeaders = (source: string) => client.uploadHeaders(source)
export const fileHtml = (html: string) => rewriteFileHtml(html, accessFileUrl)
export const stripFileHtml = (html: string) => rewriteFileHtml(html, originalFileUrl)
export const fileNodes = (nodes: any) => rewriteFileNodes(nodes, accessFileUrl)
+2
View File
@@ -3,6 +3,7 @@ import { Session } from "/@/utils/storage"
import { useMessageBox } from "/@/hooks/message"
import qs from "qs"
import other from "./other"
import { fileUploadHeaders } from './file-access'
/**
* 创建并配置一个 Axios 实例对象
@@ -48,6 +49,7 @@ service.interceptors.request.use(
// 自动适配单体和微服务架构不同的URL
config.url = other.adaptationUrl(config.url)
if (config.method?.toLowerCase() === 'post') Object.assign(config.headers, fileUploadHeaders(axios.getUri(config)))
// 处理完毕,返回config对象
return config
+34 -2
View File
@@ -29,6 +29,7 @@
v-model="state.queryForm.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
/>
<div class="log-export-time-tip">导出需选择完整时间范围,最多支持一个自然月</div>
</el-form-item>
</SearchPanel>
</el-row>
@@ -122,6 +123,10 @@ import { useBatchExport } from "/@/hooks/batchExport"
import { useI18n } from "vue-i18n"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import dayjs from "dayjs"
import customParseFormat from "dayjs/plugin/customParseFormat"
dayjs.extend(customParseFormat)
const LogDetail = defineAsyncComponent(() => import("./detail.vue"))
const LogLineChart = defineAsyncComponent(() => import("./line-chart.vue"))
@@ -178,15 +183,35 @@ const resetQuery = () => {
}
// 导出excel
const { exportProgressRef, exportTaskConfig, submitExport, handleExportSuccess, handleExportFailure } = useBatchExport({
const { exportProgressRef, exportTaskConfig, exportSubmitting, submitExport, handleExportSuccess, handleExportFailure } = useBatchExport({
progressUrl: "/admin/async-task/progress", redisKey: "admin:export:log",
taskName: "操作日志", fileName: "操作日志.xlsx", submitTask: submitLogExport,
})
const exportExcel = () => {
if (exportSubmitting.value) {
exportProgressRef.value?.show()
return
}
const range = state.queryForm.createTime
let error = ""
if (!Array.isArray(range) || range.length !== 2 || !range[0] || !range[1]) {
error = "导出前请选择完整的日志创建时间范围"
} else {
const start = dayjs(range[0], "YYYY-MM-DD HH:mm:ss", true)
const end = dayjs(range[1], "YYYY-MM-DD HH:mm:ss", true)
if (!start.isValid() || !end.isValid()) error = "请选择有效的日志创建时间范围"
else if (start.isAfter(end)) error = "日志创建时间的开始时间不能晚于结束时间"
else if (end.isAfter(start.add(1, "month"))) error = "日志导出时间范围不能超过一个月"
}
if (error) {
showSearch.value = true
useMessage().error(error)
return
}
return submitExport({
...state.queryForm,
createTime: Array.isArray(state.queryForm.createTime) ? state.queryForm.createTime : [],
createTime: range,
})
}
@@ -225,6 +250,13 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
.log-export-time-tip {
width: 100%;
font-size: 12px;
line-height: 20px;
color: var(--el-text-color-secondary);
}
pre code.hljs {
width: 65%;
}