| | |
| | | <groupId>org.slf4j</groupId> |
| | | <artifactId>slf4j-api</artifactId> |
| | | </dependency> |
| | | <!-- |
| | | <dependency> |
| | | <groupId>com.vincent</groupId> |
| | | <artifactId>rsf-framework</artifactId> |
| | | <version>1.0.0</version> |
| | | </dependency> |
| | | --> |
| | | <dependency> |
| | | <groupId>org.springframework.security</groupId> |
| | | <artifactId>spring-security-core</artifactId> |
| | |
| | | <groupId>org.apache.commons</groupId> |
| | | <artifactId>commons-lang3</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.opensearch.client</groupId> |
| | | <artifactId>opensearch-rest-client</artifactId> |
| | | <version>2.19.1</version> |
| | | </dependency> |
| | | </dependencies> |
| | | <build> |
| | | <plugins> |
| | |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.vincent.rsf.framework.common.Cools; |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.httpaudit.common.Cools; |
| | | import com.vincent.rsf.httpaudit.common.R; |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditLog; |
| | | import com.vincent.rsf.httpaudit.service.HttpAuditLogCrudService; |
| | | import com.vincent.rsf.httpaudit.web.util.HttpAuditAdminQueryHelper; |
| | |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.vincent.rsf.framework.common.Cools; |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.httpaudit.common.Cools; |
| | | import com.vincent.rsf.httpaudit.common.R; |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditRule; |
| | | import com.vincent.rsf.httpaudit.service.HttpAuditRuleService; |
| | | import com.vincent.rsf.httpaudit.web.util.HttpAuditAdminQueryHelper; |
| | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.vincent.rsf.framework.common.Cools; |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.httpaudit.common.Cools; |
| | | import com.vincent.rsf.httpaudit.common.R; |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditSysConfig; |
| | | import com.vincent.rsf.httpaudit.service.HttpAuditDbConfigService; |
| | | import com.vincent.rsf.httpaudit.service.HttpAuditSysConfigService; |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.common; |
| | | |
| | | public interface BaseRes { |
| | | |
| | | String OK = "200-Success"; |
| | | String EMPTY = "201-Empty Data"; |
| | | String LIMIT = "202-No Authority"; |
| | | String PARAM = "203-Parameters Cannot Be Empty"; |
| | | String DENIED = "403-Please Re-Login"; |
| | | String REPEAT = "407-Already Exist"; |
| | | String NO_ACTIVATION = "409-Please Activate The System First"; |
| | | String ERROR = "500-Internal Server Error"; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.common; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | public final class Cools { |
| | | |
| | | private Cools() { |
| | | } |
| | | |
| | | public static boolean isEmpty(Object... objects) { |
| | | for (Object obj : objects) { |
| | | if (isEmpty(obj)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | @SuppressWarnings("rawtypes") |
| | | public static boolean isEmpty(Object o) { |
| | | if (o == null) { |
| | | return true; |
| | | } |
| | | if (o instanceof String) { |
| | | if (o.toString().trim().equals("")) { |
| | | return true; |
| | | } |
| | | } else if (o instanceof List) { |
| | | if (((List) o).size() == 0) { |
| | | return true; |
| | | } |
| | | } else if (o instanceof Map) { |
| | | if (((Map) o).size() == 0) { |
| | | return true; |
| | | } |
| | | } else if (o instanceof Set) { |
| | | if (((Set) o).size() == 0) { |
| | | return true; |
| | | } |
| | | } else if (o instanceof Object[]) { |
| | | if (((Object[]) o).length == 0) { |
| | | return true; |
| | | } |
| | | } else if (o instanceof int[]) { |
| | | if (((int[]) o).length == 0) { |
| | | return true; |
| | | } |
| | | } else if (o instanceof long[]) { |
| | | if (((long[]) o).length == 0) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.common; |
| | | |
| | | import java.util.HashMap; |
| | | |
| | | public class R extends HashMap<String, Object> { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | private static final String CODE = "code"; |
| | | private static final String MSG = "msg"; |
| | | private static final String DATA = "data"; |
| | | |
| | | public R(Integer code, String msg) { |
| | | super.put(CODE, code); |
| | | super.put(MSG, msg); |
| | | } |
| | | |
| | | public static R ok() { |
| | | return parse(BaseRes.OK); |
| | | } |
| | | |
| | | public static R ok(String msg) { |
| | | R r = ok(); |
| | | r.put(MSG, msg); |
| | | return r; |
| | | } |
| | | |
| | | public static R ok(Object obj) { |
| | | return parse(BaseRes.OK).add(obj); |
| | | } |
| | | |
| | | public static R error() { |
| | | return parse(BaseRes.ERROR); |
| | | } |
| | | |
| | | public static R error(String msg) { |
| | | R r = error(); |
| | | r.put(MSG, msg); |
| | | return r; |
| | | } |
| | | |
| | | public R add(Object obj) { |
| | | this.put(DATA, obj); |
| | | return this; |
| | | } |
| | | |
| | | public static R parse(String message) { |
| | | if (Cools.isEmpty(message)) { |
| | | return parse(BaseRes.ERROR); |
| | | } |
| | | String[] msg = message.split("-"); |
| | | if (msg.length == 2) { |
| | | return new R(Integer.parseInt(msg[0].replaceAll(" ", "")), msg[1]); |
| | | } else { |
| | | return parse("500-".concat(message)); |
| | | } |
| | | } |
| | | } |
| | |
| | | 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.HttpAuditLogCrudService; |
| | | import com.vincent.rsf.httpaudit.service.HttpAuditLogCrudServiceImpl; |
| | | import com.vincent.rsf.httpaudit.service.HttpAuditLogSink; |
| | | 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.service.HttpAuditSysConfigService; |
| | | import com.vincent.rsf.httpaudit.service.HttpAuditSysConfigServiceImpl; |
| | | import com.vincent.rsf.httpaudit.service.MysqlHttpAuditLogSink; |
| | | import com.vincent.rsf.httpaudit.service.OpenSearchHttpAuditLogSink; |
| | | import com.vincent.rsf.httpaudit.web.HttpAuditFilter; |
| | | import com.vincent.rsf.httpaudit.web.OutboundHttpAuditInterceptor; |
| | | import org.mybatis.spring.annotation.MapperScan; |
| | | import org.opensearch.client.RestClient; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| | |
| | | import org.springframework.core.env.Environment; |
| | | import org.springframework.scheduling.annotation.EnableAsync; |
| | | import org.springframework.scheduling.annotation.EnableScheduling; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.concurrent.Executor; |
| | | |
| | | /** |
| | |
| | | @EnableConfigurationProperties(HttpAuditProperties.class) |
| | | @ConditionalOnProperty(prefix = "http-audit", name = "enabled", havingValue = "true", matchIfMissing = true) |
| | | @MapperScan("com.vincent.rsf.httpaudit.mapper") |
| | | @Import({HttpAuditAdminApiAutoConfiguration.class, HttpAuditOpenUiAutoConfiguration.class}) |
| | | @Import({HttpAuditAdminApiAutoConfiguration.class, HttpAuditOpenUiAutoConfiguration.class, HttpAuditOpenSearchConfiguration.class}) |
| | | public class HttpAuditAutoConfiguration { |
| | | |
| | | private static final Logger log = LoggerFactory.getLogger(HttpAuditAutoConfiguration.class); |
| | | |
| | | @Bean |
| | | public HttpAuditSysConfigService httpAuditSysConfigService(HttpAuditConfigMapper httpAuditConfigMapper) { |
| | |
| | | } |
| | | |
| | | @Bean |
| | | public HttpAuditAsyncRecorder httpAuditAsyncRecorder(HttpAuditLogMapper httpAuditLogMapper) { |
| | | return new HttpAuditAsyncRecorder(httpAuditLogMapper); |
| | | public HttpAuditAsyncRecorder httpAuditAsyncRecorder( |
| | | HttpAuditProperties props, |
| | | HttpAuditLogMapper httpAuditLogMapper, |
| | | @Autowired(required = false) OpenSearchHttpAuditLogSink openSearchHttpAuditLogSink) { |
| | | List<HttpAuditLogSink> sinks = new ArrayList<>(); |
| | | if (props.usesMysqlLogStorage()) { |
| | | sinks.add(new MysqlHttpAuditLogSink(httpAuditLogMapper)); |
| | | } |
| | | if (props.usesOpenSearchLogStorage()) { |
| | | if (openSearchHttpAuditLogSink == null) { |
| | | log.warn("http_audit_warn code=opensearch_sink_missing log_storage_mode={}", props.resolveLogStorageMode()); |
| | | } else { |
| | | sinks.add(openSearchHttpAuditLogSink); |
| | | } |
| | | } |
| | | if (sinks.isEmpty()) { |
| | | log.warn("http_audit_warn code=no_log_sinks log_storage_mode={}", props.resolveLogStorageMode()); |
| | | } |
| | | return new HttpAuditAsyncRecorder(sinks); |
| | | } |
| | | |
| | | @Bean |
| | | public HttpAuditCleanupService httpAuditCleanupService(HttpAuditLogMapper httpAuditLogMapper, HttpAuditProperties props) { |
| | | return new HttpAuditCleanupService(httpAuditLogMapper, props); |
| | | public HttpAuditCleanupService httpAuditCleanupService( |
| | | HttpAuditLogMapper httpAuditLogMapper, |
| | | HttpAuditProperties props, |
| | | @Autowired(required = false) @Qualifier("httpAuditOpenSearchRestClient") RestClient httpAuditOpenSearchRestClient) { |
| | | return new HttpAuditCleanupService(httpAuditLogMapper, props, httpAuditOpenSearchRestClient); |
| | | } |
| | | |
| | | @Bean |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.config; |
| | | |
| | | import org.springframework.boot.SpringApplication; |
| | | import org.springframework.boot.env.EnvironmentPostProcessor; |
| | | import org.springframework.boot.env.YamlPropertySourceLoader; |
| | | import org.springframework.core.Ordered; |
| | | import org.springframework.core.env.ConfigurableEnvironment; |
| | | import org.springframework.core.env.PropertySource; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.core.io.ClassPathResource; |
| | | import org.springframework.core.io.Resource; |
| | | |
| | | import java.io.IOException; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * jar 内默认配置,最低优先级;引用工程覆盖。 |
| | | */ |
| | | public class HttpAuditJarDefaultsEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { |
| | | |
| | | private static final Logger log = LoggerFactory.getLogger(HttpAuditJarDefaultsEnvironmentPostProcessor.class); |
| | | |
| | | private static final String RESOURCE = "META-INF/rsf-http-audit/defaults.yml"; |
| | | private static final String NAME = "httpAuditJarDefaults"; |
| | | |
| | | @Override |
| | | public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { |
| | | if (environment.getPropertySources().contains(NAME)) { |
| | | return; |
| | | } |
| | | Resource resource = new ClassPathResource(RESOURCE); |
| | | if (!resource.exists()) { |
| | | return; |
| | | } |
| | | try { |
| | | YamlPropertySourceLoader loader = new YamlPropertySourceLoader(); |
| | | List<PropertySource<?>> loaded = loader.load(NAME, resource); |
| | | for (PropertySource<?> ps : loaded) { |
| | | environment.getPropertySources().addLast(ps); |
| | | } |
| | | } catch (IOException e) { |
| | | log.warn("http_audit_warn code=jar_defaults_load_failed", e); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public int getOrder() { |
| | | return Ordered.LOWEST_PRECEDENCE; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.config; |
| | | |
| | | import com.vincent.rsf.httpaudit.props.HttpAuditProperties; |
| | | import com.vincent.rsf.httpaudit.service.OpenSearchHttpAuditLogSink; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.apache.http.HttpHost; |
| | | import org.apache.http.auth.AuthScope; |
| | | import org.apache.http.auth.UsernamePasswordCredentials; |
| | | import org.apache.http.client.CredentialsProvider; |
| | | import org.apache.http.impl.client.BasicCredentialsProvider; |
| | | import org.opensearch.client.RestClient; |
| | | import org.opensearch.client.RestClientBuilder; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Conditional; |
| | | import org.springframework.context.annotation.Configuration; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * log-storage-mode 为 2 或 3 时加载 |
| | | */ |
| | | @Configuration |
| | | @Conditional(OnOpenSearchLogStorageEnabled.class) |
| | | public class HttpAuditOpenSearchConfiguration { |
| | | |
| | | @Bean(destroyMethod = "close") |
| | | public RestClient httpAuditOpenSearchRestClient(HttpAuditProperties props) { |
| | | HttpAuditProperties.OpenSearch oc = props.getOpenSearch(); |
| | | RestClientBuilder builder = RestClient.builder(buildHosts(oc)); |
| | | builder.setRequestConfigCallback(rc -> rc |
| | | .setConnectTimeout((int) oc.getConnectTimeout().toMillis()) |
| | | .setSocketTimeout((int) oc.getSocketTimeout().toMillis())); |
| | | if (StringUtils.isNotBlank(oc.getUsername())) { |
| | | CredentialsProvider cp = new BasicCredentialsProvider(); |
| | | cp.setCredentials(AuthScope.ANY, |
| | | new UsernamePasswordCredentials(oc.getUsername(), oc.getPassword() == null ? "" : oc.getPassword())); |
| | | builder.setHttpClientConfigCallback(hcb -> hcb.setDefaultCredentialsProvider(cp)); |
| | | } |
| | | return builder.build(); |
| | | } |
| | | |
| | | @Bean |
| | | public OpenSearchHttpAuditLogSink openSearchHttpAuditLogSink( |
| | | @Qualifier("httpAuditOpenSearchRestClient") RestClient httpAuditOpenSearchRestClient, |
| | | HttpAuditProperties props) { |
| | | return new OpenSearchHttpAuditLogSink(httpAuditOpenSearchRestClient, props); |
| | | } |
| | | |
| | | private static HttpHost[] buildHosts(HttpAuditProperties.OpenSearch oc) { |
| | | String scheme = oc.getScheme() == null ? "http" : oc.getScheme(); |
| | | List<HttpHost> hosts = new ArrayList<>(); |
| | | for (String raw : oc.getUris()) { |
| | | if (raw == null || raw.trim().isEmpty()) { |
| | | continue; |
| | | } |
| | | String s = raw.trim(); |
| | | int colon = s.lastIndexOf(':'); |
| | | if (colon > 0 && colon < s.length() - 1) { |
| | | String h = s.substring(0, colon); |
| | | int port = Integer.parseInt(s.substring(colon + 1)); |
| | | hosts.add(new HttpHost(h, port, scheme)); |
| | | } else { |
| | | hosts.add(new HttpHost(s, 9200, scheme)); |
| | | } |
| | | } |
| | | if (hosts.isEmpty()) { |
| | | hosts.add(new HttpHost("localhost", 9200, scheme)); |
| | | } |
| | | return hosts.toArray(new HttpHost[0]); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.config; |
| | | |
| | | import org.springframework.context.annotation.Condition; |
| | | import org.springframework.context.annotation.ConditionContext; |
| | | import org.springframework.core.type.AnnotatedTypeMetadata; |
| | | |
| | | /** |
| | | * log-storage-mode 为 2 或 3 |
| | | */ |
| | | public class OnOpenSearchLogStorageEnabled implements Condition { |
| | | |
| | | @Override |
| | | public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { |
| | | Integer mode = context.getEnvironment().getProperty("http-audit.log-storage-mode", Integer.class, 1); |
| | | int m = (mode != null && (mode == 2 || mode == 3)) ? mode : 1; |
| | | return m == 2 || m == 3; |
| | | } |
| | | } |
| | |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.vincent.rsf.framework.common.Cools; |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.httpaudit.common.Cools; |
| | | import com.vincent.rsf.httpaudit.common.R; |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditLog; |
| | | import com.vincent.rsf.httpaudit.props.HttpAuditProperties; |
| | | import com.vincent.rsf.httpaudit.service.HttpAuditLogCrudService; |
| | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | |
| | | import java.time.Duration; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | |
| | | public class HttpAuditProperties { |
| | | |
| | | private boolean enabled = true; |
| | | |
| | | /** 1 数据库 2 OpenSearch 3 双写;未填或其它值同 1 */ |
| | | private int logStorageMode = 1; |
| | | |
| | | /** 仅 2、3 使用 */ |
| | | private OpenSearch openSearch = new OpenSearch(); |
| | | |
| | | /** 仅 1、3;无多数据源可省略 */ |
| | | private String datasource = "primary"; |
| | | |
| | | /** 是否注册 /httpAuditRule、/httpAuditLog、/httpAuditSysConfig 等管理接口 */ |
| | | private boolean adminApiEnabled = true; |
| | |
| | | return HttpAuditDbConfigHolder.getPathDescriptions(pathDescriptions); |
| | | } |
| | | |
| | | public int resolveLogStorageMode() { |
| | | if (logStorageMode == 2 || logStorageMode == 3) { |
| | | return logStorageMode; |
| | | } |
| | | return 1; |
| | | } |
| | | |
| | | public boolean usesMysqlLogStorage() { |
| | | int m = resolveLogStorageMode(); |
| | | return m == 1 || m == 3; |
| | | } |
| | | |
| | | public boolean usesOpenSearchLogStorage() { |
| | | int m = resolveLogStorageMode(); |
| | | return m == 2 || m == 3; |
| | | } |
| | | |
| | | @Data |
| | | public static class OpenSearch { |
| | | private List<String> uris = new ArrayList<>(Collections.singletonList("localhost:9200")); |
| | | private String scheme = "http"; |
| | | private String username = ""; |
| | | private String password = ""; |
| | | private String indexName = "http_audit_log"; |
| | | private Duration connectTimeout = Duration.ofSeconds(5); |
| | | private Duration socketTimeout = Duration.ofSeconds(30); |
| | | } |
| | | |
| | | private static List<String> defaultExcludes() { |
| | | List<String> list = new ArrayList<>(); |
| | | list.add("/actuator"); |
| | |
| | | package com.vincent.rsf.httpaudit.service; |
| | | |
| | | import com.fasterxml.jackson.core.JsonProcessingException; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditLog; |
| | | import com.vincent.rsf.httpaudit.mapper.HttpAuditLogMapper; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.scheduling.annotation.Async; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 异步落库;失败时打全量日志,不向业务抛错 |
| | | * 异步写入已配置的日志目标;单路失败不影响其他路与业务 |
| | | */ |
| | | @Slf4j |
| | | @RequiredArgsConstructor |
| | | public class HttpAuditAsyncRecorder { |
| | | |
| | | private final HttpAuditLogMapper httpAuditLogMapper; |
| | | private final ObjectMapper objectMapper = new ObjectMapper(); |
| | | private final List<HttpAuditLogSink> sinks; |
| | | |
| | | public HttpAuditAsyncRecorder(List<HttpAuditLogSink> sinks) { |
| | | this.sinks = sinks; |
| | | } |
| | | |
| | | @Async("httpAuditExecutor") |
| | | public void save(HttpAuditLog entity) { |
| | | try { |
| | | httpAuditLogMapper.insert(entity); |
| | | } catch (Throwable t) { |
| | | try { |
| | | Map<String, Object> dump = new LinkedHashMap<>(); |
| | | dump.put("serviceName", entity.getServiceName()); |
| | | dump.put("scopeType", entity.getScopeType()); |
| | | dump.put("uri", entity.getUri()); |
| | | dump.put("method", entity.getMethod()); |
| | | dump.put("functionDesc", entity.getFunctionDesc()); |
| | | dump.put("queryString", entity.getQueryString()); |
| | | dump.put("requestBody", entity.getRequestBody()); |
| | | dump.put("responseBody", entity.getResponseBody()); |
| | | dump.put("httpStatus", entity.getHttpStatus()); |
| | | dump.put("okFlag", entity.getOkFlag()); |
| | | dump.put("spendMs", entity.getSpendMs()); |
| | | dump.put("clientIp", entity.getClientIp()); |
| | | dump.put("errorMessage", entity.getErrorMessage()); |
| | | String json = objectMapper.writeValueAsString(dump); |
| | | log.error("http-audit 落库失败,全量JSON如下:{}", json, t); |
| | | } catch (JsonProcessingException je) { |
| | | log.error("http-audit 落库失败且序列化审计内容失败,requestBody.length={}, responseBody.length={}", |
| | | entity.getRequestBody() == null ? -1 : entity.getRequestBody().length(), |
| | | entity.getResponseBody() == null ? -1 : entity.getResponseBody().length(), |
| | | t); |
| | | log.error("序列化异常", je); |
| | | } |
| | | for (HttpAuditLogSink sink : sinks) { |
| | | sink.write(entity); |
| | | } |
| | | } |
| | | } |
| | |
| | | package com.vincent.rsf.httpaudit.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.fasterxml.jackson.databind.node.ObjectNode; |
| | | 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.opensearch.client.Request; |
| | | import org.opensearch.client.ResponseException; |
| | | import org.opensearch.client.RestClient; |
| | | import org.springframework.scheduling.annotation.Scheduled; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.time.ZoneId; |
| | | import java.time.ZoneOffset; |
| | | import java.util.Date; |
| | | |
| | | /** |
| | |
| | | |
| | | private final HttpAuditLogMapper httpAuditLogMapper; |
| | | private final HttpAuditProperties props; |
| | | private final RestClient openSearchRestClient; |
| | | private final ObjectMapper objectMapper = new ObjectMapper(); |
| | | |
| | | public HttpAuditCleanupService(HttpAuditLogMapper httpAuditLogMapper, HttpAuditProperties props) { |
| | | public HttpAuditCleanupService(HttpAuditLogMapper httpAuditLogMapper, HttpAuditProperties props, |
| | | RestClient openSearchRestClient) { |
| | | this.httpAuditLogMapper = httpAuditLogMapper; |
| | | this.props = props; |
| | | this.openSearchRestClient = openSearchRestClient; |
| | | } |
| | | |
| | | @Scheduled(cron = "${http-audit.cleanup-cron:0 30 2 * * ?}") |
| | |
| | | log.warn("http-audit 清理已跳过,retentionDays 配置无效:{}", retentionDays); |
| | | return; |
| | | } |
| | | LocalDateTime cutoff = LocalDateTime.now().minusDays(retentionDays); |
| | | Date cutoffDate = Date.from(cutoff.atZone(ZoneId.systemDefault()).toInstant()); |
| | | if (props.usesMysqlLogStorage()) { |
| | | cleanupMysql(cutoffDate, retentionDays); |
| | | } |
| | | if (props.usesOpenSearchLogStorage() && openSearchRestClient != null) { |
| | | cleanupOpenSearch(cutoffDate, retentionDays); |
| | | } |
| | | } |
| | | |
| | | private void cleanupMysql(Date cutoffDate, int retentionDays) { |
| | | 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); |
| | | log.info("http-audit MySQL 清理完成,删除 {} 条,保留天数 {}", count, retentionDays); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("http-audit 清理失败", e); |
| | | log.warn("http-audit MySQL 清理失败", e); |
| | | } |
| | | } |
| | | |
| | | private void cleanupOpenSearch(Date cutoffDate, int retentionDays) { |
| | | try { |
| | | String index = props.getOpenSearch().getIndexName(); |
| | | String cutoffIso = cutoffDate.toInstant().atOffset(ZoneOffset.UTC).toString(); |
| | | ObjectNode body = objectMapper.createObjectNode(); |
| | | ObjectNode query = objectMapper.createObjectNode(); |
| | | ObjectNode range = objectMapper.createObjectNode(); |
| | | range.set("create_time", objectMapper.createObjectNode().put("lt", cutoffIso)); |
| | | query.set("range", range); |
| | | body.set("query", query); |
| | | Request request = new Request("POST", "/" + index + "/_delete_by_query?refresh=false&conflicts=proceed"); |
| | | request.setJsonEntity(objectMapper.writeValueAsString(body)); |
| | | openSearchRestClient.performRequest(request); |
| | | log.debug("http-audit OpenSearch delete_by_query 已提交 retentionDays={}", retentionDays); |
| | | } catch (ResponseException ex) { |
| | | int code = ex.getResponse() != null ? ex.getResponse().getStatusLine().getStatusCode() : -1; |
| | | if (code == 404) { |
| | | log.debug("http-audit OpenSearch 清理跳过,索引不存在"); |
| | | } else { |
| | | log.warn("http-audit OpenSearch 清理失败 status={}", code, ex); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("http-audit OpenSearch 清理失败", e); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.service; |
| | | |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditLog; |
| | | |
| | | /** |
| | | * 审计日志写入目标;单路失败不影响其他路与业务 |
| | | */ |
| | | public interface HttpAuditLogSink { |
| | | |
| | | void write(HttpAuditLog entity); |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.service; |
| | | |
| | | import com.fasterxml.jackson.core.JsonProcessingException; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditLog; |
| | | import com.vincent.rsf.httpaudit.mapper.HttpAuditLogMapper; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 写入 MySQL sys_http_audit_log |
| | | */ |
| | | @Slf4j |
| | | @RequiredArgsConstructor |
| | | public class MysqlHttpAuditLogSink implements HttpAuditLogSink { |
| | | |
| | | private final HttpAuditLogMapper httpAuditLogMapper; |
| | | private final ObjectMapper objectMapper = new ObjectMapper(); |
| | | |
| | | @Override |
| | | public void write(HttpAuditLog entity) { |
| | | try { |
| | | httpAuditLogMapper.insert(entity); |
| | | } catch (Throwable t) { |
| | | try { |
| | | Map<String, Object> dump = new LinkedHashMap<>(); |
| | | dump.put("serviceName", entity.getServiceName()); |
| | | dump.put("scopeType", entity.getScopeType()); |
| | | dump.put("uri", entity.getUri()); |
| | | dump.put("method", entity.getMethod()); |
| | | dump.put("functionDesc", entity.getFunctionDesc()); |
| | | dump.put("queryString", entity.getQueryString()); |
| | | dump.put("requestBody", entity.getRequestBody()); |
| | | dump.put("responseBody", entity.getResponseBody()); |
| | | dump.put("httpStatus", entity.getHttpStatus()); |
| | | dump.put("okFlag", entity.getOkFlag()); |
| | | dump.put("spendMs", entity.getSpendMs()); |
| | | dump.put("clientIp", entity.getClientIp()); |
| | | dump.put("errorMessage", entity.getErrorMessage()); |
| | | String json = objectMapper.writeValueAsString(dump); |
| | | log.error("http-audit 落库失败,全量JSON如下:{}", json, t); |
| | | } catch (JsonProcessingException je) { |
| | | log.error("http-audit 落库失败且序列化审计内容失败,requestBody.length={}, responseBody.length={}", |
| | | entity.getRequestBody() == null ? -1 : entity.getRequestBody().length(), |
| | | entity.getResponseBody() == null ? -1 : entity.getResponseBody().length(), |
| | | t); |
| | | log.error("序列化异常", je); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.httpaudit.service; |
| | | |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.fasterxml.jackson.databind.SerializationFeature; |
| | | import com.vincent.rsf.httpaudit.entity.HttpAuditLog; |
| | | import com.vincent.rsf.httpaudit.props.HttpAuditProperties; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.opensearch.client.Request; |
| | | import org.opensearch.client.ResponseException; |
| | | import org.opensearch.client.RestClient; |
| | | |
| | | import java.time.ZoneOffset; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import java.util.UUID; |
| | | |
| | | /** |
| | | * 写入 OpenSearch 索引 |
| | | */ |
| | | @Slf4j |
| | | @RequiredArgsConstructor |
| | | public class OpenSearchHttpAuditLogSink implements HttpAuditLogSink { |
| | | |
| | | private static final ObjectMapper JSON = new ObjectMapper() |
| | | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); |
| | | |
| | | private final RestClient restClient; |
| | | private final HttpAuditProperties props; |
| | | |
| | | @Override |
| | | public void write(HttpAuditLog entity) { |
| | | try { |
| | | String index = props.getOpenSearch().getIndexName(); |
| | | String docId = entity.getId() != null ? String.valueOf(entity.getId()) : UUID.randomUUID().toString(); |
| | | String path = "/" + index + "/_doc/" + docId + "?refresh=false"; |
| | | Request request = new Request("PUT", path); |
| | | request.setJsonEntity(JSON.writeValueAsString(toDocument(entity))); |
| | | restClient.performRequest(request); |
| | | } catch (ResponseException ex) { |
| | | int code = ex.getResponse() != null ? ex.getResponse().getStatusLine().getStatusCode() : -1; |
| | | log.error("http-audit OpenSearch 写入失败 uri={} status={}", entity.getUri(), code, ex); |
| | | } catch (Throwable t) { |
| | | log.error("http-audit OpenSearch 写入失败 uri={}", entity.getUri(), t); |
| | | } |
| | | } |
| | | |
| | | private static Map<String, Object> toDocument(HttpAuditLog e) { |
| | | Map<String, Object> m = new LinkedHashMap<>(); |
| | | m.put("id", e.getId()); |
| | | m.put("service_name", e.getServiceName()); |
| | | m.put("scope_type", e.getScopeType()); |
| | | m.put("uri", e.getUri()); |
| | | m.put("io_direction", e.getIoDirection()); |
| | | m.put("method", e.getMethod()); |
| | | m.put("function_desc", e.getFunctionDesc()); |
| | | m.put("query_string", e.getQueryString()); |
| | | m.put("request_body", e.getRequestBody()); |
| | | m.put("response_body", e.getResponseBody()); |
| | | m.put("response_truncated", e.getResponseTruncated()); |
| | | m.put("http_status", e.getHttpStatus()); |
| | | m.put("ok_flag", e.getOkFlag()); |
| | | m.put("spend_ms", e.getSpendMs()); |
| | | m.put("client_ip", e.getClientIp()); |
| | | m.put("error_message", e.getErrorMessage()); |
| | | if (e.getCreateTime() != null) { |
| | | m.put("create_time", e.getCreateTime().toInstant().atOffset(ZoneOffset.UTC).toString()); |
| | | } |
| | | m.put("deleted", e.getDeleted()); |
| | | return m; |
| | | } |
| | | } |
| | |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.vincent.rsf.framework.common.Cools; |
| | | import com.vincent.rsf.httpaudit.common.Cools; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| New file |
| | |
| | | http-audit: |
| | | enabled: true |
| | | log-storage-mode: 1 |
| | | datasource: primary |
| | |
| | | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
| | | com.vincent.rsf.httpaudit.config.HttpAuditAutoConfiguration |
| | | org.springframework.boot.env.EnvironmentPostProcessor=\ |
| | | com.vincent.rsf.httpaudit.config.HttpAuditJarDefaultsEnvironmentPostProcessor |
| | |
| | | package com.vincent.rsf.openApi.common.datasource; |
| | | |
| | | import com.vincent.rsf.httpaudit.props.HttpAuditProperties; |
| | | import org.aspectj.lang.ProceedingJoinPoint; |
| | | import org.aspectj.lang.annotation.Around; |
| | | import org.aspectj.lang.annotation.Aspect; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.core.Ordered; |
| | | import org.springframework.core.annotation.Order; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | /** |
| | | * http-audit 数据源切换 |
| | | * http-audit 数据源切换;不切 props 包,避免切面内调 {@link HttpAuditProperties} 自递归 |
| | | */ |
| | | @Aspect |
| | | @Component |
| | | @Order(Ordered.HIGHEST_PRECEDENCE + 10) |
| | | public class HttpAuditDataSourceAspect { |
| | | |
| | | @Value("${http-audit.datasource:primary}") |
| | | private String dataSource; |
| | | @Autowired |
| | | private HttpAuditProperties httpAuditProperties; |
| | | |
| | | @Around("execution(* com.vincent.rsf.httpaudit..*(..))") |
| | | @Around("execution(* com.vincent.rsf.httpaudit..*(..)) && !execution(* com.vincent.rsf.httpaudit.props..*(..))") |
| | | public Object around(ProceedingJoinPoint joinPoint) throws Throwable { |
| | | String selected = resolveDataSource(); |
| | | DataSourceContextHolder.push(selected); |
| | |
| | | } |
| | | |
| | | private String resolveDataSource() { |
| | | if (httpAuditProperties.resolveLogStorageMode() == 2) { |
| | | return DataSourceNames.PRIMARY; |
| | | } |
| | | String dataSource = httpAuditProperties.getDatasource(); |
| | | if ("jdxaj-log".equalsIgnoreCase(dataSource)) { |
| | | return DataSourceNames.JDXAJ_LOG; |
| | | } |
| | |
| | | package com.vincent.rsf.server.common.datasource; |
| | | |
| | | import com.vincent.rsf.httpaudit.props.HttpAuditProperties; |
| | | import org.aspectj.lang.ProceedingJoinPoint; |
| | | import org.aspectj.lang.annotation.Around; |
| | | import org.aspectj.lang.annotation.Aspect; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.core.Ordered; |
| | | import org.springframework.core.annotation.Order; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | /** |
| | | * http-audit 数据源切换 |
| | | * http-audit 数据源切换;不切 props 包,避免切面内调 {@link HttpAuditProperties} 自递归 |
| | | */ |
| | | @Aspect |
| | | @Component |
| | | @Order(Ordered.HIGHEST_PRECEDENCE + 10) |
| | | public class HttpAuditDataSourceAspect { |
| | | |
| | | @Value("${http-audit.datasource:primary}") |
| | | private String dataSource; |
| | | @Autowired |
| | | private HttpAuditProperties httpAuditProperties; |
| | | |
| | | @Around("execution(* com.vincent.rsf.httpaudit..*(..))") |
| | | @Around("execution(* com.vincent.rsf.httpaudit..*(..)) && !execution(* com.vincent.rsf.httpaudit.props..*(..))") |
| | | public Object around(ProceedingJoinPoint joinPoint) throws Throwable { |
| | | String selected = resolveDataSource(); |
| | | DataSourceContextHolder.push(selected); |
| | |
| | | } |
| | | |
| | | private String resolveDataSource() { |
| | | if (httpAuditProperties.resolveLogStorageMode() == 2) { |
| | | return DataSourceNames.PRIMARY; |
| | | } |
| | | String dataSource = httpAuditProperties.getDatasource(); |
| | | if ("dj-cloud-wms".equalsIgnoreCase(dataSource)) { |
| | | return DataSourceNames.DJ_CLOUD_WMS; |
| | | } |
| | |
| | | package com.vincent.rsf.server.manager.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import javax.sql.DataSource; |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | |
| | | log.warn("dj-cloud-wms 数据源未配置,跳过 cus_barcode_sync_view"); |
| | | return Collections.emptyList(); |
| | | } |
| | | List<CusBarcodeSyncView> rows = cusBarcodeSyncViewMapper.selectList( |
| | | buildBarcodeOrQuery(codes) |
| | | .select( |
| | | CusBarcodeSyncView::getBarcode, |
| | | CusBarcodeSyncView::getItemName, |
| | | CusBarcodeSyncView::getItemSpec, |
| | | CusBarcodeSyncView::getUnitNo)); |
| | | List<CusBarcodeSyncView> rows = new ArrayList<>(); |
| | | for (String code : codes) { |
| | | List<CusBarcodeSyncView> part = cusBarcodeSyncViewMapper.selectList( |
| | | Wrappers.<CusBarcodeSyncView>lambdaQuery() |
| | | .eq(CusBarcodeSyncView::getBarcode, code) |
| | | .select( |
| | | CusBarcodeSyncView::getBarcode, |
| | | CusBarcodeSyncView::getItemName, |
| | | CusBarcodeSyncView::getItemSpec, |
| | | CusBarcodeSyncView::getUnitNo) |
| | | .last("LIMIT 1")); |
| | | if (part != null && !part.isEmpty()) { |
| | | rows.addAll(part); |
| | | } |
| | | } |
| | | return toViewMaps(rows); |
| | | } |
| | | |
| | | private LambdaQueryWrapper<CusBarcodeSyncView> buildBarcodeOrQuery(List<String> codes) { |
| | | LambdaQueryWrapper<CusBarcodeSyncView> wrapper = Wrappers.lambdaQuery(); |
| | | wrapper.and(q -> { |
| | | for (int i = 0; i < codes.size(); i++) { |
| | | String code = codes.get(i); |
| | | if (i == 0) { |
| | | q.eq(CusBarcodeSyncView::getBarcode, code); |
| | | } else { |
| | | q.or().eq(CusBarcodeSyncView::getBarcode, code); |
| | | } |
| | | } |
| | | }); |
| | | return wrapper; |
| | | } |
| | | // 原单次 OR 拼接(避免 IN/OR 单条时可改回) |
| | | // private LambdaQueryWrapper<CusBarcodeSyncView> buildBarcodeOrQuery(List<String> codes) { |
| | | // LambdaQueryWrapper<CusBarcodeSyncView> wrapper = Wrappers.lambdaQuery(); |
| | | // wrapper.and(q -> { |
| | | // for (int i = 0; i < codes.size(); i++) { |
| | | // String code = codes.get(i); |
| | | // if (i == 0) { |
| | | // q.eq(CusBarcodeSyncView::getBarcode, code); |
| | | // } else { |
| | | // q.or().eq(CusBarcodeSyncView::getBarcode, code); |
| | | // } |
| | | // } |
| | | // }); |
| | | // return wrapper; |
| | | // } |
| | | |
| | | private List<Map<String, Object>> toViewMaps(List<CusBarcodeSyncView> rows) { |
| | | if (rows == null || rows.isEmpty()) { |
| | |
| | | sync-cron: "0/3 * * * * ?" |
| | | recover-cron: "0/5 * * * * ?" |
| | | |
| | | # HTTP 接口审计(rsf-http-audit,不引入依赖则无审计;enabled=false 关闭 Filter 与管理接口) |
| | | # admin-api-enabled:是否注册 /httpAuditRule、/httpAuditLog、/httpAuditSysConfig |
| | | # 简易页默认开启;simple-ui-token 非空则校验请求头(公网建议配置) |
| | | http-audit: |
| | | enabled: true |
| | | # enabled: false |
| | | # 审计数据源:primary / dj-cloud-wms / jdxaj-log |
| | | datasource: jdxaj-log |
| | |
| | | sync-cron: "0 0/1 * * * ?" |
| | | recover-cron: "0 0/2 * * * ?" |
| | | |
| | | # HTTP 接口审计(rsf-http-audit,引入依赖即生效,可 enabled=false 关闭) |
| | | # whitelist-only=true:仅 sys_http_audit_rule 命中规则才写审计;无规则时不落库。false:排除路径外全量记录。 |
| | | # rule-cache-refresh-ms:规则表缓存刷新间隔(毫秒) |
| | | http-audit: |
| | | enabled: true |
| | | # 审计数据源:primary / dj-cloud-wms / jdxaj-log |
| | | datasource: jdxaj-log |