From c4bba32b20f0869b45ed14be04543869dd91ee6c Mon Sep 17 00:00:00 2001
From: cl <1442464845@qq.com>
Date: 星期四, 09 四月 2026 18:38:44 +0800
Subject: [PATCH] 日志1
---
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/support/HttpAuditSupport.java | 27 +
rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleEdit.jsx | 119 ++++
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleServiceImpl.java | 256 +++++++++
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleService.java | 25
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/CloudWmsErpFeignClient.java | 3
rsf-admin/src/page/system/httpAuditRule/index.jsx | 11
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java | 9
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/mapper/HttpAuditRuleMapper.java | 9
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/model/HttpAuditDecision.java | 23
rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java | 1
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditRestTemplateBeanPostProcessor.java | 28 +
rsf-admin/src/i18n/zh.js | 32 +
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HttpAuditRuleController.java | 155 +++++
rsf-admin/src/i18n/en.js | 33 +
rsf-admin/src/page/ResourceContent.js | 3
rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleList.jsx | 121 ++++
rsf-server/src/main/resources/application-dev.yml | 8
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditLog.java | 5
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/props/HttpAuditProperties.java | 28 +
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/FeignHttpAuditConfiguration.java | 20
rsf-admin/src/page/system/httpAuditLog/HttpAuditLogList.jsx | 9
rsf-open-api/src/main/resources/application-dev.yml | 4
rsf-admin/src/page/system/httpAuditLog/HttpAuditLogShow.jsx | 1
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAutoConfiguration.java | 37 +
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/HttpAuditFilter.java | 35 +
version/db/sys_http_audit_log.sql | 4
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditRule.java | 66 ++
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/AuditingFeignClient.java | 72 ++
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/FeignHttpAuditCapability.java | 20
rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleCreate.jsx | 156 ++++++
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditOutboundRecorder.java | 75 ++
version/db/http_audit_menu.sql | 20
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/OutboundHttpAuditInterceptor.java | 99 +++
33 files changed, 1,488 insertions(+), 26 deletions(-)
diff --git a/rsf-admin/src/i18n/en.js b/rsf-admin/src/i18n/en.js
index 02b05c1..9ff69d5 100644
--- a/rsf-admin/src/i18n/en.js
+++ b/rsf-admin/src/i18n/en.js
@@ -4,7 +4,10 @@
...englishMessages,
hello: 'Hello World',
'menu.httpAuditLog': 'HTTP audit',
+ 'menu.httpAuditRule': 'HTTP audit rules',
'resources.httpAuditLog.name': 'HTTP audit',
+ 'resources.httpAuditRule.name': 'HTTP audit rules',
+ 'resources.httpAuditRule.createTitle': 'New audit rule',
common: {
response: {
success: "Success",
@@ -151,6 +154,7 @@
token: 'Token',
operation: 'Operation',
httpAuditLog: 'HTTP audit',
+ httpAuditRule: 'HTTP audit rules',
config: 'Config',
tenant: 'Tenant',
userLogin: 'Token',
@@ -354,6 +358,35 @@
spendMs: "spend ms",
clientIp: "client IP",
errorMessage: "error",
+ ioDirection: "I/O",
+ },
+ httpAuditRule: {
+ directionLabel: "Direction",
+ direction: { IN: "Inbound", OUT: "Outbound", BOTH: "Both" },
+ requestMaxChars: "Request max chars (-1 = full)",
+ responseMaxChars: "Response max chars (-1 = full)",
+ recordAll: "Record all (ignore whitelist rules)",
+ recordAllOn: "Yes",
+ recordAllOff: "No",
+ ruleTypeLabel: "Rule type",
+ ruleType: {
+ URI: "URI path",
+ IP: "Client IP",
+ REQUEST_BODY: "Request body",
+ },
+ matchModeLabel: "Match mode",
+ matchMode: {
+ EQUAL: "Equal",
+ PREFIX: "Prefix",
+ CONTAINS: "Contains",
+ REGEX: "Regex",
+ },
+ pattern: "Pattern",
+ enabled: "Enabled",
+ enabledOn: "On",
+ enabledOff: "Off",
+ sortOrder: "Sort",
+ remark: "Remark",
},
operationRecord: {
namespace: "namespace",
diff --git a/rsf-admin/src/i18n/zh.js b/rsf-admin/src/i18n/zh.js
index 7749482..b72083d 100644
--- a/rsf-admin/src/i18n/zh.js
+++ b/rsf-admin/src/i18n/zh.js
@@ -4,9 +4,11 @@
...chineseMessages,
hello: '浣犲ソ涓栫晫',
'menu.httpAuditLog': 'HTTP鎺ュ彛瀹¤',
+ 'menu.httpAuditRule': 'HTTP瀹¤瑙勫垯',
resources: {
config: { name: '閰嶇疆鍙傛暟' },
httpAuditLog: { name: 'HTTP鎺ュ彛瀹¤' },
+ httpAuditRule: { name: 'HTTP瀹¤瑙勫垯', createTitle: '鏂板瀹¤瑙勫垯' },
asnOrderItem: { name: '鏀惰揣鏄庣粏' },
outStockItem: { name: '鍑哄簱鍗曟槑缁�' },
},
@@ -160,6 +162,7 @@
token: '鐧诲綍鏃ュ織',
operation: '鎿嶄綔鏃ュ織',
httpAuditLog: 'HTTP鎺ュ彛瀹¤',
+ httpAuditRule: 'HTTP瀹¤瑙勫垯',
config: '閰嶇疆鍙傛暟',
tenant: '绉熸埛绠$悊',
userLogin: '鐧诲綍鏃ュ織',
@@ -386,6 +389,35 @@
spendMs: "鑰楁椂(ms)",
clientIp: "璇锋眰IP",
errorMessage: "寮傚父淇℃伅",
+ ioDirection: "鏂瑰悜",
+ },
+ httpAuditRule: {
+ directionLabel: "浣滅敤鏂瑰悜",
+ direction: { IN: "鍏ョ珯", OUT: "鍑虹珯", BOTH: "鍙屽悜" },
+ requestMaxChars: "璇锋眰浣撴渶澶氬瓧绗�(-1鍏ㄩ噺)",
+ responseMaxChars: "鍝嶅簲浣撴渶澶氬瓧绗�(-1鍏ㄩ噺)",
+ recordAll: "鍏ㄩ噺璁板綍(鏃犺鐧藉悕鍗�)",
+ recordAllOn: "鏄�",
+ recordAllOff: "鍚�",
+ ruleTypeLabel: "瑙勫垯绫诲瀷",
+ ruleType: {
+ URI: "鎺ュ彛璺緞",
+ IP: "璇锋眰IP",
+ REQUEST_BODY: "璇锋眰浣�",
+ },
+ matchModeLabel: "鍖归厤鏂瑰紡",
+ matchMode: {
+ EQUAL: "鐩哥瓑",
+ PREFIX: "鍓嶇紑",
+ CONTAINS: "鍖呭惈",
+ REGEX: "姝e垯",
+ },
+ pattern: "鍖归厤鍐呭",
+ enabled: "鍚敤",
+ enabledOn: "鍚敤",
+ enabledOff: "鍋滅敤",
+ sortOrder: "鎺掑簭",
+ remark: "澶囨敞",
},
operationRecord: {
namespace: "鍛藉悕绌洪棿",
diff --git a/rsf-admin/src/page/ResourceContent.js b/rsf-admin/src/page/ResourceContent.js
index 3583d21..e619fc9 100644
--- a/rsf-admin/src/page/ResourceContent.js
+++ b/rsf-admin/src/page/ResourceContent.js
@@ -69,6 +69,7 @@
import rcsTest from './rcsTest';
import openApiApp from './system/openApiApp';
import httpAuditLog from './system/httpAuditLog';
+import httpAuditRule from './system/httpAuditRule';
const ResourceContent = (node) => {
switch (node.component) {
@@ -202,6 +203,8 @@
return openApiApp;
case "httpAuditLog":
return httpAuditLog;
+ case "httpAuditRule":
+ return httpAuditRule;
default:
return {
list: ListGuesser,
diff --git a/rsf-admin/src/page/system/httpAuditLog/HttpAuditLogList.jsx b/rsf-admin/src/page/system/httpAuditLog/HttpAuditLogList.jsx
index 58fe66d..5093322 100644
--- a/rsf-admin/src/page/system/httpAuditLog/HttpAuditLogList.jsx
+++ b/rsf-admin/src/page/system/httpAuditLog/HttpAuditLogList.jsx
@@ -36,6 +36,14 @@
{ id: "INTERNAL", name: "鍐呴儴" },
]}
/>,
+ <SelectInput
+ source="ioDirection"
+ label="table.field.httpAuditLog.ioDirection"
+ choices={[
+ { id: "IN", name: "IN" },
+ { id: "OUT", name: "OUT" },
+ ]}
+ />,
<TextInput source="functionDesc" label="table.field.httpAuditLog.functionDesc" />,
<TextInput source="method" label="table.field.httpAuditLog.method" />,
];
@@ -58,6 +66,7 @@
<TextField source="serviceName" label="table.field.httpAuditLog.serviceName" />
<TextField source="scopeType" label="table.field.httpAuditLog.scopeType" />
<TextField source="uri" label="table.field.httpAuditLog.uri" />
+ <TextField source="ioDirection" label="table.field.httpAuditLog.ioDirection" />
<TextField source="method" label="table.field.httpAuditLog.method" />
<TextField source="functionDesc" label="table.field.httpAuditLog.functionDesc" />
<TextField source="clientIp" label="table.field.httpAuditLog.clientIp" />
diff --git a/rsf-admin/src/page/system/httpAuditLog/HttpAuditLogShow.jsx b/rsf-admin/src/page/system/httpAuditLog/HttpAuditLogShow.jsx
index 1b19d5c..dab8957 100644
--- a/rsf-admin/src/page/system/httpAuditLog/HttpAuditLogShow.jsx
+++ b/rsf-admin/src/page/system/httpAuditLog/HttpAuditLogShow.jsx
@@ -15,6 +15,7 @@
<TextField source="serviceName" label="table.field.httpAuditLog.serviceName" />
<TextField source="scopeType" label="table.field.httpAuditLog.scopeType" />
<TextField source="uri" label="table.field.httpAuditLog.uri" />
+ <TextField source="ioDirection" label="table.field.httpAuditLog.ioDirection" />
<TextField source="method" label="table.field.httpAuditLog.method" />
<TextField source="functionDesc" label="table.field.httpAuditLog.functionDesc" />
<TextField source="clientIp" label="table.field.httpAuditLog.clientIp" />
diff --git a/rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleCreate.jsx b/rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleCreate.jsx
new file mode 100644
index 0000000..c940242
--- /dev/null
+++ b/rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleCreate.jsx
@@ -0,0 +1,156 @@
+import React from "react";
+import {
+ CreateBase,
+ TextInput,
+ NumberInput,
+ SaveButton,
+ SelectInput,
+ Toolbar,
+ Form,
+ required,
+ useNotify,
+ useTranslate,
+} from 'react-admin';
+import { Dialog, DialogContent, DialogTitle, Grid, Box } from '@mui/material';
+import DialogCloseButton from "@/page/components/DialogCloseButton";
+
+const HttpAuditRuleCreate = (props) => {
+ const { open, setOpen } = props;
+ const notify = useNotify();
+ const translate = useTranslate();
+
+ const ruleTypeChoices = [
+ { id: 'URI', name: translate('table.field.httpAuditRule.ruleType.URI') },
+ { id: 'IP', name: translate('table.field.httpAuditRule.ruleType.IP') },
+ { id: 'REQUEST_BODY', name: translate('table.field.httpAuditRule.ruleType.REQUEST_BODY') },
+ ];
+ const matchModeChoices = [
+ { id: 'EQUAL', name: translate('table.field.httpAuditRule.matchMode.EQUAL') },
+ { id: 'PREFIX', name: translate('table.field.httpAuditRule.matchMode.PREFIX') },
+ { id: 'CONTAINS', name: translate('table.field.httpAuditRule.matchMode.CONTAINS') },
+ { id: 'REGEX', name: translate('table.field.httpAuditRule.matchMode.REGEX') },
+ ];
+ const directionChoices = [
+ { id: 'IN', name: translate('table.field.httpAuditRule.direction.IN') },
+ { id: 'OUT', name: translate('table.field.httpAuditRule.direction.OUT') },
+ { id: 'BOTH', name: translate('table.field.httpAuditRule.direction.BOTH') },
+ ];
+
+ const handleClose = (event, reason) => {
+ if (reason !== "backdropClick") setOpen(false);
+ };
+
+ const handleSuccess = () => {
+ setOpen(false);
+ notify('鏂板鎴愬姛');
+ };
+
+ const handleError = (error) => {
+ notify(error?.message || '鏂板澶辫触', { type: 'error' });
+ };
+
+ return (
+ <CreateBase
+ resource="httpAuditRule"
+ record={{ direction: 'IN', recordAll: 0 }}
+ mutationOptions={{ onSuccess: handleSuccess, onError: handleError }}
+ >
+ <Dialog
+ open={open}
+ onClose={handleClose}
+ fullWidth
+ disableRestoreFocus
+ maxWidth="md"
+ >
+ <Form>
+ <DialogTitle sx={{ position: 'sticky', top: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
+ {translate('resources.httpAuditRule.createTitle')}
+ <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}>
+ <DialogCloseButton onClose={handleClose} />
+ </Box>
+ </DialogTitle>
+ <DialogContent sx={{ mt: 2 }}>
+ <Grid container rowSpacing={2} columnSpacing={2}>
+ <Grid item xs={12} sm={6}>
+ <SelectInput
+ source="ruleType"
+ label="table.field.httpAuditRule.ruleTypeLabel"
+ choices={ruleTypeChoices}
+ validate={required()}
+ fullWidth
+ />
+ </Grid>
+ <Grid item xs={12} sm={6}>
+ <SelectInput
+ source="matchMode"
+ label="table.field.httpAuditRule.matchModeLabel"
+ choices={matchModeChoices}
+ validate={required()}
+ fullWidth
+ />
+ </Grid>
+ <Grid item xs={12}>
+ <TextInput
+ source="pattern"
+ label="table.field.httpAuditRule.pattern"
+ validate={required()}
+ fullWidth
+ multiline
+ minRows={2}
+ />
+ </Grid>
+ <Grid item xs={12} sm={6}>
+ <SelectInput
+ source="direction"
+ label="table.field.httpAuditRule.directionLabel"
+ choices={directionChoices}
+ fullWidth
+ />
+ </Grid>
+ <Grid item xs={12} sm={6}>
+ <SelectInput
+ source="recordAll"
+ label="table.field.httpAuditRule.recordAll"
+ choices={[
+ { id: 1, name: translate('table.field.httpAuditRule.recordAllOn') },
+ { id: 0, name: translate('table.field.httpAuditRule.recordAllOff') },
+ ]}
+ fullWidth
+ />
+ </Grid>
+ <Grid item xs={12} sm={6}>
+ <NumberInput source="requestMaxChars" label="table.field.httpAuditRule.requestMaxChars" fullWidth />
+ </Grid>
+ <Grid item xs={12} sm={6}>
+ <NumberInput source="responseMaxChars" label="table.field.httpAuditRule.responseMaxChars" fullWidth />
+ </Grid>
+ <Grid item xs={12} sm={6}>
+ <SelectInput
+ source="enabled"
+ label="table.field.httpAuditRule.enabled"
+ choices={[
+ { id: 1, name: translate('table.field.httpAuditRule.enabledOn') },
+ { id: 0, name: translate('table.field.httpAuditRule.enabledOff') },
+ ]}
+ defaultValue={1}
+ fullWidth
+ />
+ </Grid>
+ <Grid item xs={12} sm={6}>
+ <NumberInput source="sortOrder" label="table.field.httpAuditRule.sortOrder" defaultValue={0} fullWidth />
+ </Grid>
+ <Grid item xs={12}>
+ <TextInput source="remark" label="table.field.httpAuditRule.remark" fullWidth multiline minRows={2} />
+ </Grid>
+ </Grid>
+ </DialogContent>
+ <Box sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000, p: 2, display: 'flex', justifyContent: 'flex-end' }}>
+ <Toolbar><SaveButton /></Toolbar>
+ </Box>
+ </Form>
+ </Dialog>
+ </CreateBase>
+ );
+};
+
+export default HttpAuditRuleCreate;
diff --git a/rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleEdit.jsx b/rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleEdit.jsx
new file mode 100644
index 0000000..e7e79f5
--- /dev/null
+++ b/rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleEdit.jsx
@@ -0,0 +1,119 @@
+import React from "react";
+import {
+ Edit,
+ SimpleForm,
+ TextInput,
+ NumberInput,
+ SaveButton,
+ SelectInput,
+ Toolbar,
+ required,
+ DeleteButton,
+ useTranslate,
+} from 'react-admin';
+import { Grid, Stack } from '@mui/material';
+import { EDIT_MODE } from '@/config/setting';
+import EditBaseAside from "@/page/components/EditBaseAside";
+import CustomerTopToolBar from "@/page/components/EditTopToolBar";
+
+const FormToolbar = () => (
+ <Toolbar sx={{ justifyContent: 'space-between' }}>
+ <SaveButton />
+ <DeleteButton mutationMode="optimistic" />
+ </Toolbar>
+);
+
+const HttpAuditRuleEdit = () => {
+ const translate = useTranslate();
+ const ruleTypeChoices = [
+ { id: 'URI', name: translate('table.field.httpAuditRule.ruleType.URI') },
+ { id: 'IP', name: translate('table.field.httpAuditRule.ruleType.IP') },
+ { id: 'REQUEST_BODY', name: translate('table.field.httpAuditRule.ruleType.REQUEST_BODY') },
+ ];
+ const matchModeChoices = [
+ { id: 'EQUAL', name: translate('table.field.httpAuditRule.matchMode.EQUAL') },
+ { id: 'PREFIX', name: translate('table.field.httpAuditRule.matchMode.PREFIX') },
+ { id: 'CONTAINS', name: translate('table.field.httpAuditRule.matchMode.CONTAINS') },
+ { id: 'REGEX', name: translate('table.field.httpAuditRule.matchMode.REGEX') },
+ ];
+ const directionChoices = [
+ { id: 'IN', name: translate('table.field.httpAuditRule.direction.IN') },
+ { id: 'OUT', name: translate('table.field.httpAuditRule.direction.OUT') },
+ { id: 'BOTH', name: translate('table.field.httpAuditRule.direction.BOTH') },
+ ];
+
+ return (
+ <Edit
+ resource="httpAuditRule"
+ redirect="list"
+ mutationMode={EDIT_MODE}
+ actions={<CustomerTopToolBar />}
+ aside={<EditBaseAside />}
+ >
+ <SimpleForm
+ toolbar={<FormToolbar />}
+ defaultValues={{ enabled: 1, sortOrder: 0, direction: 'IN', recordAll: 0 }}
+ >
+ <Grid container width={{ xs: '100%', xl: '80%' }} rowSpacing={3} columnSpacing={3}>
+ <Grid item xs={12} md={8}>
+ <Stack spacing={2}>
+ <TextInput source="id" label="common.field.id" disabled />
+ <SelectInput
+ source="ruleType"
+ label="table.field.httpAuditRule.ruleTypeLabel"
+ choices={ruleTypeChoices}
+ validate={required()}
+ fullWidth
+ />
+ <SelectInput
+ source="matchMode"
+ label="table.field.httpAuditRule.matchModeLabel"
+ choices={matchModeChoices}
+ validate={required()}
+ fullWidth
+ />
+ <TextInput
+ source="pattern"
+ label="table.field.httpAuditRule.pattern"
+ validate={required()}
+ fullWidth
+ multiline
+ minRows={3}
+ />
+ <SelectInput
+ source="direction"
+ label="table.field.httpAuditRule.directionLabel"
+ choices={directionChoices}
+ fullWidth
+ />
+ <SelectInput
+ source="recordAll"
+ label="table.field.httpAuditRule.recordAll"
+ choices={[
+ { id: 1, name: translate('table.field.httpAuditRule.recordAllOn') },
+ { id: 0, name: translate('table.field.httpAuditRule.recordAllOff') },
+ ]}
+ fullWidth
+ />
+ <NumberInput source="requestMaxChars" label="table.field.httpAuditRule.requestMaxChars" fullWidth />
+ <NumberInput source="responseMaxChars" label="table.field.httpAuditRule.responseMaxChars" fullWidth />
+ <SelectInput
+ source="enabled"
+ label="table.field.httpAuditRule.enabled"
+ choices={[
+ { id: 1, name: translate('table.field.httpAuditRule.enabledOn') },
+ { id: 0, name: translate('table.field.httpAuditRule.enabledOff') },
+ ]}
+ fullWidth
+ />
+ <NumberInput source="sortOrder" label="table.field.httpAuditRule.sortOrder" fullWidth />
+ <TextInput source="remark" label="table.field.httpAuditRule.remark" fullWidth multiline minRows={2} />
+ </Stack>
+ </Grid>
+ </Grid>
+ </SimpleForm>
+ </Edit>
+ );
+};
+
+export default HttpAuditRuleEdit;
diff --git a/rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleList.jsx b/rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleList.jsx
new file mode 100644
index 0000000..fa23bf0
--- /dev/null
+++ b/rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleList.jsx
@@ -0,0 +1,121 @@
+import React, { useMemo, useState } from "react";
+import {
+ List,
+ DatagridConfigurable,
+ SearchInput,
+ TopToolbar,
+ SelectColumnsButton,
+ EditButton,
+ FilterButton,
+ TextField,
+ FunctionField,
+ SelectInput,
+ WrapperField,
+ DeleteButton,
+ useTranslate,
+} from 'react-admin';
+import { Box } from '@mui/material';
+import { styled } from '@mui/material/styles';
+import HttpAuditRuleCreate from "./HttpAuditRuleCreate";
+import EmptyData from "@/page/components/EmptyData";
+import MyCreateButton from "@/page/components/MyCreateButton";
+import { OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
+
+const StyledDatagrid = styled(DatagridConfigurable)(() => ({
+ '& .RaDatagrid-row': { cursor: 'auto' },
+ '& .opt': { width: 140 },
+}));
+
+const HttpAuditRuleList = () => {
+ const [createDialog, setCreateDialog] = useState(false);
+ const translate = useTranslate();
+
+ const filters = useMemo(() => [
+ <SearchInput source="condition" alwaysOn />,
+ <SelectInput
+ source="ruleType"
+ label="table.field.httpAuditRule.ruleTypeLabel"
+ choices={[
+ { id: 'URI', name: translate('table.field.httpAuditRule.ruleType.URI') },
+ { id: 'IP', name: translate('table.field.httpAuditRule.ruleType.IP') },
+ { id: 'REQUEST_BODY', name: translate('table.field.httpAuditRule.ruleType.REQUEST_BODY') },
+ ]}
+ />,
+ <SelectInput
+ source="enabled"
+ label="table.field.httpAuditRule.enabled"
+ choices={[
+ { id: 1, name: translate('table.field.httpAuditRule.enabledOn') },
+ { id: 0, name: translate('table.field.httpAuditRule.enabledOff') },
+ ]}
+ />,
+ ], [translate]);
+
+ return (
+ <Box display="flex">
+ <List
+ title="menu.httpAuditRule"
+ empty={<EmptyData onClick={() => setCreateDialog(true)} />}
+ filters={filters}
+ sort={{ field: "sortOrder", order: "asc" }}
+ actions={(
+ <TopToolbar>
+ <FilterButton />
+ <MyCreateButton onClick={() => setCreateDialog(true)} />
+ <SelectColumnsButton preferenceKey="httpAuditRule" />
+ </TopToolbar>
+ )}
+ perPage={DEFAULT_PAGE_SIZE}
+ >
+ <StyledDatagrid
+ preferenceKey="httpAuditRule"
+ bulkActionButtons={() => <DeleteButton mutationMode={OPERATE_MODE} />}
+ rowClick={false}
+ >
+ <TextField source="id" label="common.field.id" />
+ <FunctionField
+ source="ruleType"
+ label="table.field.httpAuditRule.ruleTypeLabel"
+ render={(r) => translate(`table.field.httpAuditRule.ruleType.${r.ruleType}`)}
+ />
+ <FunctionField
+ source="matchMode"
+ label="table.field.httpAuditRule.matchModeLabel"
+ render={(r) => translate(`table.field.httpAuditRule.matchMode.${r.matchMode}`)}
+ />
+ <TextField source="pattern" label="table.field.httpAuditRule.pattern" />
+ <FunctionField
+ source="direction"
+ label="table.field.httpAuditRule.directionLabel"
+ render={(r) => translate(`table.field.httpAuditRule.direction.${r.direction || 'IN'}`)}
+ />
+ <FunctionField
+ source="recordAll"
+ label="table.field.httpAuditRule.recordAll"
+ render={(r) => (r.recordAll === 1
+ ? translate('table.field.httpAuditRule.recordAllOn')
+ : translate('table.field.httpAuditRule.recordAllOff'))}
+ />
+ <TextField source="requestMaxChars" label="table.field.httpAuditRule.requestMaxChars" />
+ <TextField source="responseMaxChars" label="table.field.httpAuditRule.responseMaxChars" />
+ <FunctionField
+ source="enabled"
+ label="table.field.httpAuditRule.enabled"
+ render={(r) => (r.enabled === 1
+ ? translate('table.field.httpAuditRule.enabledOn')
+ : translate('table.field.httpAuditRule.enabledOff'))}
+ />
+ <TextField source="sortOrder" label="table.field.httpAuditRule.sortOrder" />
+ <TextField source="remark" label="table.field.httpAuditRule.remark" />
+ <WrapperField cellClassName="opt" label="common.field.opt">
+ <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
+ <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
+ </WrapperField>
+ </StyledDatagrid>
+ </List>
+ <HttpAuditRuleCreate open={createDialog} setOpen={setCreateDialog} />
+ </Box>
+ );
+};
+
+export default HttpAuditRuleList;
diff --git a/rsf-admin/src/page/system/httpAuditRule/index.jsx b/rsf-admin/src/page/system/httpAuditRule/index.jsx
new file mode 100644
index 0000000..d403405
--- /dev/null
+++ b/rsf-admin/src/page/system/httpAuditRule/index.jsx
@@ -0,0 +1,11 @@
+import React from "react";
+import { ShowGuesser } from "react-admin";
+import HttpAuditRuleList from "./HttpAuditRuleList";
+import HttpAuditRuleEdit from "./HttpAuditRuleEdit";
+
+export default {
+ list: HttpAuditRuleList,
+ edit: HttpAuditRuleEdit,
+ show: ShowGuesser,
+ recordRepresentation: (record) => record?.pattern || record?.id || '',
+};
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAutoConfiguration.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAutoConfiguration.java
index b33ef9d..8e25d7a 100644
--- a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAutoConfiguration.java
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAutoConfiguration.java
@@ -1,9 +1,14 @@
package com.vincent.rsf.httpaudit.config;
import com.vincent.rsf.httpaudit.mapper.HttpAuditLogMapper;
+import com.vincent.rsf.httpaudit.mapper.HttpAuditRuleMapper;
import com.vincent.rsf.httpaudit.props.HttpAuditProperties;
import com.vincent.rsf.httpaudit.service.HttpAuditAsyncRecorder;
+import com.vincent.rsf.httpaudit.service.HttpAuditOutboundRecorder;
+import com.vincent.rsf.httpaudit.service.HttpAuditRuleService;
+import com.vincent.rsf.httpaudit.service.HttpAuditRuleServiceImpl;
import com.vincent.rsf.httpaudit.web.HttpAuditFilter;
+import com.vincent.rsf.httpaudit.web.OutboundHttpAuditInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -13,6 +18,7 @@
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@@ -22,14 +28,40 @@
*/
@Configuration
@EnableAsync
+@EnableScheduling
@EnableConfigurationProperties(HttpAuditProperties.class)
@ConditionalOnProperty(prefix = "http-audit", name = "enabled", havingValue = "true", matchIfMissing = true)
@MapperScan("com.vincent.rsf.httpaudit.mapper")
public class HttpAuditAutoConfiguration {
@Bean
+ public HttpAuditRuleService httpAuditRuleService(HttpAuditRuleMapper mapper, HttpAuditProperties props) {
+ return new HttpAuditRuleServiceImpl(mapper, props);
+ }
+
+ @Bean
public HttpAuditAsyncRecorder httpAuditAsyncRecorder(HttpAuditLogMapper httpAuditLogMapper) {
return new HttpAuditAsyncRecorder(httpAuditLogMapper);
+ }
+
+ @Bean
+ public HttpAuditOutboundRecorder httpAuditOutboundRecorder(
+ HttpAuditAsyncRecorder recorder,
+ HttpAuditProperties props,
+ Environment env,
+ HttpAuditRuleService httpAuditRuleService) {
+ return new HttpAuditOutboundRecorder(recorder, props, env, httpAuditRuleService);
+ }
+
+ @Bean
+ public OutboundHttpAuditInterceptor outboundHttpAuditInterceptor(HttpAuditOutboundRecorder httpAuditOutboundRecorder) {
+ return new OutboundHttpAuditInterceptor(httpAuditOutboundRecorder);
+ }
+
+ @Bean
+ public HttpAuditRestTemplateBeanPostProcessor httpAuditRestTemplateBeanPostProcessor(
+ OutboundHttpAuditInterceptor outboundHttpAuditInterceptor) {
+ return new HttpAuditRestTemplateBeanPostProcessor(outboundHttpAuditInterceptor);
}
@Bean(name = "httpAuditExecutor")
@@ -45,8 +77,9 @@
@Bean
public FilterRegistrationBean<HttpAuditFilter> httpAuditFilterRegistration(
- HttpAuditAsyncRecorder recorder, HttpAuditProperties props, Environment env) {
- HttpAuditFilter filter = new HttpAuditFilter(recorder, props, env);
+ HttpAuditAsyncRecorder recorder, HttpAuditProperties props, Environment env,
+ HttpAuditRuleService httpAuditRuleService) {
+ HttpAuditFilter filter = new HttpAuditFilter(recorder, props, env, httpAuditRuleService);
FilterRegistrationBean<HttpAuditFilter> reg = new FilterRegistrationBean<>();
reg.setFilter(filter);
reg.addUrlPatterns("/*");
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditRestTemplateBeanPostProcessor.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditRestTemplateBeanPostProcessor.java
new file mode 100644
index 0000000..cdeeaa5
--- /dev/null
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditRestTemplateBeanPostProcessor.java
@@ -0,0 +1,28 @@
+package com.vincent.rsf.httpaudit.config;
+
+import com.vincent.rsf.httpaudit.web.OutboundHttpAuditInterceptor;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * 涓烘墍鏈� RestTemplate 娉ㄥ唽鍑虹珯瀹¤鎷︽埅鍣�
+ */
+@RequiredArgsConstructor
+public class HttpAuditRestTemplateBeanPostProcessor implements BeanPostProcessor {
+
+ private final OutboundHttpAuditInterceptor outboundHttpAuditInterceptor;
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+ if (bean instanceof RestTemplate) {
+ RestTemplate rt = (RestTemplate) bean;
+ boolean exists = rt.getInterceptors().stream().anyMatch(i -> i instanceof OutboundHttpAuditInterceptor);
+ if (!exists) {
+ rt.getInterceptors().add(0, outboundHttpAuditInterceptor);
+ }
+ }
+ return bean;
+ }
+}
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditLog.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditLog.java
index 6338006..34eaaf0 100644
--- a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditLog.java
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditLog.java
@@ -29,9 +29,12 @@
/** EXTERNAL-澶栭儴锛汭NTERNAL-鍐呴儴 */
private String scopeType;
- /** 璇锋眰璺緞锛堜笉鍚煙鍚嶏級 */
+ /** 鍏ョ珯璺緞鎴栧嚭绔欏畬鏁� URL */
private String uri;
+ /** IN 鍏ョ珯 / OUT 鍑虹珯 */
+ private String ioDirection;
+
private String method;
/** 鍔熻兘璇存槑锛堟潵鑷厤缃渶闀垮墠缂�鍖归厤锛� */
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditRule.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditRule.java
new file mode 100644
index 0000000..ee8e30d
--- /dev/null
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditRule.java
@@ -0,0 +1,66 @@
+package com.vincent.rsf.httpaudit.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * HTTP 瀹¤鐧藉悕鍗曡鍒欙紙浠呭懡涓鍒欐椂鎵嶅啓瀹¤鏃ュ織锛屽彈 http-audit.whitelist-only 鎺у埗锛�
+ */
+@Data
+@Accessors(chain = true)
+@TableName("sys_http_audit_rule")
+public class HttpAuditRule implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /** URI锛氬尮閰嶈姹傝矾寰勶紱IP锛氬尮閰嶅鎴风 IP锛汻EQUEST_BODY锛氬尮閰嶈姹備綋鏂囨湰 */
+ public static final String TYPE_URI = "URI";
+ public static final String TYPE_IP = "IP";
+ public static final String TYPE_REQUEST_BODY = "REQUEST_BODY";
+
+ public static final String MODE_EQUAL = "EQUAL";
+ public static final String MODE_PREFIX = "PREFIX";
+ public static final String MODE_CONTAINS = "CONTAINS";
+ public static final String MODE_REGEX = "REGEX";
+
+ public static final String DIR_IN = "IN";
+ public static final String DIR_OUT = "OUT";
+ public static final String DIR_BOTH = "BOTH";
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private String ruleType;
+
+ private String matchMode;
+
+ private String pattern;
+
+ private String direction;
+
+ private Integer requestMaxChars;
+
+ private Integer responseMaxChars;
+
+ private Integer enabled;
+
+ private Integer recordAll;
+
+ private Integer sortOrder;
+
+ private String remark;
+
+ private Date createTime;
+
+ private Date updateTime;
+
+ @TableLogic
+ private Integer deleted;
+}
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/mapper/HttpAuditRuleMapper.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/mapper/HttpAuditRuleMapper.java
new file mode 100644
index 0000000..6e4392a
--- /dev/null
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/mapper/HttpAuditRuleMapper.java
@@ -0,0 +1,9 @@
+package com.vincent.rsf.httpaudit.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.vincent.rsf.httpaudit.entity.HttpAuditRule;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HttpAuditRuleMapper extends BaseMapper<HttpAuditRule> {
+}
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/model/HttpAuditDecision.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/model/HttpAuditDecision.java
new file mode 100644
index 0000000..0012362
--- /dev/null
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/model/HttpAuditDecision.java
@@ -0,0 +1,23 @@
+package com.vincent.rsf.httpaudit.model;
+
+import lombok.Value;
+
+/**
+ * 鏄惁鍐欏叆瀹¤鍙婅鍒欎笂鐨勮姹�/鍝嶅簲鎴柇锛�-1 鍏ㄩ噺锛宯ull 鐢� yml 榛樿锛�
+ */
+@Value
+public class HttpAuditDecision {
+
+ public static final HttpAuditDecision SKIP = new HttpAuditDecision(false, null, null);
+
+ /** 鏄惁鍐欏叆瀹¤琛� */
+ boolean audit;
+ /** 璇锋眰浣撳叆搴撴渶澶у瓧绗︼紝-1 涓嶆埅鏂� */
+ Integer requestMaxChars;
+ /** 鍝嶅簲浣撳叆搴撴渶澶у瓧绗︼紝-1 涓嶆埅鏂� */
+ Integer responseMaxChars;
+
+ public static HttpAuditDecision yes(Integer requestMaxChars, Integer responseMaxChars) {
+ return new HttpAuditDecision(true, requestMaxChars, responseMaxChars);
+ }
+}
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/props/HttpAuditProperties.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/props/HttpAuditProperties.java
index a553c23..6c00a35 100644
--- a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/props/HttpAuditProperties.java
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/props/HttpAuditProperties.java
@@ -17,11 +17,24 @@
private boolean enabled = true;
+ /**
+ * true锛氬叆绔�/鍑虹珯鏄惁钀藉簱鐢� {@code sys_http_audit_rule} 鍐冲畾锛堝惈 record_all=1 鍏ㄩ噺銆佹柟鍚� IN/OUT/BOTH銆佹埅鏂暱搴︼級锛沠alse锛氭帓闄よ矾寰勫鍏ョ珯涓庡叏閮ㄥ嚭绔欏潎璁板綍锛屾埅鏂敤鏈厤缃� + 瑙勫垯涓�屽叏閲忋�嶈鐨� request/response_max_chars锛堣嫢鏈夛級
+ */
+ private boolean whitelistOnly = true;
+
+ /** 瑙勫垯缂撳瓨瀹氭椂鍒锋柊闂撮殧锛堟绉掞級 */
+ private long ruleCacheRefreshMs = 60_000L;
+
/** 鏌ヨ绫诲搷搴旀渶澶氫繚鐣欏瓧绗︽暟 */
private int queryResponseMaxChars = 500;
/** 闈炴煡璇㈢被鍝嶅簲鏈�澶氬叆搴撳瓧鑺傦紙瓒呭嚭鎴柇骞舵爣璁帮級 */
private int maxResponseStoreChars = 65535;
+
+ /**
+ * 瑙勫垯鏈寚瀹� request_max_chars 鏃剁殑榛樿锛氬瓧绗︽暟锛�-1 琛ㄧず鍏ュ簱涓嶆埅鏂姹備綋
+ */
+ private int defaultRequestStoreChars = 65535;
/** 璇锋眰浣撶紦瀛樹笂闄愶紙瀛楄妭锛� */
private int maxRequestCacheBytes = 2 * 1024 * 1024;
@@ -31,6 +44,20 @@
/** 涓嶈惤搴撶殑璺緞鍓嶇紑 */
private List<String> excludePathPrefixes = defaultExcludes();
+
+ /**
+ * true锛氶粯璁ゆ帓闄や腑鐨� /httpAuditLog銆�/httpAuditRule 浠嶇敓鏁堬紱false锛氫笉鍐嶆帓闄よ繖涓ら」锛堜究浜庤皟璇曪紱record_all 涔熸棤娉曠粫杩� true 鏃剁殑鎺掗櫎锛�
+ */
+ private boolean excludeAuditSelfPaths = true;
+
+ /** Filter 瀹為檯浣跨敤鐨勫墠缂�锛堝彈 excludeAuditSelfPaths 褰卞搷锛� */
+ public List<String> getEffectiveExcludePrefixes() {
+ List<String> list = excludePathPrefixes == null ? new ArrayList<>() : new ArrayList<>(excludePathPrefixes);
+ if (!excludeAuditSelfPaths) {
+ list.removeIf(p -> "/httpAuditLog".equals(p) || "/httpAuditRule".equals(p));
+ }
+ return list;
+ }
/** 瑙嗕负澶栭儴璋冪敤鐨勮矾寰勫墠缂�锛堝叾浣欎负鍐呴儴锛� */
private List<String> externalPathPrefixes = defaultExternal();
@@ -51,6 +78,7 @@
list.add("/favicon.ico");
list.add("/static/");
list.add("/httpAuditLog");
+ list.add("/httpAuditRule");
return list;
}
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditOutboundRecorder.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditOutboundRecorder.java
new file mode 100644
index 0000000..238108c
--- /dev/null
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditOutboundRecorder.java
@@ -0,0 +1,75 @@
+package com.vincent.rsf.httpaudit.service;
+
+import com.vincent.rsf.httpaudit.entity.HttpAuditLog;
+import com.vincent.rsf.httpaudit.model.HttpAuditDecision;
+import com.vincent.rsf.httpaudit.props.HttpAuditProperties;
+import com.vincent.rsf.httpaudit.support.HttpAuditSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.env.Environment;
+
+import java.util.Date;
+
+/**
+ * 鍑虹珯 HTTP 瀹¤钀藉簱锛圧estTemplate / Feign 鍏辩敤锛�
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class HttpAuditOutboundRecorder {
+
+ private final HttpAuditAsyncRecorder recorder;
+ private final HttpAuditProperties props;
+ private final Environment environment;
+ private final HttpAuditRuleService ruleService;
+
+ public boolean isAuditEnabled() {
+ return props.isEnabled();
+ }
+
+ public HttpAuditDecision decideOutbound(String url, String method, String requestBody) {
+ return ruleService.decideOutbound(url, method, requestBody == null ? "" : requestBody);
+ }
+
+ public void saveOutbound(String functionDesc, String url, String method, String reqText,
+ HttpAuditDecision dec, Integer httpStatus, String resText, long startTimeMs, Throwable ex) {
+ if (!dec.isAudit()) {
+ return;
+ }
+ try {
+ int reqMax = dec.getRequestMaxChars() != null ? dec.getRequestMaxChars() : props.getDefaultRequestStoreChars();
+ int resMax = dec.getResponseMaxChars() != null ? dec.getResponseMaxChars() : props.getMaxResponseStoreChars();
+ String reqStored = HttpAuditSupport.storeWithCharLimit(reqText, reqMax);
+ String resStored = HttpAuditSupport.storeWithCharLimit(resText, resMax);
+ int truncated = HttpAuditSupport.overCharLimit(resText, resMax) ? 1 : 0;
+ int ok = (ex == null && httpStatus != null && httpStatus >= 200 && httpStatus < 400) ? 1 : 0;
+ String errMsg = null;
+ if (ex != null) {
+ String s = ex.toString();
+ errMsg = s.length() > 4000 ? s.substring(0, 4000) + "..." : s;
+ }
+ String appName = environment.getProperty("spring.application.name", "unknown");
+ String uriStored = HttpAuditSupport.truncateUriForStore(url, 512);
+ HttpAuditLog entity = new HttpAuditLog()
+ .setServiceName(appName)
+ .setScopeType("EXTERNAL")
+ .setUri(uriStored)
+ .setIoDirection("OUT")
+ .setMethod(method)
+ .setFunctionDesc(functionDesc)
+ .setQueryString(null)
+ .setRequestBody(reqStored)
+ .setResponseBody(resStored)
+ .setResponseTruncated(truncated)
+ .setHttpStatus(httpStatus)
+ .setOkFlag(ok)
+ .setSpendMs((int) Math.min(Integer.MAX_VALUE, System.currentTimeMillis() - startTimeMs))
+ .setClientIp(null)
+ .setErrorMessage(errMsg)
+ .setCreateTime(new Date())
+ .setDeleted(0);
+ recorder.save(entity);
+ } catch (Throwable t) {
+ log.debug("http-audit 鍑虹珯缁勮瀹¤澶辫触锛歿}", t.getMessage());
+ }
+ }
+}
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleService.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleService.java
new file mode 100644
index 0000000..4a9fc39
--- /dev/null
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleService.java
@@ -0,0 +1,25 @@
+package com.vincent.rsf.httpaudit.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.vincent.rsf.httpaudit.entity.HttpAuditRule;
+import com.vincent.rsf.httpaudit.model.HttpAuditDecision;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 瀹¤瑙勫垯锛氬懡涓垯璁板綍锛堢櫧鍚嶅崟妯″紡锛�
+ */
+public interface HttpAuditRuleService extends IService<HttpAuditRule> {
+
+ /** 鏄惁搴斿啓鍏ュ璁℃棩蹇楋紙whitelist-only=false 鏃舵亽涓� true锛� */
+ boolean shouldAudit(HttpServletRequest request, String requestBody);
+
+ /** 鍏ョ珯鏄惁璁板強鎴柇闀垮害 */
+ HttpAuditDecision decideInbound(HttpServletRequest request, String requestBody);
+
+ /** 鍑虹珯 RestTemplate 鏄惁璁板強鎴柇闀垮害锛沠ullUrl 涓哄畬鏁磋姹� URL */
+ HttpAuditDecision decideOutbound(String fullUrl, String method, String requestBody);
+
+ /** 閲嶈浇瑙勫垯缂撳瓨锛堜繚瀛�/淇敼/鍒犻櫎鍚庤皟鐢級 */
+ void refreshCache();
+}
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleServiceImpl.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleServiceImpl.java
new file mode 100644
index 0000000..9fb77a1
--- /dev/null
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleServiceImpl.java
@@ -0,0 +1,256 @@
+package com.vincent.rsf.httpaudit.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.vincent.rsf.httpaudit.entity.HttpAuditRule;
+import com.vincent.rsf.httpaudit.mapper.HttpAuditRuleMapper;
+import com.vincent.rsf.httpaudit.model.HttpAuditDecision;
+import com.vincent.rsf.httpaudit.props.HttpAuditProperties;
+import com.vincent.rsf.httpaudit.support.HttpAuditSupport;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.http.HttpServletRequest;
+import java.net.URI;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.regex.Pattern;
+
+/**
+ * 瑙勫垯缂撳瓨锛涘叆绔�/鍑虹珯鎴栧叧绯伙紱record_all 鏃剁櫧鍚嶅崟涓嬩篃鍏ㄨ
+ */
+@Slf4j
+public class HttpAuditRuleServiceImpl extends ServiceImpl<HttpAuditRuleMapper, HttpAuditRule> implements HttpAuditRuleService {
+
+ private final HttpAuditProperties props;
+ private final CopyOnWriteArrayList<HttpAuditRule> cache = new CopyOnWriteArrayList<>();
+
+ public HttpAuditRuleServiceImpl(HttpAuditRuleMapper mapper, HttpAuditProperties props) {
+ this.baseMapper = mapper;
+ this.props = props;
+ }
+
+ @PostConstruct
+ public void init() {
+ refreshCache();
+ }
+
+ @Override
+ @Scheduled(fixedDelayString = "${http-audit.rule-cache-refresh-ms:60000}")
+ public void refreshCache() {
+ try {
+ List<HttpAuditRule> list = list(new LambdaQueryWrapper<HttpAuditRule>()
+ .eq(HttpAuditRule::getDeleted, 0)
+ .eq(HttpAuditRule::getEnabled, 1)
+ .orderByAsc(HttpAuditRule::getSortOrder)
+ .orderByAsc(HttpAuditRule::getId));
+ cache.clear();
+ cache.addAll(list);
+ log.debug("http-audit 瑙勫垯缂撳瓨宸插埛鏂帮紝鏉℃暟={}", cache.size());
+ } catch (Exception e) {
+ log.warn("http-audit 瑙勫垯缂撳瓨鍒锋柊澶辫触锛堢櫧鍚嶅崟灏嗕笉鐢熸晥锛屽叆绔欎笉钀藉簱锛�", e);
+ }
+ }
+
+ @Override
+ public boolean shouldAudit(HttpServletRequest request, String requestBody) {
+ return decideInbound(request, requestBody).isAudit();
+ }
+
+ @Override
+ public HttpAuditDecision decideInbound(HttpServletRequest request, String requestBody) {
+ if (!props.isWhitelistOnly()) {
+ return HttpAuditDecision.yes(reqLimitFromRecordAllRow(), resLimitFromRecordAllRow());
+ }
+ if (cache.isEmpty()) {
+ return HttpAuditDecision.SKIP;
+ }
+ HttpAuditRule allRow = firstRecordAllRule();
+ if (allRow != null) {
+ return HttpAuditDecision.yes(allRow.getRequestMaxChars(), allRow.getResponseMaxChars());
+ }
+ String path = HttpAuditSupport.safePath(request);
+ String ip = HttpAuditSupport.clientIp(request);
+ String body = requestBody == null ? "" : requestBody;
+ for (HttpAuditRule r : cache) {
+ if (isRecordAll(r)) {
+ continue;
+ }
+ if (!appliesInbound(r)) {
+ continue;
+ }
+ try {
+ if (matchInbound(r, path, ip, body)) {
+ return HttpAuditDecision.yes(r.getRequestMaxChars(), r.getResponseMaxChars());
+ }
+ } catch (Exception e) {
+ log.debug("http-audit 瑙勫垯 id={} 鍖归厤寮傚父锛歿}", r.getId(), e.getMessage());
+ }
+ }
+ return HttpAuditDecision.SKIP;
+ }
+
+ @Override
+ public HttpAuditDecision decideOutbound(String fullUrl, String method, String requestBody) {
+ if (!props.isWhitelistOnly()) {
+ return HttpAuditDecision.yes(reqLimitFromRecordAllRow(), resLimitFromRecordAllRow());
+ }
+ if (cache.isEmpty()) {
+ return HttpAuditDecision.SKIP;
+ }
+ HttpAuditRule allRow = firstRecordAllRule();
+ if (allRow != null) {
+ return HttpAuditDecision.yes(allRow.getRequestMaxChars(), allRow.getResponseMaxChars());
+ }
+ String body = requestBody == null ? "" : requestBody;
+ for (HttpAuditRule r : cache) {
+ if (isRecordAll(r)) {
+ continue;
+ }
+ if (!appliesOutbound(r)) {
+ continue;
+ }
+ try {
+ if (matchOutbound(r, fullUrl, body)) {
+ return HttpAuditDecision.yes(r.getRequestMaxChars(), r.getResponseMaxChars());
+ }
+ } catch (Exception e) {
+ log.debug("http-audit 鍑虹珯瑙勫垯 id={} 鍖归厤寮傚父锛歿}", r.getId(), e.getMessage());
+ }
+ }
+ return HttpAuditDecision.SKIP;
+ }
+
+ private Integer reqLimitFromRecordAllRow() {
+ HttpAuditRule row = firstRecordAllRule();
+ return row == null ? null : row.getRequestMaxChars();
+ }
+
+ private Integer resLimitFromRecordAllRow() {
+ HttpAuditRule row = firstRecordAllRule();
+ return row == null ? null : row.getResponseMaxChars();
+ }
+
+ private HttpAuditRule firstRecordAllRule() {
+ for (HttpAuditRule r : cache) {
+ if (isRecordAll(r)) {
+ return r;
+ }
+ }
+ return null;
+ }
+
+ private static boolean isRecordAll(HttpAuditRule r) {
+ return r.getRecordAll() != null && r.getRecordAll() == 1;
+ }
+
+ private static String dir(HttpAuditRule r) {
+ String d = r.getDirection();
+ if (d == null || d.isEmpty()) {
+ return HttpAuditRule.DIR_IN;
+ }
+ return d;
+ }
+
+ private static boolean appliesInbound(HttpAuditRule r) {
+ if (isRecordAll(r)) {
+ return false;
+ }
+ String d = dir(r);
+ return HttpAuditRule.DIR_IN.equals(d) || HttpAuditRule.DIR_BOTH.equals(d);
+ }
+
+ private static boolean appliesOutbound(HttpAuditRule r) {
+ if (isRecordAll(r)) {
+ return false;
+ }
+ String d = dir(r);
+ return HttpAuditRule.DIR_OUT.equals(d) || HttpAuditRule.DIR_BOTH.equals(d);
+ }
+
+ private static boolean matchInbound(HttpAuditRule r, String path, String ip, String body) {
+ return matchByRuleType(r, path, ip, body);
+ }
+
+ private boolean matchOutbound(HttpAuditRule r, String fullUrl, String body) {
+ String t = r.getRuleType();
+ String mode = r.getMatchMode();
+ String p = r.getPattern();
+ if (p == null) {
+ return false;
+ }
+ if (HttpAuditRule.TYPE_URI.equals(t)) {
+ if (matchString(fullUrl, mode, p)) {
+ return true;
+ }
+ return matchString(extractPath(fullUrl), mode, p);
+ }
+ if (HttpAuditRule.TYPE_IP.equals(t)) {
+ String host = extractHost(fullUrl);
+ return matchString(host, mode, p);
+ }
+ if (HttpAuditRule.TYPE_REQUEST_BODY.equals(t)) {
+ return matchString(body, mode, p);
+ }
+ return false;
+ }
+
+ private static String extractPath(String url) {
+ try {
+ URI u = URI.create(url);
+ String path = u.getPath();
+ return path == null ? "" : path;
+ } catch (Exception e) {
+ return url == null ? "" : url;
+ }
+ }
+
+ private static String extractHost(String url) {
+ try {
+ URI u = URI.create(url);
+ String h = u.getHost();
+ return h == null ? "" : h;
+ } catch (Exception e) {
+ return "";
+ }
+ }
+
+ private static boolean matchByRuleType(HttpAuditRule r, String path, String ip, String body) {
+ String t = r.getRuleType();
+ String mode = r.getMatchMode();
+ String p = r.getPattern();
+ if (p == null) {
+ return false;
+ }
+ if (HttpAuditRule.TYPE_URI.equals(t)) {
+ return matchString(path, mode, p);
+ }
+ if (HttpAuditRule.TYPE_IP.equals(t)) {
+ return matchString(ip, mode, p);
+ }
+ if (HttpAuditRule.TYPE_REQUEST_BODY.equals(t)) {
+ return matchString(body, mode, p);
+ }
+ return false;
+ }
+
+ private static boolean matchString(String value, String mode, String pattern) {
+ if (value == null) {
+ value = "";
+ }
+ if (HttpAuditRule.MODE_EQUAL.equals(mode)) {
+ return value.equals(pattern);
+ }
+ if (HttpAuditRule.MODE_PREFIX.equals(mode)) {
+ return value.startsWith(pattern);
+ }
+ if (HttpAuditRule.MODE_CONTAINS.equals(mode)) {
+ return value.contains(pattern);
+ }
+ if (HttpAuditRule.MODE_REGEX.equals(mode)) {
+ return Pattern.compile(pattern).matcher(value).find();
+ }
+ return false;
+ }
+}
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/support/HttpAuditSupport.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/support/HttpAuditSupport.java
index 67ac455..e4cdcb1 100644
--- a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/support/HttpAuditSupport.java
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/support/HttpAuditSupport.java
@@ -55,7 +55,7 @@
public static boolean shouldExclude(HttpServletRequest request, HttpAuditProperties props) {
String path = safePath(request);
- for (String p : props.getExcludePathPrefixes()) {
+ for (String p : props.getEffectiveExcludePrefixes()) {
if (p != null && !p.isEmpty() && path.startsWith(p)) {
return true;
}
@@ -119,4 +119,29 @@
}
return s.substring(0, maxChars) + "...(truncated,len=" + s.length() + ")";
}
+
+ /** maxChars<0 涓嶆埅鏂� */
+ public static String storeWithCharLimit(String s, int maxChars) {
+ if (s == null) {
+ return null;
+ }
+ if (maxChars < 0) {
+ return s;
+ }
+ return truncateForStore(s, maxChars);
+ }
+
+ public static boolean overCharLimit(String s, int maxChars) {
+ return s != null && maxChars >= 0 && s.length() > maxChars;
+ }
+
+ public static String truncateUriForStore(String uri, int maxLen) {
+ if (uri == null) {
+ return "";
+ }
+ if (maxLen <= 0 || uri.length() <= maxLen) {
+ return uri;
+ }
+ return uri.substring(0, maxLen - 3) + "...";
+ }
}
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/HttpAuditFilter.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/HttpAuditFilter.java
index 1abd8f1..784dc49 100644
--- a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/HttpAuditFilter.java
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/HttpAuditFilter.java
@@ -1,8 +1,10 @@
package com.vincent.rsf.httpaudit.web;
import com.vincent.rsf.httpaudit.entity.HttpAuditLog;
+import com.vincent.rsf.httpaudit.model.HttpAuditDecision;
import com.vincent.rsf.httpaudit.props.HttpAuditProperties;
import com.vincent.rsf.httpaudit.service.HttpAuditAsyncRecorder;
+import com.vincent.rsf.httpaudit.service.HttpAuditRuleService;
import com.vincent.rsf.httpaudit.support.HttpAuditSupport;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -29,6 +31,7 @@
private final HttpAuditAsyncRecorder recorder;
private final HttpAuditProperties props;
private final Environment environment;
+ private final HttpAuditRuleService httpAuditRuleService;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
@@ -74,21 +77,33 @@
reqBody = HttpAuditSupport.bytesToString(req.getContentAsByteArray(), charset);
}
+ HttpAuditDecision dec = httpAuditRuleService.decideInbound(req, reqBody);
+ if (!dec.isAudit()) {
+ return;
+ }
+
+ int reqMax = dec.getRequestMaxChars() != null ? dec.getRequestMaxChars() : props.getDefaultRequestStoreChars();
+ String reqStored = HttpAuditSupport.storeWithCharLimit(reqBody, reqMax);
+
String respCt = res.getContentType();
String resBodyRaw = HttpAuditSupport.bytesToString(res.getContentAsByteArray(), charset);
+ int resMax;
+ if (dec.getResponseMaxChars() != null) {
+ resMax = dec.getResponseMaxChars();
+ } else if (HttpAuditSupport.isQueryLike(req)) {
+ resMax = props.getQueryResponseMaxChars();
+ } else {
+ resMax = props.getMaxResponseStoreChars();
+ }
+
String resBodyToStore;
int truncated = 0;
if (respCt != null && (respCt.contains("octet-stream") || respCt.contains("application/pdf"))) {
resBodyToStore = "[binary response omitted]";
truncated = 1;
- } else if (HttpAuditSupport.isQueryLike(req)) {
- resBodyToStore = HttpAuditSupport.truncateForStore(resBodyRaw, props.getQueryResponseMaxChars());
- if (resBodyRaw != null && resBodyRaw.length() > props.getQueryResponseMaxChars()) {
- truncated = 1;
- }
} else {
- resBodyToStore = HttpAuditSupport.truncateForStore(resBodyRaw, props.getMaxResponseStoreChars());
- if (resBodyRaw != null && resBodyRaw.length() > props.getMaxResponseStoreChars()) {
+ resBodyToStore = HttpAuditSupport.storeWithCharLimit(resBodyRaw, resMax);
+ if (HttpAuditSupport.overCharLimit(resBodyRaw, resMax)) {
truncated = 1;
}
}
@@ -102,15 +117,17 @@
}
String appName = environment.getProperty("spring.application.name", "unknown");
+ String path = HttpAuditSupport.safePath(req);
HttpAuditLog logEntity = new HttpAuditLog()
.setServiceName(appName)
.setScopeType(HttpAuditSupport.resolveScope(req, props))
- .setUri(HttpAuditSupport.safePath(req))
+ .setUri(path)
+ .setIoDirection("IN")
.setMethod(req.getMethod())
.setFunctionDesc(HttpAuditSupport.resolveFunctionDesc(req, props))
.setQueryString(req.getQueryString())
- .setRequestBody(reqBody)
+ .setRequestBody(reqStored)
.setResponseBody(resBodyToStore)
.setResponseTruncated(truncated)
.setHttpStatus(status)
diff --git a/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/OutboundHttpAuditInterceptor.java b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/OutboundHttpAuditInterceptor.java
new file mode 100644
index 0000000..b235c10
--- /dev/null
+++ b/rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/OutboundHttpAuditInterceptor.java
@@ -0,0 +1,99 @@
+package com.vincent.rsf.httpaudit.web;
+
+import com.vincent.rsf.httpaudit.model.HttpAuditDecision;
+import com.vincent.rsf.httpaudit.service.HttpAuditOutboundRecorder;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpRequest;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.util.StreamUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * RestTemplate 鍑虹珯瀹¤
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class OutboundHttpAuditInterceptor implements ClientHttpRequestInterceptor {
+
+ private static final String FN_REST = "HTTP鍑虹珯(RestTemplate)";
+
+ private final HttpAuditOutboundRecorder outboundRecorder;
+
+ @Override
+ public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
+ if (!outboundRecorder.isAuditEnabled()) {
+ return execution.execute(request, body);
+ }
+ String url = request.getURI().toString();
+ String method = request.getMethod().name();
+ String reqText = body == null || body.length == 0 ? "" : new String(body, StandardCharsets.UTF_8);
+ HttpAuditDecision dec = outboundRecorder.decideOutbound(url, method, reqText);
+ long t0 = System.currentTimeMillis();
+ ClientHttpResponse raw;
+ try {
+ raw = execution.execute(request, body);
+ } catch (IOException e) {
+ outboundRecorder.saveOutbound(FN_REST, url, method, reqText, dec, null, null, t0, e);
+ throw e;
+ }
+ byte[] respBytes;
+ try {
+ respBytes = StreamUtils.copyToByteArray(raw.getBody());
+ } catch (IOException e) {
+ outboundRecorder.saveOutbound(FN_REST, url, method, reqText, dec, raw.getRawStatusCode(), null, t0, e);
+ throw e;
+ }
+ if (!dec.isAudit()) {
+ return new CopiedBodyClientHttpResponse(raw, respBytes);
+ }
+ String resText = new String(respBytes, StandardCharsets.UTF_8);
+ outboundRecorder.saveOutbound(FN_REST, url, method, reqText, dec, raw.getRawStatusCode(), resText, t0, null);
+ return new CopiedBodyClientHttpResponse(raw, respBytes);
+ }
+
+ private static class CopiedBodyClientHttpResponse implements ClientHttpResponse {
+
+ private final ClientHttpResponse delegate;
+ private final byte[] body;
+
+ CopiedBodyClientHttpResponse(ClientHttpResponse delegate, byte[] body) {
+ this.delegate = delegate;
+ this.body = body != null ? body : new byte[0];
+ }
+
+ @Override
+ public org.springframework.http.HttpStatus getStatusCode() throws IOException {
+ return delegate.getStatusCode();
+ }
+
+ @Override
+ public int getRawStatusCode() throws IOException {
+ return delegate.getRawStatusCode();
+ }
+
+ @Override
+ public String getStatusText() throws IOException {
+ return delegate.getStatusText();
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+
+ @Override
+ public org.springframework.http.HttpHeaders getHeaders() {
+ return delegate.getHeaders();
+ }
+
+ @Override
+ public java.io.InputStream getBody() throws IOException {
+ return new java.io.ByteArrayInputStream(body);
+ }
+ }
+}
diff --git a/rsf-open-api/src/main/resources/application-dev.yml b/rsf-open-api/src/main/resources/application-dev.yml
index 6148deb..02409d8 100644
--- a/rsf-open-api/src/main/resources/application-dev.yml
+++ b/rsf-open-api/src/main/resources/application-dev.yml
@@ -75,5 +75,9 @@
http-audit:
enabled: true
+ whitelist-only: true
+ # false锛�/httpAuditLog銆�/httpAuditRule 涔熶細琚� Filter 璁板綍锛堜笌 rsf-server dev 涓�鑷达級
+ exclude-audit-self-paths: false
+ rule-cache-refresh-ms: 60000
query-response-max-chars: 500
max-response-store-chars: 65535
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/AuditingFeignClient.java b/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/AuditingFeignClient.java
new file mode 100644
index 0000000..23361ce
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/AuditingFeignClient.java
@@ -0,0 +1,72 @@
+package com.vincent.rsf.server.api.feign;
+
+import com.vincent.rsf.httpaudit.model.HttpAuditDecision;
+import com.vincent.rsf.httpaudit.service.HttpAuditOutboundRecorder;
+import feign.Client;
+import feign.Request;
+import feign.Response;
+import feign.Util;
+import lombok.RequiredArgsConstructor;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Feign 鍑虹珯鍐� sys_http_audit_log锛堜笌 RestTemplate 鍏辩敤瑙勫垯涓庢埅鏂級
+ */
+@RequiredArgsConstructor
+public class AuditingFeignClient implements Client {
+
+ private static final String FN_FEIGN = "HTTP鍑虹珯(Feign)";
+
+ private final Client delegate;
+ private final HttpAuditOutboundRecorder outboundRecorder;
+
+ @Override
+ public Response execute(Request request, Request.Options options) throws IOException {
+ if (!outboundRecorder.isAuditEnabled()) {
+ return delegate.execute(request, options);
+ }
+ String url = request.url();
+ String method = request.httpMethod().name();
+ byte[] reqBytes = request.body() == null ? new byte[0] : request.body();
+ String reqText = new String(reqBytes, StandardCharsets.UTF_8);
+ HttpAuditDecision dec = outboundRecorder.decideOutbound(url, method, reqText);
+ long t0 = System.currentTimeMillis();
+ Response resp;
+ try {
+ resp = delegate.execute(request, options);
+ } catch (Throwable t) {
+ outboundRecorder.saveOutbound(FN_FEIGN, url, method, reqText, dec, null, null, t0, t);
+ if (t instanceof IOException) {
+ throw (IOException) t;
+ }
+ if (t instanceof RuntimeException) {
+ throw (RuntimeException) t;
+ }
+ if (t instanceof Error) {
+ throw (Error) t;
+ }
+ throw new IOException(t);
+ }
+ if (!dec.isAudit()) {
+ return resp;
+ }
+ byte[] respBytes;
+ try {
+ respBytes = Util.toByteArray(resp.body().asInputStream());
+ } catch (IOException e) {
+ outboundRecorder.saveOutbound(FN_FEIGN, url, method, reqText, dec, resp.status(), null, t0, e);
+ throw e;
+ }
+ String resText = new String(respBytes, StandardCharsets.UTF_8);
+ outboundRecorder.saveOutbound(FN_FEIGN, url, method, reqText, dec, resp.status(), resText, t0, null);
+ return Response.builder()
+ .status(resp.status())
+ .reason(resp.reason())
+ .headers(resp.headers())
+ .body(respBytes)
+ .request(resp.request())
+ .build();
+ }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/CloudWmsErpFeignClient.java b/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/CloudWmsErpFeignClient.java
index 2d6cd63..70e0f82 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/CloudWmsErpFeignClient.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/CloudWmsErpFeignClient.java
@@ -16,7 +16,8 @@
@FeignClient(
name = "cloudWmsErp",
url = "${platform.erp.base-url:http://127.0.0.1:8080}",
- fallbackFactory = CloudWmsErpFeignClientFallbackFactory.class
+ fallbackFactory = CloudWmsErpFeignClientFallbackFactory.class,
+ configuration = FeignHttpAuditConfiguration.class
)
public interface CloudWmsErpFeignClient {
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/FeignHttpAuditCapability.java b/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/FeignHttpAuditCapability.java
new file mode 100644
index 0000000..d63db92
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/FeignHttpAuditCapability.java
@@ -0,0 +1,20 @@
+package com.vincent.rsf.server.api.feign;
+
+import com.vincent.rsf.httpaudit.service.HttpAuditOutboundRecorder;
+import feign.Capability;
+import feign.Client;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 鐙珛 public 绫伙紝渚� Feign 鍙嶅皠 enrich锛涘尶鍚嶅唴閮ㄧ被鍦� JDK17+ 浼� IllegalAccessException銆�
+ */
+@RequiredArgsConstructor
+public class FeignHttpAuditCapability implements Capability {
+
+ private final HttpAuditOutboundRecorder outboundRecorder;
+
+ @Override
+ public Client enrich(Client client) {
+ return new AuditingFeignClient(client, outboundRecorder);
+ }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/FeignHttpAuditConfiguration.java b/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/FeignHttpAuditConfiguration.java
new file mode 100644
index 0000000..a63870a
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/api/feign/FeignHttpAuditConfiguration.java
@@ -0,0 +1,20 @@
+package com.vincent.rsf.server.api.feign;
+
+import com.vincent.rsf.httpaudit.service.HttpAuditOutboundRecorder;
+import feign.Capability;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Feign 鍑虹珯 HTTP 瀹¤鍏叡閰嶇疆锛涗换鎰� {@code @FeignClient(configuration = FeignHttpAuditConfiguration.class)} 鍗崇敓鏁堛��
+ */
+@Configuration
+public class FeignHttpAuditConfiguration {
+
+ @Bean
+ @ConditionalOnBean(HttpAuditOutboundRecorder.class)
+ public Capability feignHttpAuditCapability(HttpAuditOutboundRecorder outboundRecorder) {
+ return new FeignHttpAuditCapability(outboundRecorder);
+ }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java b/rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java
index bf7e82b..e57fd61 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java
@@ -52,6 +52,7 @@
"sys_role_menu",
"sys_menu",
"sys_http_audit_log",
+ "sys_http_audit_rule",
"man_loc_type_rela",
"man_qly_inspect_result",
"view_stock_manage",
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
index 9b841c4..6745e5d 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
@@ -4,7 +4,6 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.CoercionAction;
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
@@ -56,6 +55,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import java.util.*;
@@ -2219,6 +2219,13 @@
log.error("RCS璧勬簮璁块棶寮傚父锛屼换鍔′笅鍙戝け璐ワ紒浠诲姟缂栫爜锛歿}锛岄敊璇俊鎭細{}", task.getTaskCode(), errorMsg);
}
continue;
+ } catch (HttpStatusCodeException e) {
+ long endTime = System.currentTimeMillis();
+ log.error("========== RCS浠诲姟涓嬪彂寮傚父 ==========");
+ log.error("璇锋眰RCS-HTTP鐘舵�佸紓甯革紝鑰楁椂锛歿}ms锛屼换鍔$紪鐮侊細{}锛宻tatus锛歿}锛宐ody锛歿}", (endTime - startTime), task.getTaskCode(), e.getRawStatusCode(), e.getResponseBodyAsString(), e);
+ log.error("璇锋眰RCS-鍦板潃锛歿}", pubTakUrl);
+ log.error("璇锋眰RCS-鍙傛暟锛歿}", JSONObject.toJSONString(taskParams));
+ continue;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
log.error("========== RCS浠诲姟涓嬪彂寮傚父 ==========");
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HttpAuditRuleController.java b/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HttpAuditRuleController.java
new file mode 100644
index 0000000..dd1fb67
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HttpAuditRuleController.java
@@ -0,0 +1,155 @@
+package com.vincent.rsf.server.system.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.vincent.rsf.framework.common.R;
+import com.vincent.rsf.httpaudit.entity.HttpAuditRule;
+import com.vincent.rsf.httpaudit.service.HttpAuditRuleService;
+import com.vincent.rsf.server.common.domain.BaseParam;
+import com.vincent.rsf.server.common.domain.PageParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+@RestController
+public class HttpAuditRuleController extends BaseController {
+
+ private static final Set<String> RULE_TYPES = new HashSet<>(Arrays.asList(
+ HttpAuditRule.TYPE_URI, HttpAuditRule.TYPE_IP, HttpAuditRule.TYPE_REQUEST_BODY));
+ private static final Set<String> MATCH_MODES = new HashSet<>(Arrays.asList(
+ HttpAuditRule.MODE_EQUAL, HttpAuditRule.MODE_PREFIX, HttpAuditRule.MODE_CONTAINS, HttpAuditRule.MODE_REGEX));
+
+ @Autowired
+ private HttpAuditRuleService httpAuditRuleService;
+
+ @PreAuthorize("hasAuthority('system:httpAuditRule:list')")
+ @PostMapping("/httpAuditRule/page")
+ public R page(@RequestBody Map<String, Object> map) {
+ BaseParam baseParam = buildParam(map, BaseParam.class);
+ PageParam<HttpAuditRule, BaseParam> pageParam = new PageParam<>(baseParam, HttpAuditRule.class);
+ QueryWrapper<HttpAuditRule> wrapper = pageParam.buildWrapper(true, qw -> {
+ qw.orderByAsc("sort_order").orderByAsc("id");
+ }, "create_time");
+ Page<HttpAuditRule> page = httpAuditRuleService.page(pageParam, wrapper);
+ return R.ok().add(page);
+ }
+
+ @PreAuthorize("hasAuthority('system:httpAuditRule:list')")
+ @GetMapping("/httpAuditRule/{id}")
+ public R get(@PathVariable Long id) {
+ return R.ok().add(httpAuditRuleService.getById(id));
+ }
+
+ @PreAuthorize("hasAuthority('system:httpAuditRule:save')")
+ @PostMapping("/httpAuditRule/save")
+ public R save(@RequestBody HttpAuditRule rule) {
+ normalizeRecordAllRule(rule);
+ R err = validate(rule);
+ if (err != null) {
+ return err;
+ }
+ Date now = new Date();
+ if (rule.getEnabled() == null) {
+ rule.setEnabled(1);
+ }
+ if (rule.getSortOrder() == null) {
+ rule.setSortOrder(0);
+ }
+ if (StringUtils.isBlank(rule.getDirection())) {
+ rule.setDirection(HttpAuditRule.DIR_IN);
+ }
+ rule.setCreateTime(now);
+ rule.setUpdateTime(now);
+ if (httpAuditRuleService.save(rule)) {
+ httpAuditRuleService.refreshCache();
+ return R.ok("Save Success").add(rule);
+ }
+ return R.error("Save Fail");
+ }
+
+ @PreAuthorize("hasAuthority('system:httpAuditRule:update')")
+ @PostMapping("/httpAuditRule/update")
+ public R update(@RequestBody HttpAuditRule rule) {
+ normalizeRecordAllRule(rule);
+ R err = validate(rule);
+ if (err != null) {
+ return err;
+ }
+ if (rule.getId() == null) {
+ return R.error("id required");
+ }
+ if (rule.getEnabled() == null) {
+ rule.setEnabled(1);
+ }
+ if (rule.getSortOrder() == null) {
+ rule.setSortOrder(0);
+ }
+ if (StringUtils.isBlank(rule.getDirection())) {
+ rule.setDirection(HttpAuditRule.DIR_IN);
+ }
+ rule.setUpdateTime(new Date());
+ if (httpAuditRuleService.updateById(rule)) {
+ httpAuditRuleService.refreshCache();
+ return R.ok("Update Success").add(rule);
+ }
+ return R.error("Update Fail");
+ }
+
+ @PreAuthorize("hasAuthority('system:httpAuditRule:remove')")
+ @PostMapping("/httpAuditRule/remove/{ids}")
+ public R remove(@PathVariable Long[] ids) {
+ if (httpAuditRuleService.removeByIds(Arrays.asList(ids))) {
+ httpAuditRuleService.refreshCache();
+ return R.ok("Remove Success");
+ }
+ return R.error("Remove Fail");
+ }
+
+ private static void normalizeRecordAllRule(HttpAuditRule rule) {
+ if (rule == null || rule.getRecordAll() == null || rule.getRecordAll() != 1) {
+ return;
+ }
+ if (StringUtils.isBlank(rule.getRuleType())) {
+ rule.setRuleType(HttpAuditRule.TYPE_URI);
+ }
+ if (StringUtils.isBlank(rule.getMatchMode())) {
+ rule.setMatchMode(HttpAuditRule.MODE_EQUAL);
+ }
+ if (StringUtils.isBlank(rule.getPattern())) {
+ rule.setPattern("*");
+ }
+ if (StringUtils.isBlank(rule.getDirection())) {
+ rule.setDirection(HttpAuditRule.DIR_BOTH);
+ }
+ }
+
+ private static R validate(HttpAuditRule rule) {
+ if (rule == null) {
+ return R.error("body required");
+ }
+ if (StringUtils.isBlank(rule.getRuleType()) || !RULE_TYPES.contains(rule.getRuleType())) {
+ return R.error("ruleType invalid");
+ }
+ if (StringUtils.isBlank(rule.getMatchMode()) || !MATCH_MODES.contains(rule.getMatchMode())) {
+ return R.error("matchMode invalid");
+ }
+ if (StringUtils.isBlank(rule.getPattern())) {
+ return R.error("pattern required");
+ }
+ String dir = rule.getDirection();
+ if (StringUtils.isNotBlank(dir)) {
+ if (!Arrays.asList(HttpAuditRule.DIR_IN, HttpAuditRule.DIR_OUT, HttpAuditRule.DIR_BOTH).contains(dir)) {
+ return R.error("direction invalid");
+ }
+ }
+ return null;
+ }
+}
diff --git a/rsf-server/src/main/resources/application-dev.yml b/rsf-server/src/main/resources/application-dev.yml
index 76ca7f2..4d2ccea 100644
--- a/rsf-server/src/main/resources/application-dev.yml
+++ b/rsf-server/src/main/resources/application-dev.yml
@@ -115,10 +115,18 @@
flagReceiving: false
# HTTP 鎺ュ彛瀹¤锛坮sf-http-audit锛屽紩鍏ヤ緷璧栧嵆鐢熸晥锛屽彲 enabled=false 鍏抽棴锛�
+# whitelist-only=true锛氫粎 sys_http_audit_rule 鍛戒腑瑙勫垯鎵嶅啓瀹¤锛涙棤瑙勫垯鏃朵笉钀藉簱銆俧alse锛氭帓闄よ矾寰勫鍏ㄩ噺璁板綍銆�
+# rule-cache-refresh-ms锛氳鍒欒〃缂撳瓨鍒锋柊闂撮殧锛堟绉掞級
http-audit:
enabled: true
+ whitelist-only: true
+ # false锛�/httpAuditLog銆�/httpAuditRule 涔熶細琚� Filter 璁板綍锛堣皟璇曠敤锛涚敓浜у缓璁� true锛�
+ exclude-audit-self-paths: false
+ rule-cache-refresh-ms: 60000
query-response-max-chars: 500
max-response-store-chars: 65535
+ # 瑙勫垯鏈~ request_max_chars 鏃堕粯璁わ紱-1 琛ㄧず璇锋眰浣撳叆搴撲笉鎴柇
+ default-request-store-chars: 65535
path-descriptions:
"/erp/order": "浜戜粨-璁㈠崟鏌ヨ"
"/erp/order/add": "浜戜粨-鍗曟嵁涓嬪彂"
diff --git a/version/db/http_audit_menu.sql b/version/db/http_audit_menu.sql
index 9ce0b4f..147c9b1 100644
--- a/version/db/http_audit_menu.sql
+++ b/version/db/http_audit_menu.sql
@@ -1,22 +1,22 @@
--- HTTP 鎺ュ彛瀹¤鑿滃崟锛堢郴缁熺鐞嗕笅锛屼笌鎿嶄綔鏃ュ織鍚岀骇锛夛紱鎵ц鍓嶈纭 id 210-212 鏈鍗犵敤
+-- HTTP 鎺ュ彛瀹¤鑿滃崟锛堢郴缁熺鐞嗕笅锛屼笌鎿嶄綔鏃ュ織鍚岀骇锛夛紱鎵ц鍓嶈纭 id 390-392 鏈鍗犵敤
SET NAMES utf8mb4;
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
-SELECT 210, 'menu.httpAuditLog', 1, 'menu.system', '1,210', 'menu.httpAuditLog', '/system/httpAuditLog', 'httpAuditLog', NULL, NULL, 0, NULL, 'Http', 6, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
-FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 210);
+SELECT 390, 'menu.httpAuditLog', 1, 'menu.system', '1,390', 'menu.httpAuditLog', '/system/httpAuditLog', 'httpAuditLog', NULL, NULL, 0, NULL, 'Http', 6, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 390);
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
-SELECT 211, 'Query HttpAuditLog', 210, '', '1,210,211', NULL, NULL, NULL, NULL, NULL, 1, 'system:httpAuditLog:list', NULL, 0, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
-FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 211);
+SELECT 391, 'Query HttpAuditLog', 390, '', '1,390,391', NULL, NULL, NULL, NULL, NULL, 1, 'system:httpAuditLog:list', NULL, 0, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 391);
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
-SELECT 212, 'Delete HttpAuditLog', 210, '', '1,210,212', NULL, NULL, NULL, NULL, NULL, 1, 'system:httpAuditLog:remove', NULL, 1, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
-FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 212);
+SELECT 392, 'Delete HttpAuditLog', 390, '', '1,390,392', NULL, NULL, NULL, NULL, NULL, 1, 'system:httpAuditLog:remove', NULL, 1, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 392);
-- 瓒呯骇绠$悊鍛樿鑹�(role_id=1)鎺堟潈鑿滃崟锛堣嫢宸插瓨鍦ㄥ垯璺宠繃锛�
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
-SELECT 1, 210 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 210);
+SELECT 1, 390 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 390);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
-SELECT 1, 211 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 211);
+SELECT 1, 391 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 391);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
-SELECT 1, 212 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 212);
+SELECT 1, 392 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 392);
diff --git a/version/db/sys_http_audit_log.sql b/version/db/sys_http_audit_log.sql
index d2ed6d9..85bf657 100644
--- a/version/db/sys_http_audit_log.sql
+++ b/version/db/sys_http_audit_log.sql
@@ -5,7 +5,8 @@
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '涓婚敭',
`service_name` varchar(64) DEFAULT NULL COMMENT '搴旂敤 spring.application.name',
`scope_type` varchar(16) NOT NULL COMMENT 'EXTERNAL 澶栭儴 / INTERNAL 鍐呴儴',
- `uri` varchar(512) NOT NULL COMMENT '璇锋眰璺緞',
+ `uri` varchar(512) NOT NULL COMMENT '璺緞鎴栧嚭绔欏畬鏁碪RL',
+ `io_direction` varchar(8) DEFAULT 'IN' COMMENT 'IN鍏ョ珯 OUT鍑虹珯',
`method` varchar(16) DEFAULT NULL COMMENT 'HTTP 鏂规硶',
`function_desc` varchar(255) DEFAULT NULL COMMENT '鍔熻兘鎻忚堪',
`query_string` varchar(2048) DEFAULT NULL COMMENT 'QueryString',
@@ -22,5 +23,6 @@
PRIMARY KEY (`id`),
KEY `idx_create_time` (`create_time`),
KEY `idx_uri` (`uri`(191)),
+ KEY `idx_io_direction` (`io_direction`),
KEY `idx_ok_client` (`ok_flag`,`client_ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='HTTP鎺ュ彛瀹¤';
--
Gitblit v1.9.1