rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/config/HttpAuditAutoConfiguration.java
@@ -1,9 +1,12 @@ package com.vincent.rsf.httpaudit.config; import com.vincent.rsf.httpaudit.mapper.HttpAuditLogMapper; import com.vincent.rsf.httpaudit.mapper.HttpAuditConfigMapper; 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.HttpAuditCleanupService; import com.vincent.rsf.httpaudit.service.HttpAuditDbConfigService; import com.vincent.rsf.httpaudit.service.HttpAuditOutboundRecorder; import com.vincent.rsf.httpaudit.service.HttpAuditRuleService; import com.vincent.rsf.httpaudit.service.HttpAuditRuleServiceImpl; @@ -45,6 +48,16 @@ } @Bean public HttpAuditCleanupService httpAuditCleanupService(HttpAuditLogMapper httpAuditLogMapper, HttpAuditProperties props) { return new HttpAuditCleanupService(httpAuditLogMapper, props); } @Bean public HttpAuditDbConfigService httpAuditDbConfigService(HttpAuditConfigMapper httpAuditConfigMapper) { return new HttpAuditDbConfigService(httpAuditConfigMapper); } @Bean public HttpAuditOutboundRecorder httpAuditOutboundRecorder( HttpAuditAsyncRecorder recorder, HttpAuditProperties props, rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/mapper/HttpAuditConfigMapper.java
New file @@ -0,0 +1,15 @@ package com.vincent.rsf.httpaudit.mapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; import java.util.Map; @Mapper public interface HttpAuditConfigMapper { @Select("SELECT config_key, config_val FROM sys_http_audit_config WHERE deleted = 0 AND enabled = 1") List<Map<String, Object>> listEnabledConfig(); } rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/props/HttpAuditDbConfigHolder.java
New file @@ -0,0 +1,88 @@ package com.vincent.rsf.httpaudit.props; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Collections; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * 审计配置缓存 */ public final class HttpAuditDbConfigHolder { public static final String KEY_WHITELIST_ONLY = "whitelist-only"; public static final String KEY_EXCLUDE_AUDIT_SELF_PATHS = "exclude-audit-self-paths"; public static final String KEY_RULE_CACHE_REFRESH_MS = "rule-cache-refresh-ms"; public static final String KEY_QUERY_RESPONSE_MAX_CHARS = "query-response-max-chars"; public static final String KEY_MAX_RESPONSE_STORE_CHARS = "max-response-store-chars"; public static final String KEY_DEFAULT_REQUEST_STORE_CHARS = "default-request-store-chars"; public static final String KEY_PATH_DESCRIPTIONS = "path-descriptions"; public static final String KEY_CLEANUP_ENABLED = "cleanup-enabled"; public static final String KEY_CLEANUP_RETENTION_DAYS = "cleanup-retention-days"; private static final ConcurrentHashMap<String, String> CONFIG = new ConcurrentHashMap<>(); private static final ObjectMapper MAPPER = new ObjectMapper(); private HttpAuditDbConfigHolder() { } public static void replace(Map<String, String> map) { CONFIG.clear(); if (map != null && !map.isEmpty()) { CONFIG.putAll(map); } } public static boolean getBoolean(String key, boolean defaultValue) { String raw = CONFIG.get(key); if (raw == null) { return defaultValue; } return "1".equals(raw) || "true".equalsIgnoreCase(raw.trim()); } public static int getInt(String key, int defaultValue) { String raw = CONFIG.get(key); if (raw == null) { return defaultValue; } try { return Integer.parseInt(raw.trim()); } catch (Exception ignore) { return defaultValue; } } public static long getLong(String key, long defaultValue) { String raw = CONFIG.get(key); if (raw == null) { return defaultValue; } try { return Long.parseLong(raw.trim()); } catch (Exception ignore) { return defaultValue; } } public static Map<String, String> getPathDescriptions(Map<String, String> defaultValue) { String raw = CONFIG.get(KEY_PATH_DESCRIPTIONS); if (raw == null || raw.trim().isEmpty()) { return defaultValue; } try { Map<String, String> parsed = MAPPER.readValue(raw, new TypeReference<Map<String, String>>() { }); return parsed == null ? defaultValue : parsed; } catch (Exception ignore) { return defaultValue; } } public static Map<String, String> snapshot() { return Collections.unmodifiableMap(CONFIG); } } rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/props/HttpAuditProperties.java
@@ -25,6 +25,11 @@ /** 规则缓存定时刷新间隔(毫秒) */ private long ruleCacheRefreshMs = 60_000L; /** 定时清理开关 */ private boolean cleanupEnabled = true; /** 保留天数 */ private int cleanupRetentionDays = 180; /** 查询类响应最多保留字符数 */ private int queryResponseMaxChars = 500; @@ -53,7 +58,7 @@ /** Filter 实际使用的前缀(受 excludeAuditSelfPaths 影响) */ public List<String> getEffectiveExcludePrefixes() { List<String> list = excludePathPrefixes == null ? new ArrayList<>() : new ArrayList<>(excludePathPrefixes); if (!excludeAuditSelfPaths) { if (!isExcludeAuditSelfPaths()) { list.removeIf(p -> "/httpAuditLog".equals(p) || "/httpAuditRule".equals(p)); } return list; @@ -65,6 +70,42 @@ /** 路径 -> 功能描述(按最长路径前缀匹配) */ private Map<String, String> pathDescriptions = new LinkedHashMap<>(); public boolean isWhitelistOnly() { return HttpAuditDbConfigHolder.getBoolean(HttpAuditDbConfigHolder.KEY_WHITELIST_ONLY, whitelistOnly); } public boolean isExcludeAuditSelfPaths() { return HttpAuditDbConfigHolder.getBoolean(HttpAuditDbConfigHolder.KEY_EXCLUDE_AUDIT_SELF_PATHS, excludeAuditSelfPaths); } public long getRuleCacheRefreshMs() { return HttpAuditDbConfigHolder.getLong(HttpAuditDbConfigHolder.KEY_RULE_CACHE_REFRESH_MS, ruleCacheRefreshMs); } public int getQueryResponseMaxChars() { return HttpAuditDbConfigHolder.getInt(HttpAuditDbConfigHolder.KEY_QUERY_RESPONSE_MAX_CHARS, queryResponseMaxChars); } public int getMaxResponseStoreChars() { return HttpAuditDbConfigHolder.getInt(HttpAuditDbConfigHolder.KEY_MAX_RESPONSE_STORE_CHARS, maxResponseStoreChars); } public int getDefaultRequestStoreChars() { return HttpAuditDbConfigHolder.getInt(HttpAuditDbConfigHolder.KEY_DEFAULT_REQUEST_STORE_CHARS, defaultRequestStoreChars); } public boolean isCleanupEnabled() { return HttpAuditDbConfigHolder.getBoolean(HttpAuditDbConfigHolder.KEY_CLEANUP_ENABLED, cleanupEnabled); } public int getCleanupRetentionDays() { return HttpAuditDbConfigHolder.getInt(HttpAuditDbConfigHolder.KEY_CLEANUP_RETENTION_DAYS, cleanupRetentionDays); } public Map<String, String> getPathDescriptions() { return HttpAuditDbConfigHolder.getPathDescriptions(pathDescriptions); } private static List<String> defaultExcludes() { List<String> list = new ArrayList<>(); list.add("/actuator"); rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditCleanupService.java
New file @@ -0,0 +1,51 @@ package com.vincent.rsf.httpaudit.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.vincent.rsf.httpaudit.entity.HttpAuditLog; import com.vincent.rsf.httpaudit.mapper.HttpAuditLogMapper; import com.vincent.rsf.httpaudit.props.HttpAuditProperties; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; /** * 审计日志定期清理 */ @Slf4j public class HttpAuditCleanupService { private final HttpAuditLogMapper httpAuditLogMapper; private final HttpAuditProperties props; public HttpAuditCleanupService(HttpAuditLogMapper httpAuditLogMapper, HttpAuditProperties props) { this.httpAuditLogMapper = httpAuditLogMapper; this.props = props; } @Scheduled(cron = "${http-audit.cleanup-cron:0 30 2 * * ?}") public void cleanup() { if (!props.isCleanupEnabled()) { return; } int retentionDays = props.getCleanupRetentionDays(); if (retentionDays <= 0) { log.warn("http-audit 清理已跳过,retentionDays 配置无效:{}", retentionDays); return; } try { LocalDateTime cutoff = LocalDateTime.now().minusDays(retentionDays); Date cutoffDate = Date.from(cutoff.atZone(ZoneId.systemDefault()).toInstant()); int count = httpAuditLogMapper.delete(new LambdaQueryWrapper<HttpAuditLog>() .lt(HttpAuditLog::getCreateTime, cutoffDate)); if (count > 0) { log.info("http-audit 清理完成,删除 {} 条,保留天数 {}", count, retentionDays); } } catch (Exception e) { log.warn("http-audit 清理失败", e); } } } rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditDbConfigService.java
New file @@ -0,0 +1,53 @@ package com.vincent.rsf.httpaudit.service; import com.vincent.rsf.httpaudit.mapper.HttpAuditConfigMapper; import com.vincent.rsf.httpaudit.props.HttpAuditDbConfigHolder; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import javax.annotation.PostConstruct; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 审计配置刷新 */ @Slf4j public class HttpAuditDbConfigService { private final HttpAuditConfigMapper httpAuditConfigMapper; public HttpAuditDbConfigService(HttpAuditConfigMapper httpAuditConfigMapper) { this.httpAuditConfigMapper = httpAuditConfigMapper; } @PostConstruct public void init() { refresh(); } @Scheduled(fixedDelayString = "${http-audit.db-config-refresh-ms:60000}") public void refresh() { try { List<Map<String, Object>> rows = httpAuditConfigMapper.listEnabledConfig(); Map<String, String> map = new HashMap<>(); for (Map<String, Object> row : rows) { Object keyObj = row.get("config_key"); if (keyObj == null) { continue; } String key = String.valueOf(keyObj).trim(); if (key.isEmpty()) { continue; } Object valObj = row.get("config_val"); map.put(key, valObj == null ? "" : String.valueOf(valObj)); } HttpAuditDbConfigHolder.replace(map); } catch (Exception e) { log.warn("http-audit 配置刷新失败,使用内存/默认配置", e); } } } rsf-http-audit/src/main/java/com/vincent/rsf/httpaudit/service/HttpAuditRuleServiceImpl.java
@@ -25,6 +25,7 @@ private final HttpAuditProperties props; private final CopyOnWriteArrayList<HttpAuditRule> cache = new CopyOnWriteArrayList<>(); private volatile long lastRefreshAt = 0L; public HttpAuditRuleServiceImpl(HttpAuditRuleMapper mapper, HttpAuditProperties props) { this.baseMapper = mapper; @@ -37,8 +38,14 @@ } @Override @Scheduled(fixedDelayString = "${http-audit.rule-cache-refresh-ms:60000}") @Scheduled(fixedDelay = 5000) public void refreshCache() { long now = System.currentTimeMillis(); long interval = Math.max(5000L, props.getRuleCacheRefreshMs()); if (now - lastRefreshAt < interval) { return; } lastRefreshAt = now; try { List<HttpAuditRule> list = list(new LambdaQueryWrapper<HttpAuditRule>() .eq(HttpAuditRule::getDeleted, 0) rsf-server/src/main/resources/application-dev.yml
@@ -143,17 +143,4 @@ # enabled: true enabled: false # 审计数据源:primary / cus-item-sync datasource: primary 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": "云仓-单据下发" "/erp/order/addAll": "云仓-批量单据下发" "/erp/order/cancel": "云仓-取消单据" datasource: primary rsf-server/src/main/resources/application-prod.yml
@@ -137,17 +137,4 @@ http-audit: enabled: true # 审计数据源:primary / cus-item-sync datasource: primary 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": "云仓-单据下发" "/erp/order/addAll": "云仓-批量单据下发" "/erp/order/cancel": "云仓-取消单据" datasource: primary