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
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:
1 parent
3e744b63f1
commit
1fa2f11057
10 files changed
+362
-14
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(/&/gi, '&').replace(/"/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) : ''; })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user