feat(security): 服务器安全日志页面(统计卡片+TOP攻击源+VxeTable列表)
This commit is contained in:
parent
8b86b73424
commit
e069a823b3
@ -1,6 +1,7 @@
|
|||||||
# admin.xpcool.com 变更记录
|
# admin.xpcool.com 变更记录
|
||||||
> 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要
|
> 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要
|
||||||
|
|
||||||
|
2026-08-27 | CHG | 服务器安全日志页(挂在系统监控菜单下,id=96):views/monitor/security/index.vue——顶部 4 统计卡片(SSH爆破/fail2ban封禁/成功登录/总数)+ TOP 攻击源标签区 + 筛选(来源IP/事件类型/时间范围 t-date-range-picker)+ Vben VxeTable 列表(时间/来源IP/来源端口/目标端口/事件类型标签/详情 tooltip,事件标签按类型配色);api/serverSecurity.ts(list/stats 全 POST,无 URL 参数);typecheck 通过
|
||||||
2026-08-27 | CHG | 全量表格重构:views 下全部 11 个 t-table 页面统一改为 Vben Vxe Table(useVbenVxeGrid,#/adapter/vxe-table)+ Vben TableAction 行操作——system/{log(范式),login-log,admin,role}、recruitment/{info,crawler,push}、house/{data,listing,community,presale}、dashboard/workbench(两处本地小表 proxyConfig.enabled=false + gridApi.setGridOptions 写数据)。要点:gridOptions 需注解 VxeTableGridOptions(否则 type:'seq' 推断 string 报错);单元格自定义用 columns[].slots.default + h(TDesign组件);远程分页走 proxyConfig.ajax.query 返回 {items,total};筛选区保留手写 TDesign 组件不动;admin/role/listing/community 行操作 ActionItem 带 auth 权限码、删除 danger 标红;listing 多选批量标记改 vxe checkboxConfig + gridApi.grid.getCheckboxRecords();confirm 弹窗交互(删除/重置密码)保留 DialogPlugin 不变;typecheck 通过
|
2026-08-27 | CHG | 全量表格重构:views 下全部 11 个 t-table 页面统一改为 Vben Vxe Table(useVbenVxeGrid,#/adapter/vxe-table)+ Vben TableAction 行操作——system/{log(范式),login-log,admin,role}、recruitment/{info,crawler,push}、house/{data,listing,community,presale}、dashboard/workbench(两处本地小表 proxyConfig.enabled=false + gridApi.setGridOptions 写数据)。要点:gridOptions 需注解 VxeTableGridOptions(否则 type:'seq' 推断 string 报错);单元格自定义用 columns[].slots.default + h(TDesign组件);远程分页走 proxyConfig.ajax.query 返回 {items,total};筛选区保留手写 TDesign 组件不动;admin/role/listing/community 行操作 ActionItem 带 auth 权限码、删除 danger 标红;listing 多选批量标记改 vxe checkboxConfig + gridApi.grid.getCheckboxRecords();confirm 弹窗交互(删除/重置密码)保留 DialogPlugin 不变;typecheck 通过
|
||||||
|
|
||||||
2026-08-27 | FIX | 恢复丢失的前端代码:recovery 分支经 rebase/改名(dev↔main)折腾后工作树回退,未提交改动全丢;从悬空提交 f02a514(recovery 工作树快照,含 HEAD 缺失的全部文件)整体恢复 apps/web-tdesign/src——含 styles/theme.css 全局主题、dashboard/workbench 工作台页、system 五页卡片化美化、操作日志增强版(VxeTable)、house 预售/楼盘地图、recruitment 页面;另补 log 页卡片美化(wb-page-head+t-card)。已提交保护
|
2026-08-27 | FIX | 恢复丢失的前端代码:recovery 分支经 rebase/改名(dev↔main)折腾后工作树回退,未提交改动全丢;从悬空提交 f02a514(recovery 工作树快照,含 HEAD 缺失的全部文件)整体恢复 apps/web-tdesign/src——含 styles/theme.css 全局主题、dashboard/workbench 工作台页、system 五页卡片化美化、操作日志增强版(VxeTable)、house 预售/楼盘地图、recruitment 页面;另补 log 页卡片美化(wb-page-head+t-card)。已提交保护
|
||||||
|
|||||||
78
apps/web-tdesign/src/api/serverSecurity.ts
Normal file
78
apps/web-tdesign/src/api/serverSecurity.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// 服务器安全日志模块 API 客户端。
|
||||||
|
// 接口规范:全部 POST;URL 不含参数(查询/路径参数均禁止);入参一律走 body。
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
// ---------- 数据类型 ----------
|
||||||
|
export interface SecurityLogItem {
|
||||||
|
id: number;
|
||||||
|
logTime: string;
|
||||||
|
srcIp: string;
|
||||||
|
srcPort: number;
|
||||||
|
destPort: number;
|
||||||
|
eventType: string;
|
||||||
|
eventName: string;
|
||||||
|
detail: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityLogListResp {
|
||||||
|
list: SecurityLogItem[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityTopIp {
|
||||||
|
srcIp: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityByType {
|
||||||
|
eventType: string;
|
||||||
|
eventName: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityLogStatsResp {
|
||||||
|
total: number;
|
||||||
|
failed: number;
|
||||||
|
banned: number;
|
||||||
|
accepted: number;
|
||||||
|
topIps: SecurityTopIp[];
|
||||||
|
byType: SecurityByType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 列表 ----------
|
||||||
|
export interface SecurityLogQuery {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
srcIp?: string;
|
||||||
|
eventType?: string;
|
||||||
|
destPort?: number;
|
||||||
|
dateFrom?: string;
|
||||||
|
dateTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSecurityLogList(data: SecurityLogQuery) {
|
||||||
|
return requestClient.post<SecurityLogListResp>(
|
||||||
|
'/api/service/admin/server-security/log/list',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 统计 ----------
|
||||||
|
export function getSecurityLogStats(data: {
|
||||||
|
dateFrom?: string;
|
||||||
|
dateTo?: string;
|
||||||
|
}) {
|
||||||
|
return requestClient.post<SecurityLogStatsResp>(
|
||||||
|
'/api/service/admin/server-security/log/stats',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 事件类型选项(与后端 SecurityEventName 对应) ----------
|
||||||
|
export const SECURITY_EVENT_OPTIONS = [
|
||||||
|
{ label: 'SSH爆破尝试', value: 'failed_ssh' },
|
||||||
|
{ label: 'SSH成功登录', value: 'accepted_ssh' },
|
||||||
|
{ label: 'fail2ban封禁', value: 'banned' },
|
||||||
|
{ label: 'fail2ban解封', value: 'unbanned' },
|
||||||
|
];
|
||||||
200
apps/web-tdesign/src/views/monitor/security/index.vue
Normal file
200
apps/web-tdesign/src/views/monitor/security/index.vue
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SecurityLogStatsResp } from '#/api/serverSecurity';
|
||||||
|
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
getSecurityLogList,
|
||||||
|
getSecurityLogStats,
|
||||||
|
SECURITY_EVENT_OPTIONS,
|
||||||
|
} from '#/api/serverSecurity';
|
||||||
|
|
||||||
|
// 服务器安全日志:展示宿主机采集上报的 SSH 爆破/登录/fail2ban 封禁记录
|
||||||
|
const query = reactive({
|
||||||
|
srcIp: '',
|
||||||
|
eventType: '',
|
||||||
|
dateRange: [] as string[],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 统计卡片(默认最近24小时)
|
||||||
|
const stats = ref<SecurityLogStatsResp>({
|
||||||
|
total: 0,
|
||||||
|
failed: 0,
|
||||||
|
banned: 0,
|
||||||
|
accepted: 0,
|
||||||
|
topIps: [],
|
||||||
|
byType: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 把日期数组转成后端时间范围(HH:MM:SS 补齐) */
|
||||||
|
function rangeToParam(): { dateFrom?: string; dateTo?: string } {
|
||||||
|
if (
|
||||||
|
query.dateRange.length === 2 &&
|
||||||
|
query.dateRange[0] &&
|
||||||
|
query.dateRange[1]
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
dateFrom: `${query.dateRange[0]} 00:00:00`,
|
||||||
|
dateTo: `${query.dateRange[1]} 23:59:59`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
stats.value = (await getSecurityLogStats(
|
||||||
|
rangeToParam(),
|
||||||
|
)) as SecurityLogStatsResp;
|
||||||
|
} catch {
|
||||||
|
// 统计失败不阻塞列表
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridOptions: VxeTableGridOptions = {
|
||||||
|
columns: [
|
||||||
|
{ type: 'seq', width: 50, title: '#' },
|
||||||
|
{ field: 'logTime', title: '时间', width: 165 },
|
||||||
|
{ field: 'srcIp', title: '来源IP', width: 140 },
|
||||||
|
{ field: 'srcPort', title: '来源端口', width: 100 },
|
||||||
|
{ field: 'destPort', title: '目标端口', width: 100 },
|
||||||
|
{
|
||||||
|
field: 'eventName',
|
||||||
|
title: '事件类型',
|
||||||
|
width: 130,
|
||||||
|
slots: { default: 'event_type' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'detail',
|
||||||
|
title: '日志详情',
|
||||||
|
minWidth: 320,
|
||||||
|
showOverflow: 'tooltip',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pagerConfig: { pageSize: 20 },
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({
|
||||||
|
page,
|
||||||
|
}: {
|
||||||
|
page: { currentPage: number; pageSize: number };
|
||||||
|
}) => {
|
||||||
|
const data = await getSecurityLogList({
|
||||||
|
page: page.currentPage,
|
||||||
|
size: page.pageSize,
|
||||||
|
srcIp: query.srcIp || undefined,
|
||||||
|
eventType: query.eventType || undefined,
|
||||||
|
...rangeToParam(),
|
||||||
|
});
|
||||||
|
return { items: data.list ?? [], total: data.total ?? 0 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||||
|
|
||||||
|
function search() {
|
||||||
|
loadStats();
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
query.srcIp = '';
|
||||||
|
query.eventType = '';
|
||||||
|
query.dateRange = [];
|
||||||
|
loadStats();
|
||||||
|
gridApi.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadStats);
|
||||||
|
|
||||||
|
// 事件类型徽标配色(爆破红/封禁橙/登录绿/解封蓝)
|
||||||
|
const typeColor: Record<string, string> = {
|
||||||
|
failed_ssh: 'danger',
|
||||||
|
banned: 'warning',
|
||||||
|
accepted_ssh: 'success',
|
||||||
|
unbanned: 'primary',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="mb-3 grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||||
|
<t-card :bordered="false" theme="danger">
|
||||||
|
<div class="text-sm text-gray-500">SSH爆破尝试</div>
|
||||||
|
<div class="mt-1 text-2xl font-bold">{{ stats.failed }}</div>
|
||||||
|
</t-card>
|
||||||
|
<t-card :bordered="false" theme="warning">
|
||||||
|
<div class="text-sm text-gray-500">fail2ban封禁</div>
|
||||||
|
<div class="mt-1 text-2xl font-bold">{{ stats.banned }}</div>
|
||||||
|
</t-card>
|
||||||
|
<t-card :bordered="false" theme="success">
|
||||||
|
<div class="text-sm text-gray-500">SSH成功登录</div>
|
||||||
|
<div class="mt-1 text-2xl font-bold">{{ stats.accepted }}</div>
|
||||||
|
</t-card>
|
||||||
|
<t-card :bordered="false">
|
||||||
|
<div class="text-sm text-gray-500">日志总数</div>
|
||||||
|
<div class="mt-1 text-2xl font-bold">{{ stats.total }}</div>
|
||||||
|
</t-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选 -->
|
||||||
|
<div class="mb-3 flex flex-wrap items-center gap-2">
|
||||||
|
<t-input
|
||||||
|
v-model="query.srcIp"
|
||||||
|
clearable
|
||||||
|
placeholder="来源IP"
|
||||||
|
style="width: 160px"
|
||||||
|
@enter="search"
|
||||||
|
/>
|
||||||
|
<t-select
|
||||||
|
v-model="query.eventType"
|
||||||
|
clearable
|
||||||
|
:options="SECURITY_EVENT_OPTIONS"
|
||||||
|
placeholder="事件类型"
|
||||||
|
style="width: 150px"
|
||||||
|
/>
|
||||||
|
<t-date-range-picker
|
||||||
|
v-model="query.dateRange"
|
||||||
|
clearable
|
||||||
|
placeholder="发生时间范围"
|
||||||
|
style="width: 260px"
|
||||||
|
/>
|
||||||
|
<t-button theme="primary" @click="search">查询</t-button>
|
||||||
|
<t-button variant="outline" @click="reset">重置</t-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TOP 攻击源 -->
|
||||||
|
<t-card v-if="stats.topIps.length > 0" :bordered="false" class="mb-3">
|
||||||
|
<template #header>
|
||||||
|
<span class="font-medium">TOP 攻击来源(爆破+封禁)</span>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<div
|
||||||
|
v-for="(ip, idx) in stats.topIps.slice(0, 8)"
|
||||||
|
:key="ip.srcIp"
|
||||||
|
class="flex items-center gap-2 rounded border border-gray-200 px-3 py-1.5 dark:border-gray-600"
|
||||||
|
>
|
||||||
|
<span class="text-gray-400">#{{ idx + 1 }}</span>
|
||||||
|
<span class="font-mono">{{ ip.srcIp }}</span>
|
||||||
|
<t-tag theme="danger" variant="light">{{ ip.count }}次</t-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t-card>
|
||||||
|
|
||||||
|
<!-- 日志列表 -->
|
||||||
|
<Grid>
|
||||||
|
<template #event_type="{ row }">
|
||||||
|
<t-tag :theme="typeColor[row.eventType] || 'default'" variant="light">
|
||||||
|
{{ row.eventName }}
|
||||||
|
</t-tag>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
Loading…
Reference in New Issue
Block a user