| | |
| | | ...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", |
| | |
| | | token: 'Token', |
| | | operation: 'Operation', |
| | | httpAuditLog: 'HTTP audit', |
| | | httpAuditRule: 'HTTP audit rules', |
| | | config: 'Config', |
| | | tenant: 'Tenant', |
| | | userLogin: 'Token', |
| | |
| | | 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", |
| | |
| | | ...chineseMessages, |
| | | hello: '你好世界', |
| | | 'menu.httpAuditLog': 'HTTP接口审计', |
| | | 'menu.httpAuditRule': 'HTTP审计规则', |
| | | resources: { |
| | | config: { name: '配置参数' }, |
| | | httpAuditLog: { name: 'HTTP接口审计' }, |
| | | httpAuditRule: { name: 'HTTP审计规则', createTitle: '新增审计规则' }, |
| | | asnOrderItem: { name: '收货明细' }, |
| | | outStockItem: { name: '出库单明细' }, |
| | | }, |
| | |
| | | token: '登录日志', |
| | | operation: '操作日志', |
| | | httpAuditLog: 'HTTP接口审计', |
| | | httpAuditRule: 'HTTP审计规则', |
| | | config: '配置参数', |
| | | tenant: '租户管理', |
| | | userLogin: '登录日志', |
| | |
| | | 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: "命名空间", |
| | |
| | | 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) { |
| | |
| | | return openApiApp; |
| | | case "httpAuditLog": |
| | | return httpAuditLog; |
| | | case "httpAuditRule": |
| | | return httpAuditRule; |
| | | default: |
| | | return { |
| | | list: ListGuesser, |
| | |
| | | { 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" />, |
| | | ]; |
| | |
| | | <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" /> |
| | |
| | | <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" /> |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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 || '', |
| | | }; |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | */ |
| | | @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") |
| | |
| | | |
| | | @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("/*"); |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | /** EXTERNAL-外部;INTERNAL-内部 */ |
| | | private String scopeType; |
| | | |
| | | /** 请求路径(不含域名) */ |
| | | /** 入站路径或出站完整 URL */ |
| | | private String uri; |
| | | |
| | | /** IN 入站 / OUT 出站 */ |
| | | private String ioDirection; |
| | | |
| | | private String method; |
| | | |
| | | /** 功能说明(来自配置最长前缀匹配) */ |
| New file |
| | |
| | | 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; |
| | | } |
| New file |
| | |
| | | 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> { |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | |
| | | |
| | | 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; |
| | |
| | | |
| | | /** 不落库的路径前缀 */ |
| | | 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(); |
| | |
| | | list.add("/favicon.ico"); |
| | | list.add("/static/"); |
| | | list.add("/httpAuditLog"); |
| | | list.add("/httpAuditRule"); |
| | | return list; |
| | | } |
| | | |
| New file |
| | |
| | | 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()); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 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(); |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | |
| | | 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; |
| | | } |
| | |
| | | } |
| | | 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) + "..."; |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | private final HttpAuditAsyncRecorder recorder; |
| | | private final HttpAuditProperties props; |
| | | private final Environment environment; |
| | | private final HttpAuditRuleService httpAuditRuleService; |
| | | |
| | | @Override |
| | | protected boolean shouldNotFilter(HttpServletRequest request) { |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | 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) |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | 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 |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| | |
| | | @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 { |
| | | |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | |
| | | "sys_role_menu", |
| | | "sys_menu", |
| | | "sys_http_audit_log", |
| | | "sys_http_audit_rule", |
| | | "man_loc_type_rela", |
| | | "man_qly_inspect_result", |
| | | "view_stock_manage", |
| | |
| | | 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; |
| | |
| | | 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.*; |
| | |
| | | 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任务下发异常 =========="); |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | 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": "云仓-单据下发" |
| | |
| | | -- 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); |
| | |
| | | `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', |
| | |
| | | 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接口审计'; |