cl
11 小时以前 c4bba32b20f0869b45ed14be04543869dd91ee6c
日志1
16个文件已添加
17个文件已修改
1514 ■■■■■ 已修改文件
rsf-admin/src/i18n/en.js 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/ResourceContent.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditLog/HttpAuditLogList.jsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditLog/HttpAuditLogShow.jsx 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleCreate.jsx 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleEdit.jsx 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleList.jsx 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/httpAuditRule/index.jsx 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAutoConfiguration.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditRestTemplateBeanPostProcessor.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditLog.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditRule.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/mapper/HttpAuditRuleMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/model/HttpAuditDecision.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/props/HttpAuditProperties.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditOutboundRecorder.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleService.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleServiceImpl.java 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/support/HttpAuditSupport.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/HttpAuditFilter.java 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/OutboundHttpAuditInterceptor.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/resources/application-dev.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/AuditingFeignClient.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/CloudWmsErpFeignClient.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/FeignHttpAuditCapability.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/FeignHttpAuditConfiguration.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HttpAuditRuleController.java 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/application-dev.yml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/http_audit_menu.sql 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/sys_http_audit_log.sql 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
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",
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: "正则",
                },
                pattern: "匹配内容",
                enabled: "启用",
                enabledOn: "启用",
                enabledOff: "停用",
                sortOrder: "排序",
                remark: "备注",
            },
            operationRecord: {
                namespace: "命名空间",
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,
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" />
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" />
rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleCreate.jsx
New file
@@ -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;
rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleEdit.jsx
New file
@@ -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;
rsf-admin/src/page/system/httpAuditRule/HttpAuditRuleList.jsx
New file
@@ -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;
rsf-admin/src/page/system/httpAuditRule/index.jsx
New file
@@ -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 || '',
};
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("/*");
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditRestTemplateBeanPostProcessor.java
New file
@@ -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;
    }
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditLog.java
@@ -29,9 +29,12 @@
    /** EXTERNAL-外部;INTERNAL-内部 */
    private String scopeType;
    /** 请求路径(不含域名) */
    /** 入站路径或出站完整 URL */
    private String uri;
    /** IN 入站 / OUT 出站 */
    private String ioDirection;
    private String method;
    /** 功能说明(来自配置最长前缀匹配) */
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/entity/HttpAuditRule.java
New file
@@ -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;REQUEST_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;
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/mapper/HttpAuditRuleMapper.java
New file
@@ -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> {
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/model/HttpAuditDecision.java
New file
@@ -0,0 +1,23 @@
package com.vincent.rsf.httpaudit.model;
import lombok.Value;
/**
 * 是否写入审计及规则上的请求/响应截断(-1 全量,null 用 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);
    }
}
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、截断长度);false:排除路径外入站与全部出站均记录,截断用本配置 + 规则中「全量」行的 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;
    }
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditOutboundRecorder.java
New file
@@ -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 审计落库(RestTemplate / 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());
        }
    }
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleService.java
New file
@@ -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 是否记及截断长度;fullUrl 为完整请求 URL */
    HttpAuditDecision decideOutbound(String fullUrl, String method, String requestBody);
    /** 重载规则缓存(保存/修改/删除后调用) */
    void refreshCache();
}
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleServiceImpl.java
New file
@@ -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;
    }
}
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&lt;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) + "...";
    }
}
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)
rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/web/OutboundHttpAuditInterceptor.java
New file
@@ -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);
        }
    }
}
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
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/AuditingFeignClient.java
New file
@@ -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();
    }
}
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 {
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/FeignHttpAuditCapability.java
New file
@@ -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);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/FeignHttpAuditConfiguration.java
New file
@@ -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);
    }
}
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",
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,任务编码:{},status:{},body:{}", (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任务下发异常 ==========");
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HttpAuditRuleController.java
New file
@@ -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;
    }
}
rsf-server/src/main/resources/application-dev.yml
@@ -115,10 +115,18 @@
    flagReceiving: false
# HTTP 接口审计(rsf-http-audit,引入依赖即生效,可 enabled=false 关闭)
# whitelist-only=true:仅 sys_http_audit_rule 命中规则才写审计;无规则时不落库。false:排除路径外全量记录。
# 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": "云仓-单据下发"
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);
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 '路径或出站完整URL',
  `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接口审计';