Initial commit
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.lingniu.framework</groupId>
|
||||
<artifactId>lingniu-framework-dependencies</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<relativePath>../../../lingniu-framework-dependencies/pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>lingniu-framework-plugin-web</artifactId>
|
||||
<name>${project.artifactId}</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.lingniu.framework</groupId>
|
||||
<artifactId>lingniu-framework-plugin-util</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.lingniu.framework</groupId>
|
||||
<artifactId>lingniu-framework-plugin-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
<scope>compile</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jdk8</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.module</groupId>
|
||||
<artifactId>jackson-module-parameter-names</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,45 @@
|
||||
package cn.lingniu.framework.plugin.web;
|
||||
|
||||
import cn.lingniu.framework.plugin.core.config.CommonConstant;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.web.context.WebServerInitializedEvent;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class ApplicationStartEventListener {
|
||||
|
||||
@Async
|
||||
@Order
|
||||
@EventListener(WebServerInitializedEvent.class)
|
||||
public void afterStart(WebServerInitializedEvent event) {
|
||||
try {
|
||||
if (event == null || event.getWebServer() == null) {
|
||||
log.warn("Web server initialized event is null or web server is null");
|
||||
return;
|
||||
}
|
||||
|
||||
Environment environment = event.getApplicationContext().getEnvironment();
|
||||
if (environment == null) {
|
||||
log.warn("Application environment is null");
|
||||
return;
|
||||
}
|
||||
|
||||
String appName = environment.getProperty(CommonConstant.SPRING_APP_NAME_KEY, "UNKNOWN").toUpperCase();
|
||||
int localPort = event.getWebServer().getPort();
|
||||
String profile = StringUtils.arrayToCommaDelimitedString(environment.getActiveProfiles());
|
||||
String containerType = event.getWebServer().getClass().getSimpleName();
|
||||
|
||||
log.info("Application [{}] started successfully on port [{}] with profiles [{}], container type: [{}]",
|
||||
appName, localPort, profile, containerType);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error occurred while handling web server initialized event", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog;
|
||||
|
||||
|
||||
import cn.lingniu.framework.plugin.web.apilog.enums.OperateTypeEnum;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 访问日志注解
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ApiAccessLog {
|
||||
|
||||
// ========== 开关字段 ==========
|
||||
|
||||
/**
|
||||
* 是否记录访问日志
|
||||
*/
|
||||
boolean enable() default true;
|
||||
/**
|
||||
* 是否记录请求参数
|
||||
*
|
||||
* 默认记录,主要考虑请求数据一般不大。可手动设置为 false 进行关闭
|
||||
*/
|
||||
boolean requestEnable() default true;
|
||||
/**
|
||||
* 是否记录响应结果
|
||||
*
|
||||
* 默认不记录,主要考虑响应数据可能比较大。可手动设置为 true 进行打开
|
||||
*/
|
||||
boolean responseEnable() default false;
|
||||
/**
|
||||
* 敏感参数数组
|
||||
*
|
||||
* 添加后,请求参数、响应结果不会记录该参数
|
||||
*/
|
||||
String[] sanitizeKeys() default {};
|
||||
/**
|
||||
* 操作分类
|
||||
*
|
||||
* 实际并不是数组,因为枚举不能设置 null 作为默认值
|
||||
*/
|
||||
OperateTypeEnum[] operateType() default {};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 操作日志的操作类型
|
||||
*
|
||||
* @author portal
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum OperateTypeEnum {
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
GET(1),
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
CREATE(2),
|
||||
/**
|
||||
* 修改
|
||||
*/
|
||||
UPDATE(3),
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
DELETE(4),
|
||||
/**
|
||||
* 导出
|
||||
*/
|
||||
EXPORT(5),
|
||||
/**
|
||||
* 导入
|
||||
*/
|
||||
IMPORT(6),
|
||||
/**
|
||||
* 其它
|
||||
*
|
||||
* 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识
|
||||
*/
|
||||
OTHER(0);
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private final Integer type;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.filter;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.lingniu.framework.plugin.core.base.CommonResult;
|
||||
import cn.lingniu.framework.plugin.core.context.ApplicationNameContext;
|
||||
import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.lingniu.framework.plugin.util.json.JsonUtils;
|
||||
import cn.lingniu.framework.plugin.util.servlet.ServletUtils;
|
||||
import cn.lingniu.framework.plugin.web.apilog.ApiAccessLog;
|
||||
import cn.lingniu.framework.plugin.web.apilog.enums.OperateTypeEnum;
|
||||
import cn.lingniu.framework.plugin.web.apilog.interceptor.ApiAccessLogInterceptor;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogCommonApi;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO;
|
||||
import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig;
|
||||
import cn.lingniu.framework.plugin.web.bae.filter.ApiRequestFilter;
|
||||
import cn.lingniu.framework.plugin.web.bae.util.WebFrameworkUtils;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* API 访问日志 Filter
|
||||
* todo 目的:记录 API 访问日志输出---后续可以输出db
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
public class ApiAccessLogFilter extends ApiRequestFilter {
|
||||
|
||||
private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"};
|
||||
|
||||
private final String applicationName;
|
||||
|
||||
private final ApiAccessLogCommonApi apiAccessLogApi;
|
||||
|
||||
public ApiAccessLogFilter(FrameworkWebConfig frameworkWebConfig, String applicationName, ApiAccessLogCommonApi apiAccessLogApi) {
|
||||
super(frameworkWebConfig);
|
||||
this.applicationName = applicationName;
|
||||
this.apiAccessLogApi = apiAccessLogApi;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullableProblems")
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
// 获得开始时间
|
||||
LocalDateTime beginTime = LocalDateTime.now();
|
||||
// 提前获得参数,避免 XssFilter 过滤处理
|
||||
Map<String, String> queryString = ServletUtils.getParamMap(request);
|
||||
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
|
||||
|
||||
try {
|
||||
// 继续过滤器
|
||||
filterChain.doFilter(request, response);
|
||||
// 正常执行,记录日志
|
||||
createApiAccessLog(request, beginTime, queryString, requestBody, null);
|
||||
} catch (Exception ex) {
|
||||
// 异常执行,记录日志
|
||||
createApiAccessLog(request, beginTime, queryString, requestBody, ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime,
|
||||
Map<String, String> queryString, String requestBody, Exception ex) {
|
||||
ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO();
|
||||
try {
|
||||
boolean enable = buildApiAccessLog(accessLog, request, beginTime, queryString, requestBody, ex);
|
||||
if (!enable) {
|
||||
return;
|
||||
}
|
||||
apiAccessLogApi.createApiAccessLogAsync(accessLog);
|
||||
} catch (Throwable th) {
|
||||
log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), JsonUtils.toJsonString(accessLog), th);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean buildApiAccessLog(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, LocalDateTime beginTime,
|
||||
Map<String, String> queryString, String requestBody, Exception ex) {
|
||||
// 判断:是否要记录操作日志
|
||||
HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD);
|
||||
ApiAccessLog accessLogAnnotation = null;
|
||||
if (handlerMethod != null) {
|
||||
accessLogAnnotation = handlerMethod.getMethodAnnotation(ApiAccessLog.class);
|
||||
if (accessLogAnnotation != null && BooleanUtil.isFalse(accessLogAnnotation.enable())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理用户信息
|
||||
accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
|
||||
accessLog.setUserType(WebFrameworkUtils.getLoginUserType(request));
|
||||
// 设置访问结果
|
||||
CommonResult<?> result = WebFrameworkUtils.getCommonResult(request);
|
||||
if (result != null) {
|
||||
accessLog.setResultCode(result.getCode());
|
||||
accessLog.setResultMsg(result.getMsg());
|
||||
} else if (ex != null) {
|
||||
accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode());
|
||||
accessLog .setResultMsg(ExceptionUtil.getRootCauseMessage(ex));
|
||||
} else {
|
||||
accessLog.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode());
|
||||
accessLog.setResultMsg("");
|
||||
}
|
||||
// 设置请求字段
|
||||
accessLog.setRequestUrl(request.getRequestURI());
|
||||
accessLog.setRequestMethod(request.getMethod());
|
||||
accessLog.setApplicationName(ApplicationNameContext.getApplicationName());
|
||||
accessLog.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
accessLog.setUserIp(ServletUtils.getClientIP(request));
|
||||
String[] sanitizeKeys = accessLogAnnotation != null ? accessLogAnnotation.sanitizeKeys() : null;
|
||||
Boolean requestEnable = accessLogAnnotation != null ? accessLogAnnotation.requestEnable() : Boolean.TRUE;
|
||||
if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录,所以判断 !false
|
||||
Map<String, Object> requestParams = MapUtil.<String, Object>builder()
|
||||
.put("query", sanitizeMap(queryString, sanitizeKeys))
|
||||
.put("body", sanitizeJson(requestBody, sanitizeKeys)).build();
|
||||
accessLog.setRequestParams(JsonUtils.toJsonString(requestParams));
|
||||
}
|
||||
Boolean responseEnable = accessLogAnnotation != null ? accessLogAnnotation.responseEnable() : Boolean.FALSE;
|
||||
if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录,默认强制要求 true
|
||||
accessLog.setResponseBody(sanitizeJson(result, sanitizeKeys));
|
||||
}
|
||||
// 持续时间
|
||||
accessLog.setBeginTime(beginTime);
|
||||
accessLog.setEndTime(LocalDateTime.now());
|
||||
accessLog.setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS));
|
||||
// 操作模块
|
||||
OperateTypeEnum operateType = accessLogAnnotation != null && accessLogAnnotation.operateType().length > 0 ?
|
||||
accessLogAnnotation.operateType()[0] : parseOperateLogType(request);
|
||||
accessLog.setOperateType(operateType.getType());
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========== 解析 @ApiAccessLog、@Swagger 注解 ==========
|
||||
|
||||
private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) {
|
||||
RequestMethod requestMethod = ArrayUtil.firstMatch(method ->
|
||||
StrUtil.equalsAnyIgnoreCase(method.name(), request.getMethod()), RequestMethod.values());
|
||||
if (requestMethod == null) {
|
||||
return OperateTypeEnum.OTHER;
|
||||
}
|
||||
switch (requestMethod) {
|
||||
case GET:
|
||||
return OperateTypeEnum.GET;
|
||||
case POST:
|
||||
return OperateTypeEnum.CREATE;
|
||||
case PUT:
|
||||
return OperateTypeEnum.UPDATE;
|
||||
case DELETE:
|
||||
return OperateTypeEnum.DELETE;
|
||||
default:
|
||||
return OperateTypeEnum.OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ==========
|
||||
|
||||
private static String sanitizeMap(Map<String, ?> map, String[] sanitizeKeys) {
|
||||
if (CollUtil.isEmpty(map)) {
|
||||
return null;
|
||||
}
|
||||
if (sanitizeKeys != null) {
|
||||
MapUtil.removeAny(map, sanitizeKeys);
|
||||
}
|
||||
MapUtil.removeAny(map, SANITIZE_KEYS);
|
||||
return JsonUtils.toJsonString(map);
|
||||
}
|
||||
|
||||
private static String sanitizeJson(String jsonString, String[] sanitizeKeys) {
|
||||
if (StrUtil.isEmpty(jsonString)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JsonNode rootNode = JsonUtils.parseTree(jsonString);
|
||||
sanitizeJson(rootNode, sanitizeKeys);
|
||||
return JsonUtils.toJsonString(rootNode);
|
||||
} catch (Exception e) {
|
||||
// 脱敏失败的情况下,直接忽略异常,避免影响用户请求
|
||||
log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
|
||||
return jsonString;
|
||||
}
|
||||
}
|
||||
|
||||
private static String sanitizeJson(CommonResult<?> commonResult, String[] sanitizeKeys) {
|
||||
if (commonResult == null) {
|
||||
return null;
|
||||
}
|
||||
String jsonString = JsonUtils.toJsonString(commonResult);
|
||||
try {
|
||||
JsonNode rootNode = JsonUtils.parseTree(jsonString);
|
||||
sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉
|
||||
return JsonUtils.toJsonString(rootNode);
|
||||
} catch (Exception e) {
|
||||
// 脱敏失败的情况下,直接忽略异常,避免影响用户请求
|
||||
log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
|
||||
return jsonString;
|
||||
}
|
||||
}
|
||||
|
||||
private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) {
|
||||
// 情况一:数组,遍历处理
|
||||
if (node.isArray()) {
|
||||
for (JsonNode childNode : node) {
|
||||
sanitizeJson(childNode, sanitizeKeys);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 情况二:非 Object,只是某个值,直接返回
|
||||
if (!node.isObject()) {
|
||||
return;
|
||||
}
|
||||
// 情况三:Object,遍历处理
|
||||
Iterator<Map.Entry<String, JsonNode>> iterator = node.fields();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, JsonNode> entry = iterator.next();
|
||||
if (ArrayUtil.contains(sanitizeKeys, entry.getKey())
|
||||
|| ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
sanitizeJson(entry.getValue(), sanitizeKeys);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.interceptor;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.lingniu.framework.plugin.util.servlet.ServletUtils;
|
||||
import cn.lingniu.framework.plugin.util.spring.SpringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* API 访问日志 Interceptor
|
||||
*
|
||||
* todo 目的:在非 prod 环境时,打印 request 和 response 两条日志到日志文件(控制台)中。
|
||||
*/
|
||||
@Slf4j
|
||||
public class ApiAccessLogInterceptor implements HandlerInterceptor {
|
||||
|
||||
public static final String ATTRIBUTE_HANDLER_METHOD = "HANDLER_METHOD";
|
||||
|
||||
private static final String ATTRIBUTE_STOP_WATCH = "ApiAccessLogInterceptor.StopWatch";
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
// 记录 HandlerMethod,提供给 ApiAccessLogFilter 使用
|
||||
HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod) handler : null;
|
||||
if (handlerMethod != null) {
|
||||
request.setAttribute(ATTRIBUTE_HANDLER_METHOD, handlerMethod);
|
||||
}
|
||||
|
||||
// 打印 request 日志
|
||||
if (!SpringUtils.isProd()) {
|
||||
Map<String, String> queryString = ServletUtils.getParamMap(request);
|
||||
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
|
||||
if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) {
|
||||
log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI());
|
||||
} else {
|
||||
log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(),
|
||||
StrUtil.blankToDefault(requestBody, queryString.toString()));
|
||||
}
|
||||
// 计时
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch);
|
||||
// 打印 Controller 路径
|
||||
printHandlerMethodPosition(handlerMethod);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||
// 打印 response 日志
|
||||
if (!SpringUtils.isProd()) {
|
||||
StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH);
|
||||
stopWatch.stop();
|
||||
log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]",
|
||||
request.getRequestURI(), stopWatch.getTotalTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印 Controller 方法路径
|
||||
*/
|
||||
private void printHandlerMethodPosition(HandlerMethod handlerMethod) {
|
||||
if (handlerMethod == null) {
|
||||
return;
|
||||
}
|
||||
Method method = handlerMethod.getMethod();
|
||||
Class<?> clazz = method.getDeclaringClass();
|
||||
try {
|
||||
// 获取 method 的 lineNumber
|
||||
List<String> clazzContents = FileUtil.readUtf8Lines(
|
||||
ResourceUtil.getResource(null, clazz).getPath().replace("/target/classes/", "/src/main/java/")
|
||||
+ clazz.getSimpleName() + ".java");
|
||||
Optional<Integer> lineNumber = IntStream.range(0, clazzContents.size())
|
||||
.filter(i -> clazzContents.get(i).contains(" " + method.getName() + "(")) // 简单匹配,不考虑方法重名
|
||||
.mapToObj(i -> i + 1) // 行号从 1 开始
|
||||
.findFirst();
|
||||
if (!lineNumber.isPresent()) {
|
||||
return;
|
||||
}
|
||||
// 打印结果
|
||||
System.out.printf("\tController 方法路径:%s(%s.java:%d)\n", clazz.getName(), clazz.getSimpleName(), lineNumber.get());
|
||||
} catch (Exception ignore) {
|
||||
// 忽略异常。原因:仅仅打印,非重要逻辑
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.logger;
|
||||
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogCommonApi;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogService;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* API 访问日志的 API 实现类
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class ApiAccessLogApiImpl implements ApiAccessLogCommonApi {
|
||||
|
||||
@Resource
|
||||
private ApiAccessLogService apiAccessLogService;
|
||||
|
||||
@Override
|
||||
public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) {
|
||||
apiAccessLogService.createApiAccessLog(createDTO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.logger;
|
||||
|
||||
import cn.lingniu.framework.plugin.util.json.JsonUtils;
|
||||
import cn.lingniu.framework.plugin.util.object.BeanUtils;
|
||||
import cn.lingniu.framework.plugin.util.string.StrUtils;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogService;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogDO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
|
||||
/**
|
||||
* API 访问日志 Service 实现类 todo 可以扩展
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Validated
|
||||
public class ApiAccessLogServiceImpl implements ApiAccessLogService {
|
||||
|
||||
|
||||
@Override
|
||||
public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) {
|
||||
ApiAccessLogDO apiAccessLog = BeanUtils.toBean(createDTO, ApiAccessLogDO.class);
|
||||
apiAccessLog.setRequestParams(StrUtils.maxLength(apiAccessLog.getRequestParams(), ApiAccessLogDO.REQUEST_PARAMS_MAX_LENGTH));
|
||||
apiAccessLog.setResultMsg(StrUtils.maxLength(apiAccessLog.getResultMsg(), ApiAccessLogDO.RESULT_MSG_MAX_LENGTH));
|
||||
log.info("api请求正常,详细信息:{}", JsonUtils.toJsonString(createDTO));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.logger;
|
||||
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiErrorLogCommonApi;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiErrorLogService;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* API 访问日志的 API 接口
|
||||
*/
|
||||
@Service
|
||||
public class ApiErrorLogApiImpl implements ApiErrorLogCommonApi {
|
||||
|
||||
@Resource
|
||||
private ApiErrorLogService apiErrorLogService;
|
||||
|
||||
@Override
|
||||
public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) {
|
||||
apiErrorLogService.createApiErrorLog(createDTO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.logger;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.lingniu.framework.plugin.util.json.JsonUtils;
|
||||
import cn.lingniu.framework.plugin.util.object.BeanUtils;
|
||||
import cn.lingniu.framework.plugin.util.string.StrUtils;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiErrorLogService;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogDO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
|
||||
/**
|
||||
* API 错误日志 Service 实现类 todo 可以扩展
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class ApiErrorLogServiceImpl implements ApiErrorLogService {
|
||||
|
||||
|
||||
@Override
|
||||
public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) {
|
||||
ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class) ;
|
||||
apiErrorLog.setRequestParams(StrUtils.maxLength(apiErrorLog.getRequestParams(), ApiErrorLogDO.REQUEST_PARAMS_MAX_LENGTH));
|
||||
log.error("api请求异常,详细信息:{}", JsonUtils.toJsonString(apiErrorLog));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.logger.api;
|
||||
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* API 访问日志的 API 接口
|
||||
*/
|
||||
public interface ApiAccessLogCommonApi {
|
||||
|
||||
/**
|
||||
* 创建 API 访问日志
|
||||
*
|
||||
* @param createDTO 创建信息
|
||||
*/
|
||||
void createApiAccessLog(@Valid ApiAccessLogCreateReqDTO createDTO);
|
||||
|
||||
/**
|
||||
* 【异步】创建 API 访问日志
|
||||
*
|
||||
* @param createDTO 访问日志 DTO
|
||||
*/
|
||||
@Async
|
||||
default void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) {
|
||||
createApiAccessLog(createDTO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.logger.api;
|
||||
|
||||
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO;
|
||||
|
||||
/**
|
||||
* API 访问日志 Service 接口
|
||||
*/
|
||||
public interface ApiAccessLogService {
|
||||
|
||||
/**
|
||||
* 创建 API 访问日志
|
||||
*
|
||||
* @param createReqDTO API 访问日志
|
||||
*/
|
||||
void createApiAccessLog(ApiAccessLogCreateReqDTO createReqDTO);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.logger.api;
|
||||
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* API 错误日志的 API 接口
|
||||
*/
|
||||
public interface ApiErrorLogCommonApi {
|
||||
|
||||
/**
|
||||
* 创建 API 错误日志
|
||||
*
|
||||
* @param createDTO 创建信息
|
||||
*/
|
||||
void createApiErrorLog(@Valid ApiErrorLogCreateReqDTO createDTO);
|
||||
|
||||
/**
|
||||
* 【异步】创建 API 异常日志
|
||||
*
|
||||
* @param createDTO 异常日志 DTO
|
||||
*/
|
||||
@Async
|
||||
default void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) {
|
||||
createApiErrorLog(createDTO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.logger.api;
|
||||
|
||||
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO;
|
||||
|
||||
/**
|
||||
* API 错误日志 Service 接口
|
||||
*/
|
||||
public interface ApiErrorLogService {
|
||||
|
||||
/**
|
||||
* 创建 API 错误日志
|
||||
*
|
||||
* @param createReqDTO API 错误日志
|
||||
*/
|
||||
void createApiErrorLog(ApiErrorLogCreateReqDTO createReqDTO);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.logger.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 访问日志---后续可以跟用户登陆信息互通
|
||||
*/
|
||||
@Data
|
||||
public class ApiAccessLogCreateReqDTO {
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long userId = 0L;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType = 0;
|
||||
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@NotNull(message = "应用名不能为空")
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
@NotNull(message = "http 请求方法不能为空")
|
||||
private String requestMethod;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
@NotNull(message = "访问地址不能为空")
|
||||
private String requestUrl;
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
private String requestParams;
|
||||
/**
|
||||
* 响应结果
|
||||
*/
|
||||
private String responseBody;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotNull(message = "ip 不能为空")
|
||||
private String userIp;
|
||||
/**
|
||||
* 浏览器 UA
|
||||
*/
|
||||
@NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
/**
|
||||
* 开始请求时间
|
||||
*/
|
||||
@NotNull(message = "开始请求时间不能为空")
|
||||
private LocalDateTime beginTime;
|
||||
/**
|
||||
* 结束请求时间
|
||||
*/
|
||||
@NotNull(message = "结束请求时间不能为空")
|
||||
private LocalDateTime endTime;
|
||||
/**
|
||||
* 执行时长,单位:毫秒
|
||||
*/
|
||||
@NotNull(message = "执行时长不能为空")
|
||||
private Integer duration;
|
||||
/**
|
||||
* 结果码
|
||||
*/
|
||||
@NotNull(message = "错误码不能为空")
|
||||
private Integer resultCode;
|
||||
/**
|
||||
* 结果提示
|
||||
*/
|
||||
private String resultMsg;
|
||||
|
||||
/**
|
||||
* 操作分类
|
||||
*
|
||||
* 枚举,参见 OperateTypeEnum 类
|
||||
*/
|
||||
private Integer operateType;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.logger.model;
|
||||
|
||||
import cn.lingniu.framework.plugin.core.base.CommonResult;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 访问日志
|
||||
*/
|
||||
@Data
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiAccessLogDO {
|
||||
|
||||
/**
|
||||
* {@link #requestParams} 的最大长度
|
||||
*/
|
||||
public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000;
|
||||
|
||||
/**
|
||||
* {@link #resultMsg} 的最大长度
|
||||
*/
|
||||
public static final Integer RESULT_MSG_MAX_LENGTH = 512;
|
||||
|
||||
|
||||
/**
|
||||
* 应用名
|
||||
*
|
||||
* 目前读取 `spring.application.name` 配置项
|
||||
*/
|
||||
private String applicationName;
|
||||
|
||||
// ========== 请求相关字段 ==========
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
private String requestMethod;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
private String requestUrl;
|
||||
/**
|
||||
* 请求参数
|
||||
*
|
||||
* query: Query String
|
||||
* body: Quest Body
|
||||
*/
|
||||
private String requestParams;
|
||||
/**
|
||||
* 响应结果
|
||||
*/
|
||||
private String responseBody;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
private String userIp;
|
||||
/**
|
||||
* 浏览器 UA
|
||||
*/
|
||||
private String userAgent;
|
||||
|
||||
// ========== 执行相关字段 ==========
|
||||
|
||||
|
||||
/**
|
||||
* 开始请求时间
|
||||
*/
|
||||
private LocalDateTime beginTime;
|
||||
/**
|
||||
* 结束请求时间
|
||||
*/
|
||||
private LocalDateTime endTime;
|
||||
/**
|
||||
* 执行时长,单位:毫秒
|
||||
*/
|
||||
private Integer duration;
|
||||
|
||||
/**
|
||||
* 结果码
|
||||
*
|
||||
* 目前使用的 {@link CommonResult#getCode()} 属性
|
||||
*/
|
||||
private Integer resultCode;
|
||||
/**
|
||||
* 结果提示
|
||||
*
|
||||
* 目前使用的 {@link CommonResult#getMsg()} 属性
|
||||
*/
|
||||
private String resultMsg;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.logger.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 错误日志
|
||||
*/
|
||||
@Data
|
||||
public class ApiErrorLogCreateReqDTO {
|
||||
|
||||
/**
|
||||
* 账号编号
|
||||
*/
|
||||
private Long userId = 0L;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType = 0;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@NotNull(message = "应用名不能为空")
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
@NotNull(message = "http 请求方法不能为空")
|
||||
private String requestMethod;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
@NotNull(message = "访问地址不能为空")
|
||||
private String requestUrl;
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
@NotNull(message = "请求参数不能为空")
|
||||
private String requestParams;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotNull(message = "ip 不能为空")
|
||||
private String userIp;
|
||||
/**
|
||||
* 浏览器 UA
|
||||
*/
|
||||
@NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 异常时间
|
||||
*/
|
||||
@NotNull(message = "异常时间不能为空")
|
||||
private LocalDateTime exceptionTime;
|
||||
/**
|
||||
* 异常名
|
||||
*/
|
||||
@NotNull(message = "异常名不能为空")
|
||||
private String exceptionName;
|
||||
/**
|
||||
* 异常发生的类全名
|
||||
*/
|
||||
@NotNull(message = "异常发生的类全名不能为空")
|
||||
private String exceptionClassName;
|
||||
/**
|
||||
* 异常发生的类文件
|
||||
*/
|
||||
@NotNull(message = "异常发生的类文件不能为空")
|
||||
private String exceptionFileName;
|
||||
/**
|
||||
* 异常发生的方法名
|
||||
*/
|
||||
@NotNull(message = "异常发生的方法名不能为空")
|
||||
private String exceptionMethodName;
|
||||
/**
|
||||
* 异常发生的方法所在行
|
||||
*/
|
||||
@NotNull(message = "异常发生的方法所在行不能为空")
|
||||
private Integer exceptionLineNumber;
|
||||
/**
|
||||
* 异常的栈轨迹异常的栈轨迹
|
||||
*/
|
||||
@NotNull(message = "异常的栈轨迹不能为空")
|
||||
private String exceptionStackTrace;
|
||||
/**
|
||||
* 异常导致的根消息
|
||||
*/
|
||||
@NotNull(message = "异常导致的根消息不能为空")
|
||||
private String exceptionRootCauseMessage;
|
||||
/**
|
||||
* 异常导致的消息
|
||||
*/
|
||||
@NotNull(message = "异常导致的消息不能为空")
|
||||
private String exceptionMessage;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package cn.lingniu.framework.plugin.web.apilog.logger.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 异常数据
|
||||
*/
|
||||
@Data
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
|
||||
public class ApiErrorLogDO {
|
||||
|
||||
/**
|
||||
* {@link #requestParams} 的最大长度
|
||||
*/
|
||||
public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000;
|
||||
|
||||
|
||||
/**
|
||||
* 应用名
|
||||
*
|
||||
* 目前读取 spring.application.name
|
||||
*/
|
||||
private String applicationName;
|
||||
|
||||
// ========== 请求相关字段 ==========
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
private String requestMethod;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
private String requestUrl;
|
||||
/**
|
||||
* 请求参数
|
||||
*
|
||||
* query: Query String
|
||||
* body: Quest Body
|
||||
*/
|
||||
private String requestParams;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
private String userIp;
|
||||
/**
|
||||
* 浏览器 UA
|
||||
*/
|
||||
private String userAgent;
|
||||
|
||||
// ========== 异常相关字段 ==========
|
||||
|
||||
/**
|
||||
* 异常发生时间
|
||||
*/
|
||||
private LocalDateTime exceptionTime;
|
||||
/**
|
||||
* 异常名
|
||||
*
|
||||
* {@link Throwable#getClass()} 的类全名
|
||||
*/
|
||||
private String exceptionName;
|
||||
/**
|
||||
* 异常导致的消息
|
||||
*
|
||||
* {@link cn.hutool.core.exceptions.ExceptionUtil#getMessage(Throwable)}
|
||||
*/
|
||||
private String exceptionMessage;
|
||||
/**
|
||||
* 异常导致的根消息
|
||||
*
|
||||
* {@link cn.hutool.core.exceptions.ExceptionUtil#getRootCauseMessage(Throwable)}
|
||||
*/
|
||||
private String exceptionRootCauseMessage;
|
||||
/**
|
||||
* 异常的栈轨迹
|
||||
*
|
||||
* {@link org.apache.commons.lang3.exception.ExceptionUtils#getStackTrace(Throwable)}
|
||||
*/
|
||||
private String exceptionStackTrace;
|
||||
/**
|
||||
* 异常发生的类全名
|
||||
*
|
||||
* {@link StackTraceElement#getClassName()}
|
||||
*/
|
||||
private String exceptionClassName;
|
||||
/**
|
||||
* 异常发生的类文件
|
||||
*
|
||||
* {@link StackTraceElement#getFileName()}
|
||||
*/
|
||||
private String exceptionFileName;
|
||||
/**
|
||||
* 异常发生的方法名
|
||||
*
|
||||
* {@link StackTraceElement#getMethodName()}
|
||||
*/
|
||||
private String exceptionMethodName;
|
||||
/**
|
||||
* 异常发生的方法所在行
|
||||
*
|
||||
* {@link StackTraceElement#getLineNumber()}
|
||||
*/
|
||||
private Integer exceptionLineNumber;
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.lingniu.framework.plugin.web.bae.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 可以过滤排除指定的url
|
||||
*
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public abstract class ApiRequestFilter extends OncePerRequestFilter {
|
||||
|
||||
protected final FrameworkWebConfig frameworkWebConfig;
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
if (Boolean.FALSE.equals(frameworkWebConfig.getApiLog().getEnable())) {
|
||||
return true;
|
||||
}
|
||||
if (CollectionUtils.isEmpty(frameworkWebConfig.getApiLog().getExcludeUrls())) {
|
||||
return false;
|
||||
}
|
||||
// 过滤 API 请求的地址
|
||||
String apiUri = request.getRequestURI().substring(request.getContextPath().length());
|
||||
for (String exclude : frameworkWebConfig.getApiLog().getExcludeUrls()) {
|
||||
if (StrUtil.startWithAny(apiUri, exclude)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.lingniu.framework.plugin.web.bae.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.lingniu.framework.plugin.util.servlet.ServletUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Request Body 缓存 Filter,实现它的可重复读取
|
||||
*/
|
||||
public class CacheRequestBodyFilter extends OncePerRequestFilter {
|
||||
|
||||
/**
|
||||
* 需要排除的 URI
|
||||
* 1. 排除 Spring Boot Admin 相关请求,避免客户端连接中断导致的异常。
|
||||
*/
|
||||
private static final String[] IGNORE_URIS = {"/admin/", "/actuator/"};
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(new CacheRequestBodyWrapper(request), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 1. 校验是否为排除的 URL
|
||||
String requestURI = request.getRequestURI();
|
||||
if (StrUtil.startWithAny(requestURI, IGNORE_URIS)) {
|
||||
return true;
|
||||
}
|
||||
// 2. 只处理 json 请求内容
|
||||
return !ServletUtils.isJsonRequest(request);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package cn.lingniu.framework.plugin.web.bae.filter;
|
||||
|
||||
|
||||
import cn.lingniu.framework.plugin.util.servlet.ServletUtils;
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* Request Body 缓存 Wrapper
|
||||
*/
|
||||
public class CacheRequestBodyWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
/**
|
||||
* 缓存的内容
|
||||
*/
|
||||
private final byte[] body;
|
||||
|
||||
public CacheRequestBodyWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
body = ServletUtils.getBodyBytes(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentLength() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthLong() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() {
|
||||
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
|
||||
// 返回 ServletInputStream
|
||||
return new ServletInputStream() {
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return inputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package cn.lingniu.framework.plugin.web.bae.handler;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.lingniu.framework.plugin.core.base.CommonResult;
|
||||
import cn.lingniu.framework.plugin.core.context.ApplicationNameContext;
|
||||
import cn.lingniu.framework.plugin.core.exception.ServerException;
|
||||
import cn.lingniu.framework.plugin.core.exception.ServiceException;
|
||||
import cn.lingniu.framework.plugin.core.exception.util.ServiceExceptionUtil;
|
||||
import cn.lingniu.framework.plugin.util.collection.SetUtils;
|
||||
import cn.lingniu.framework.plugin.util.json.JsonUtils;
|
||||
import cn.lingniu.framework.plugin.util.servlet.ServletUtils;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiErrorLogCommonApi;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO;
|
||||
import cn.lingniu.framework.plugin.web.bae.util.WebFrameworkUtils;
|
||||
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
|
||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import javax.validation.ValidationException;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants.*;
|
||||
|
||||
|
||||
/**
|
||||
* 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
|
||||
*/
|
||||
//@RestControllerAdvice
|
||||
@ControllerAdvice
|
||||
@ResponseBody
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 忽略的 ServiceException 错误提示,避免打印过多 logger
|
||||
*/
|
||||
public static final Set<String> IGNORE_ERROR_MESSAGES = SetUtils.asSet("无效的刷新令牌");
|
||||
|
||||
private final ApiErrorLogCommonApi apiErrorLogApi;
|
||||
|
||||
/**
|
||||
* 处理所有异常,主要是提供给 Filter 使用
|
||||
* 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。
|
||||
*
|
||||
* @param request 请求
|
||||
* @param ex 异常
|
||||
* @return 通用返回
|
||||
*/
|
||||
public CommonResult<?> allExceptionHandler(HttpServletRequest request, Throwable ex) {
|
||||
if (ex instanceof MissingServletRequestParameterException) {
|
||||
return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex);
|
||||
}
|
||||
if (ex instanceof MethodArgumentTypeMismatchException) {
|
||||
return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex);
|
||||
}
|
||||
if (ex instanceof MethodArgumentNotValidException) {
|
||||
return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex);
|
||||
}
|
||||
if (ex instanceof BindException) {
|
||||
return bindExceptionHandler((BindException) ex);
|
||||
}
|
||||
if (ex instanceof ConstraintViolationException) {
|
||||
return constraintViolationExceptionHandler((ConstraintViolationException) ex);
|
||||
}
|
||||
if (ex instanceof ValidationException) {
|
||||
return validationException((ValidationException) ex);
|
||||
}
|
||||
if (ex instanceof MaxUploadSizeExceededException) {
|
||||
return maxUploadSizeExceededExceptionHandler((MaxUploadSizeExceededException) ex);
|
||||
}
|
||||
if (ex instanceof NoHandlerFoundException) {
|
||||
return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
|
||||
}
|
||||
if (ex instanceof HttpRequestMethodNotSupportedException) {
|
||||
return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
|
||||
}
|
||||
if (ex instanceof HttpMediaTypeNotSupportedException) {
|
||||
return httpMediaTypeNotSupportedExceptionHandler((HttpMediaTypeNotSupportedException) ex);
|
||||
}
|
||||
if (ex instanceof ServiceException) {
|
||||
return serviceExceptionHandler((ServiceException) ex);
|
||||
}
|
||||
if (ex instanceof AccessDeniedException) {
|
||||
return accessDeniedExceptionHandler(request, (AccessDeniedException) ex);
|
||||
}
|
||||
return defaultExceptionHandler(request, ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 请求参数缺失
|
||||
*
|
||||
* 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数
|
||||
*/
|
||||
@ExceptionHandler(value = MissingServletRequestParameterException.class)
|
||||
public CommonResult<?> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) {
|
||||
log.warn("[missingServletRequestParameterExceptionHandler]", ex);
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 请求参数类型错误
|
||||
*
|
||||
* 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public CommonResult<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) {
|
||||
log.warn("[methodArgumentTypeMismatchExceptionHandler]", ex);
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 参数校验不正确
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public CommonResult<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) {
|
||||
log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex);
|
||||
// 获取 errorMessage
|
||||
String errorMessage = null;
|
||||
FieldError fieldError = ex.getBindingResult().getFieldError();
|
||||
if (fieldError == null) {
|
||||
// 组合校验,参考自 https://t.zsxq.com/3HVTx
|
||||
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
|
||||
if (CollUtil.isNotEmpty(allErrors)) {
|
||||
errorMessage = allErrors.get(0).getDefaultMessage();
|
||||
}
|
||||
} else {
|
||||
errorMessage = fieldError.getDefaultMessage();
|
||||
}
|
||||
// 转换 CommonResult
|
||||
if (StrUtil.isEmpty(errorMessage)) {
|
||||
return CommonResult.error(BAD_REQUEST);
|
||||
}
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", errorMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public CommonResult<?> bindExceptionHandler(BindException ex) {
|
||||
log.warn("[handleBindException]", ex);
|
||||
FieldError fieldError = ex.getFieldError();
|
||||
assert fieldError != null; // 断言,避免告警
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 请求参数类型错误
|
||||
*
|
||||
* 例如说,接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) {
|
||||
log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex);
|
||||
if (ex.getCause() instanceof InvalidFormatException) {
|
||||
InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause();
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue()));
|
||||
}
|
||||
if (StrUtil.startWith(ex.getMessage(), "Required request body is missing")) {
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), "请求参数类型错误: request body 缺失");
|
||||
}
|
||||
return defaultExceptionHandler(ServletUtils.getRequest(), ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Validator 校验不通过产生的异常
|
||||
*/
|
||||
@ExceptionHandler(value = ConstraintViolationException.class)
|
||||
public CommonResult<?> constraintViolationExceptionHandler(ConstraintViolationException ex) {
|
||||
log.warn("[constraintViolationExceptionHandler]", ex);
|
||||
ConstraintViolation<?> constraintViolation = ex.getConstraintViolations().iterator().next();
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常
|
||||
*/
|
||||
@ExceptionHandler(value = ValidationException.class)
|
||||
public CommonResult<?> validationException(ValidationException ex) {
|
||||
log.warn("[constraintViolationExceptionHandler]", ex);
|
||||
// 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读
|
||||
return CommonResult.error(BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理上传文件过大异常
|
||||
*/
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public CommonResult<?> maxUploadSizeExceededExceptionHandler(MaxUploadSizeExceededException ex) {
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), "上传文件过大,请调整后重试");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 请求地址不存在
|
||||
*
|
||||
* 注意,它需要设置如下两个配置项:
|
||||
* 1. spring.mvc.throw-exception-if-no-handler-found 为 true
|
||||
* 2. spring.mvc.static-path-pattern 为 /statics/**
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
public CommonResult<?> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) {
|
||||
log.warn("[noHandlerFoundExceptionHandler]", ex);
|
||||
return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 请求方法不正确
|
||||
*
|
||||
* 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public CommonResult<?> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) {
|
||||
log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex);
|
||||
return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 请求的 Content-Type 不正确
|
||||
*
|
||||
* 例如说,A 接口的 Content-Type 为 application/json,结果请求的 Content-Type 为 application/octet-stream,导致不匹配
|
||||
*/
|
||||
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
|
||||
public CommonResult<?> httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException ex) {
|
||||
log.warn("[httpMediaTypeNotSupportedExceptionHandler]", ex);
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求类型不正确:%s", ex.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Spring Security 权限不足的异常
|
||||
*
|
||||
* 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截
|
||||
*/
|
||||
@ExceptionHandler(value = AccessDeniedException.class)
|
||||
public CommonResult<?> accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) {
|
||||
log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req),
|
||||
req.getRequestURL(), ex);
|
||||
return CommonResult.error(FORBIDDEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Guava UncheckedExecutionException
|
||||
*
|
||||
* 例如说,缓存加载报错,可见 <a href="https://t.zsxq.com/UszdH">https://t.zsxq.com/UszdH</a>
|
||||
*/
|
||||
@ExceptionHandler(value = UncheckedExecutionException.class)
|
||||
public CommonResult<?> uncheckedExecutionExceptionHandler(HttpServletRequest req, UncheckedExecutionException ex) {
|
||||
return allExceptionHandler(req, ex.getCause());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常 ServiceException 例如说,商品库存不足,用户手机号已存在。
|
||||
*/
|
||||
@ExceptionHandler(value = ServiceException.class)
|
||||
public CommonResult<?> serviceExceptionHandler(ServiceException ex) {
|
||||
// 不包含的时候,才进行打印,避免 ex 堆栈过多
|
||||
if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) {
|
||||
// 即使打印,也只打印第一层 StackTraceElement,并且使用 warn 在控制台输出,更容易看到
|
||||
try {
|
||||
StackTraceElement[] stackTraces = ex.getStackTrace();
|
||||
for (StackTraceElement stackTrace : stackTraces) {
|
||||
if (ObjUtil.notEqual(stackTrace.getClassName(), ServiceExceptionUtil.class.getName())) {
|
||||
log.warn("[serviceExceptionHandler]\n\t{}", stackTrace);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 忽略日志,避免影响主流程
|
||||
}
|
||||
}
|
||||
return CommonResult.error(ex.getCode(), ex.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常 ServiceException 例如说,商品库存不足,用户手机号已存在。
|
||||
*/
|
||||
@ExceptionHandler(value = ServerException.class)
|
||||
public CommonResult<?> serverExceptionHandler(ServerException ex) {
|
||||
try {
|
||||
StackTraceElement[] stackTraces = ex.getStackTrace();
|
||||
for (StackTraceElement stackTrace : stackTraces) {
|
||||
if (ObjUtil.notEqual(stackTrace.getClassName(), ServiceExceptionUtil.class.getName())) {
|
||||
log.warn("[serverExceptionHandler]\n\t{}", stackTrace);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 忽略日志,避免影响主流程
|
||||
}
|
||||
return CommonResult.error(ex.getCode(), ex.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统异常,兜底处理所有的一切
|
||||
*/
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) {
|
||||
// 特殊:如果是 ServiceException 的异常,则直接返回
|
||||
if (ex.getCause() != null && ex.getCause() instanceof ServiceException) {
|
||||
return serviceExceptionHandler((ServiceException) ex.getCause());
|
||||
}
|
||||
if (ex.getCause() != null && ex.getCause() instanceof ServerException) {
|
||||
return serviceExceptionHandler((ServiceException) ex.getCause());
|
||||
}
|
||||
// 情况二:处理异常
|
||||
log.error("[defaultExceptionHandler]", ex);
|
||||
// 插入异常日志
|
||||
createExceptionLog(req, ex);
|
||||
// 返回 ERROR CommonResult
|
||||
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
|
||||
}
|
||||
|
||||
private void createExceptionLog(HttpServletRequest req, Throwable e) {
|
||||
// 插入错误日志
|
||||
ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO();
|
||||
try {
|
||||
// 初始化 errorLog
|
||||
buildExceptionLog(errorLog, req, e);
|
||||
// 执行插入 errorLog
|
||||
apiErrorLogApi.createApiErrorLogAsync(errorLog);
|
||||
} catch (Throwable th) {
|
||||
log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th);
|
||||
}
|
||||
}
|
||||
|
||||
private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) {
|
||||
// 处理用户信息
|
||||
errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
|
||||
errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request));
|
||||
// 设置异常字段
|
||||
errorLog.setExceptionName(e.getClass().getName());
|
||||
errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));
|
||||
errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
|
||||
errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e));
|
||||
StackTraceElement[] stackTraceElements = e.getStackTrace();
|
||||
Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
|
||||
StackTraceElement stackTraceElement = stackTraceElements[0];
|
||||
errorLog.setExceptionClassName(stackTraceElement.getClassName());
|
||||
errorLog.setExceptionFileName(stackTraceElement.getFileName());
|
||||
errorLog.setExceptionMethodName(stackTraceElement.getMethodName());
|
||||
errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
|
||||
// 设置其它字段
|
||||
errorLog.setApplicationName(ApplicationNameContext.getApplicationName());
|
||||
errorLog.setRequestUrl(request.getRequestURI());
|
||||
Map<String, Object> requestParams = MapUtil.<String, Object>builder()
|
||||
.put("query", ServletUtils.getParamMap(request))
|
||||
.put("body", ServletUtils.getBody(request)).build();
|
||||
errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
|
||||
errorLog.setRequestMethod(request.getMethod());
|
||||
errorLog.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
errorLog.setUserIp(ServletUtils.getClientIP(request));
|
||||
errorLog.setExceptionTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.lingniu.framework.plugin.web.bae.handler;
|
||||
|
||||
import cn.lingniu.framework.plugin.core.base.CommonResult;
|
||||
import cn.lingniu.framework.plugin.web.bae.util.WebFrameworkUtils;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
/**
|
||||
* 全局响应结果(ResponseBody)处理器
|
||||
*
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class GlobalResponseBodyHandler implements ResponseBodyAdvice {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullableProblems") // 避免 IDEA 警告
|
||||
public boolean supports(MethodParameter returnType, Class converterType) {
|
||||
if (returnType.getMethod() == null) {
|
||||
return false;
|
||||
}
|
||||
// 只拦截返回结果为 CommonResult 类型
|
||||
return returnType.getMethod().getReturnType() == CommonResult.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullableProblems") // 避免 IDEA 警告
|
||||
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,
|
||||
ServerHttpRequest request, ServerHttpResponse response) {
|
||||
// 记录 Controller 结果
|
||||
if (returnType.getMethod().getReturnType() == CommonResult.class) {
|
||||
WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult<?>) body);
|
||||
return body;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cn.lingniu.framework.plugin.web.bae.interceptor;
|
||||
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.lingniu.framework.plugin.core.base.CommonResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Order(Ordered.LOWEST_PRECEDENCE + 1000)
|
||||
public class ResponseCheckInterceptor extends HandlerInterceptorAdapter implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
|
||||
if (!(o instanceof HandlerMethod)) {
|
||||
return;
|
||||
}
|
||||
HandlerMethod handlerMethod = (HandlerMethod) o;
|
||||
boolean flag = ClassUtil.isAssignable(CommonResult.class, handlerMethod.getMethod().getReturnType())
|
||||
|| ClassUtil.isAssignable(Iterable.class, handlerMethod.getMethod().getReturnType());
|
||||
if (log.isErrorEnabled() && !flag) {
|
||||
log.error("\r\n\t {} 接口响应返回类型为:{} 不符合规范\r\n\t\t返回类型必须是:CommonResult,响应规范请查看:GlobalResponseBodyHandler, 异常响应体实现细则请查看:cn.lingniu.framework.plugin.web.base.handler.GlobalExceptionHandler",
|
||||
handlerMethod.getMethod().getName(), handlerMethod.getMethod().getReturnType().getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package cn.lingniu.framework.plugin.web.bae.util;
|
||||
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.lingniu.framework.plugin.core.base.CommonResult;
|
||||
import cn.lingniu.framework.plugin.core.base.TerminalEnum;
|
||||
import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 专属于 web 包的工具类
|
||||
*/
|
||||
public class WebFrameworkUtils {
|
||||
|
||||
//todo 后续扩展
|
||||
private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id";
|
||||
private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type";
|
||||
//todo 后续扩展
|
||||
|
||||
private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
|
||||
|
||||
/**
|
||||
* 终端的 Header
|
||||
*
|
||||
* @see TerminalEnum
|
||||
*/
|
||||
public static final String HEADER_TERMINAL = "terminal";
|
||||
|
||||
private static FrameworkWebConfig frameworkWebConfig;
|
||||
|
||||
public WebFrameworkUtils(FrameworkWebConfig frameworkWebConfig) {
|
||||
WebFrameworkUtils.frameworkWebConfig = frameworkWebConfig;
|
||||
}
|
||||
|
||||
public static void setLoginUserId(ServletRequest request, Long userId) {
|
||||
request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户类型
|
||||
*
|
||||
* @param request 请求
|
||||
* @param userType 用户类型
|
||||
*/
|
||||
public static void setLoginUserType(ServletRequest request, Integer userType) {
|
||||
request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户的编号,从请求中
|
||||
* 注意:该方法仅限于 framework 框架使用!!!
|
||||
*
|
||||
* @param request 请求
|
||||
* @return 用户编号
|
||||
*/
|
||||
public static Long getLoginUserId(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户的类型
|
||||
* 注意:该方法仅限于 web 相关的 framework 组件使用!!!
|
||||
*
|
||||
* @param request 请求
|
||||
* @return 用户编号
|
||||
*/
|
||||
public static Integer getLoginUserType(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
// 1. 优先,从 Attribute 中获取
|
||||
Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE);
|
||||
if (userType != null) {
|
||||
return userType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Integer getLoginUserType() {
|
||||
HttpServletRequest request = getRequest();
|
||||
return getLoginUserType(request);
|
||||
}
|
||||
|
||||
public static Long getLoginUserId() {
|
||||
HttpServletRequest request = getRequest();
|
||||
return getLoginUserId(request);
|
||||
}
|
||||
|
||||
public static Integer getTerminal() {
|
||||
HttpServletRequest request = getRequest();
|
||||
if (request == null) {
|
||||
return TerminalEnum.UNKNOWN.getTerminal();
|
||||
}
|
||||
String terminalValue = request.getHeader(HEADER_TERMINAL);
|
||||
return NumberUtil.parseInt(terminalValue, TerminalEnum.UNKNOWN.getTerminal());
|
||||
}
|
||||
|
||||
public static void setCommonResult(ServletRequest request, CommonResult<?> result) {
|
||||
request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result);
|
||||
}
|
||||
|
||||
public static CommonResult<?> getCommonResult(ServletRequest request) {
|
||||
return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public static HttpServletRequest getRequest() {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (!(requestAttributes instanceof ServletRequestAttributes)) {
|
||||
return null;
|
||||
}
|
||||
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
|
||||
return servletRequestAttributes.getRequest();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package cn.lingniu.framework.plugin.web.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* HTTP API 加解密配置
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
public class ApiEncryptProperties {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
private Boolean enable = false;
|
||||
|
||||
/**
|
||||
* 请求头(响应头)名称不能为空
|
||||
*
|
||||
* 1. 如果该请求头非空,则表示请求参数已被「前端」加密,「后端」需要解密
|
||||
* 2. 如果该响应头非空,则表示响应结果已被「后端」加密,「前端」需要解密
|
||||
*/
|
||||
private String header = "X-Api-Encrypt";
|
||||
|
||||
/**
|
||||
* 对称加密算法,用于请求/响应的加解密---对称加密算法不能为空
|
||||
*
|
||||
* 目前支持
|
||||
* 【对称加密】:
|
||||
* 1. {@link cn.hutool.crypto.symmetric.SymmetricAlgorithm#AES}
|
||||
* 2. {@link cn.hutool.crypto.symmetric.SM4#ALGORITHM_NAME} (需要自己二次开发,成本低)
|
||||
* 【非对称加密】
|
||||
* 1. {@link cn.hutool.crypto.asymmetric.AsymmetricAlgorithm#RSA}
|
||||
* 2. {@link cn.hutool.crypto.asymmetric.SM2} (需要自己二次开发,成本低)
|
||||
*
|
||||
* @see <a href="https://help.aliyun.com/zh/ssl-certificate/what-are-a-public-key-and-a-private-key">什么是公钥和私钥?</a>
|
||||
*/
|
||||
private String algorithm = "AES";
|
||||
|
||||
/**
|
||||
* 请求的解密密钥---请求的解密密钥不能为空
|
||||
*
|
||||
* 注意:
|
||||
* 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。
|
||||
* 2. 如果是【非对称加密】时,它「后端」对应的是“私钥”。对应的,「前端」对应的是“公钥”。(重要!!!)
|
||||
*/
|
||||
private String requestKey = "";
|
||||
|
||||
/**
|
||||
* 响应的加密密钥-- 响应的加密密钥不能为空
|
||||
*
|
||||
* 注意:
|
||||
* 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。
|
||||
* 2. 如果是【非对称加密】时,它「后端」对应的是“公钥”。对应的,「前端」对应的是“私钥”。(重要!!!)
|
||||
*/
|
||||
private String responseKey = "";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package cn.lingniu.framework.plugin.web.config;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ApiLogProperties {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
private Boolean enable = false;
|
||||
|
||||
/**
|
||||
* 排除的url
|
||||
*/
|
||||
private List<String> excludeUrls = Lists.newArrayList();
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.lingniu.framework.plugin.web.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 可扩展多种公共属性配置,时区等问题
|
||||
*/
|
||||
@ConfigurationProperties(prefix = FrameworkWebConfig.PRE_FIX)
|
||||
@Data
|
||||
public class FrameworkWebConfig {
|
||||
|
||||
public static final String PRE_FIX = "framework.lingniu.web";
|
||||
|
||||
|
||||
/**
|
||||
* 接口请求日志
|
||||
*/
|
||||
private ApiLogProperties apiLog = new ApiLogProperties();
|
||||
|
||||
/**
|
||||
* api接口加解密
|
||||
*/
|
||||
private ApiEncryptProperties apiEncrypt = new ApiEncryptProperties();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.lingniu.framework.plugin.web.encrypt;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* HTTP API 加解密注解
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ApiEncrypt {
|
||||
|
||||
/**
|
||||
* 是否对请求参数进行解密,默认 true
|
||||
*/
|
||||
boolean request() default true;
|
||||
|
||||
/**
|
||||
* 是否对响应结果进行加密,默认 true
|
||||
*/
|
||||
boolean response() default true;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package cn.lingniu.framework.plugin.web.encrypt.filter;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.asymmetric.AsymmetricDecryptor;
|
||||
import cn.hutool.crypto.asymmetric.KeyType;
|
||||
import cn.hutool.crypto.symmetric.SymmetricDecryptor;
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* 解密请求 {@link HttpServletRequestWrapper} 实现类
|
||||
*/
|
||||
public class ApiDecryptRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final byte[] body;
|
||||
|
||||
public ApiDecryptRequestWrapper(HttpServletRequest request,
|
||||
SymmetricDecryptor symmetricDecryptor,
|
||||
AsymmetricDecryptor asymmetricDecryptor) throws IOException {
|
||||
super(request);
|
||||
// 读取 body,允许 HEX、BASE64 传输
|
||||
String requestBody = StrUtil.utf8Str(
|
||||
IoUtil.readBytes(request.getInputStream(), false));
|
||||
|
||||
// 解密 body
|
||||
body = symmetricDecryptor != null ? symmetricDecryptor.decrypt(requestBody)
|
||||
: asymmetricDecryptor.decrypt(requestBody, KeyType.PrivateKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentLength() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthLong() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() {
|
||||
ByteArrayInputStream stream = new ByteArrayInputStream(body);
|
||||
return new ServletInputStream() {
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return stream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package cn.lingniu.framework.plugin.web.encrypt.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import cn.hutool.crypto.asymmetric.AsymmetricDecryptor;
|
||||
import cn.hutool.crypto.asymmetric.AsymmetricEncryptor;
|
||||
import cn.hutool.crypto.symmetric.SymmetricDecryptor;
|
||||
import cn.hutool.crypto.symmetric.SymmetricEncryptor;
|
||||
import cn.lingniu.framework.plugin.core.base.CommonResult;
|
||||
import cn.lingniu.framework.plugin.core.exception.util.ServiceExceptionUtil;
|
||||
import cn.lingniu.framework.plugin.util.object.ObjectUtils;
|
||||
import cn.lingniu.framework.plugin.util.servlet.ServletUtils;
|
||||
import cn.lingniu.framework.plugin.web.config.ApiEncryptProperties;
|
||||
import cn.lingniu.framework.plugin.web.encrypt.ApiEncrypt;
|
||||
import cn.lingniu.framework.plugin.web.bae.handler.GlobalExceptionHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerExecutionChain;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
/**
|
||||
* API 加密过滤器,处理 {@link ApiEncrypt} 注解。
|
||||
* 1. 解密请求参数
|
||||
* 2. 加密响应结果
|
||||
*
|
||||
* 疑问:为什么不使用 SpringMVC 的 RequestBodyAdvice 或 ResponseBodyAdvice 机制呢?
|
||||
* 回答:考虑到项目中会记录访问日志、异常日志,以及 HTTP API 签名等场景,最好是全局级、且提前做解析!!!
|
||||
*/
|
||||
@Slf4j
|
||||
public class ApiEncryptFilter extends OncePerRequestFilter {
|
||||
|
||||
private final ApiEncryptProperties apiEncryptProperties;
|
||||
|
||||
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||
|
||||
private final GlobalExceptionHandler globalExceptionHandler;
|
||||
|
||||
private final SymmetricDecryptor requestSymmetricDecryptor;
|
||||
private final AsymmetricDecryptor requestAsymmetricDecryptor;
|
||||
|
||||
private final SymmetricEncryptor responseSymmetricEncryptor;
|
||||
private final AsymmetricEncryptor responseAsymmetricEncryptor;
|
||||
|
||||
public ApiEncryptFilter(ApiEncryptProperties apiEncryptProperties,
|
||||
RequestMappingHandlerMapping requestMappingHandlerMapping,
|
||||
GlobalExceptionHandler globalExceptionHandler) {
|
||||
this.apiEncryptProperties = apiEncryptProperties;
|
||||
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
|
||||
this.globalExceptionHandler = globalExceptionHandler;
|
||||
if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "AES")) {
|
||||
this.requestSymmetricDecryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getRequestKey()));
|
||||
this.requestAsymmetricDecryptor = null;
|
||||
this.responseSymmetricEncryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getResponseKey()));
|
||||
this.responseAsymmetricEncryptor = null;
|
||||
} else if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "RSA")) {
|
||||
this.requestSymmetricDecryptor = null;
|
||||
this.requestAsymmetricDecryptor = SecureUtil.rsa(apiEncryptProperties.getRequestKey(), null);
|
||||
this.responseSymmetricEncryptor = null;
|
||||
this.responseAsymmetricEncryptor = SecureUtil.rsa(null, apiEncryptProperties.getResponseKey());
|
||||
} else {
|
||||
// 补充说明:如果要支持 SM2、SM4 等算法,可在此处增加对应实例的创建,并添加相应的 Maven 依赖即可。
|
||||
throw new IllegalArgumentException("不支持的加密算法:" + apiEncryptProperties.getAlgorithm());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullableProblems")
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
// 获取 @ApiEncrypt 注解
|
||||
ApiEncrypt apiEncrypt = getApiEncrypt(request);
|
||||
boolean requestEnable = apiEncrypt != null && apiEncrypt.request();
|
||||
boolean responseEnable = apiEncrypt != null && apiEncrypt.response();
|
||||
String encryptHeader = request.getHeader(apiEncryptProperties.getHeader());
|
||||
if (!requestEnable && !responseEnable && StrUtil.isBlank(encryptHeader)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 解密请求
|
||||
if (ObjectUtils.equalsAny(HttpMethod.valueOf(request.getMethod()),
|
||||
HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)) {
|
||||
try {
|
||||
if (StrUtil.isNotBlank(encryptHeader)) {
|
||||
request = new ApiDecryptRequestWrapper(request,
|
||||
requestSymmetricDecryptor, requestAsymmetricDecryptor);
|
||||
} else if (requestEnable) {
|
||||
throw ServiceExceptionUtil.invalidParamException("请求未包含加密标头,请检查是否正确配置了加密标头");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
|
||||
ServletUtils.writeJSON(response, result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 执行过滤器链
|
||||
if (responseEnable) {
|
||||
// 特殊:仅包装,最后执行。目的:Response 内容可以被重复读取!!!
|
||||
response = new ApiEncryptResponseWrapper(response);
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
|
||||
// 3. 加密响应(真正执行)
|
||||
if (responseEnable) {
|
||||
((ApiEncryptResponseWrapper) response).encrypt(apiEncryptProperties,
|
||||
responseSymmetricEncryptor, responseAsymmetricEncryptor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 @ApiEncrypt 注解
|
||||
*
|
||||
* @param request 请求
|
||||
*/
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
private ApiEncrypt getApiEncrypt(HttpServletRequest request) {
|
||||
try {
|
||||
// 特殊:兼容 SpringBoot 2.X 版本会报错的问题 https://t.zsxq.com/kqyiB
|
||||
if (!ServletRequestPathUtils.hasParsedRequestPath(request)) {
|
||||
ServletRequestPathUtils.parseAndCache(request);
|
||||
}
|
||||
|
||||
// 解析 @ApiEncrypt 注解
|
||||
HandlerExecutionChain mappingHandler = requestMappingHandlerMapping.getHandler(request);
|
||||
if (mappingHandler == null) {
|
||||
return null;
|
||||
}
|
||||
Object handler = mappingHandler.getHandler();
|
||||
if (handler instanceof HandlerMethod) {
|
||||
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||
ApiEncrypt annotation = handlerMethod.getMethodAnnotation(ApiEncrypt.class);
|
||||
if (annotation == null) {
|
||||
annotation = handlerMethod.getBeanType().getAnnotation(ApiEncrypt.class);
|
||||
}
|
||||
return annotation;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[getApiEncrypt][url({}/{}) 获取 @ApiEncrypt 注解失败]",
|
||||
request.getRequestURI(), request.getMethod(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package cn.lingniu.framework.plugin.web.encrypt.filter;
|
||||
|
||||
import cn.hutool.crypto.asymmetric.AsymmetricEncryptor;
|
||||
import cn.hutool.crypto.asymmetric.KeyType;
|
||||
import cn.hutool.crypto.symmetric.SymmetricEncryptor;
|
||||
import cn.lingniu.framework.plugin.web.config.ApiEncryptProperties;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.WriteListener;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpServletResponseWrapper;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* 加密响应 {@link HttpServletResponseWrapper} 实现类
|
||||
*
|
||||
*/
|
||||
public class ApiEncryptResponseWrapper extends HttpServletResponseWrapper {
|
||||
|
||||
private final ByteArrayOutputStream byteArrayOutputStream;
|
||||
private final ServletOutputStream servletOutputStream;
|
||||
private final PrintWriter printWriter;
|
||||
|
||||
public ApiEncryptResponseWrapper(HttpServletResponse response) {
|
||||
super(response);
|
||||
this.byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
this.servletOutputStream = this.getOutputStream();
|
||||
this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream));
|
||||
}
|
||||
|
||||
public void encrypt(ApiEncryptProperties properties,
|
||||
SymmetricEncryptor symmetricEncryptor,
|
||||
AsymmetricEncryptor asymmetricEncryptor) throws IOException {
|
||||
// 1.1 清空 body
|
||||
HttpServletResponse response = (HttpServletResponse) this.getResponse();
|
||||
response.resetBuffer();
|
||||
// 1.2 获取 body
|
||||
this.flushBuffer();
|
||||
byte[] body = byteArrayOutputStream.toByteArray();
|
||||
// 2. 添加加密 header 标识
|
||||
this.addHeader(properties.getHeader(), "true");
|
||||
// 特殊:特殊:https://juejin.cn/post/6867327674675625992
|
||||
this.addHeader("Access-Control-Expose-Headers", properties.getHeader());
|
||||
|
||||
// 3.1 加密 body
|
||||
String encryptedBody = symmetricEncryptor != null ? symmetricEncryptor.encryptBase64(body)
|
||||
: asymmetricEncryptor.encryptBase64(body, KeyType.PublicKey);
|
||||
// 3.2 输出加密后的 body:(设置 header 要放在 response 的 write 之前)
|
||||
response.getWriter().write(encryptedBody);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter getWriter() {
|
||||
return printWriter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flushBuffer() throws IOException {
|
||||
if (servletOutputStream != null) {
|
||||
servletOutputStream.flush();
|
||||
}
|
||||
if (printWriter != null) {
|
||||
printWriter.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
byteArrayOutputStream.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletOutputStream getOutputStream() {
|
||||
return new ServletOutputStream() {
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWriteListener(WriteListener writeListener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) {
|
||||
byteArrayOutputStream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullableProblems")
|
||||
public void write(byte[] b) throws IOException {
|
||||
byteArrayOutputStream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullableProblems")
|
||||
public void write(byte[] b, int off, int len) {
|
||||
byteArrayOutputStream.write(b, off, len);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cn.lingniu.framework.plugin.web.init;
|
||||
|
||||
import cn.lingniu.framework.plugin.core.base.WebFilterOrderEnum;
|
||||
import cn.lingniu.framework.plugin.web.config.ApiEncryptProperties;
|
||||
import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig;
|
||||
import cn.lingniu.framework.plugin.web.encrypt.filter.ApiEncryptFilter;
|
||||
import cn.lingniu.framework.plugin.web.bae.handler.GlobalExceptionHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
|
||||
@AutoConfiguration
|
||||
@Slf4j
|
||||
@EnableConfigurationProperties(ApiEncryptProperties.class)
|
||||
@ConditionalOnProperty(prefix = "framework.lingniu.web.apiEncrypt", name = "enable", havingValue = "true")
|
||||
public class ApiEncryptAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<ApiEncryptFilter> apiEncryptFilter(FrameworkWebConfig frameworkWebConfig,
|
||||
RequestMappingHandlerMapping requestMappingHandlerMapping,
|
||||
GlobalExceptionHandler globalExceptionHandler) {
|
||||
ApiEncryptFilter filter = new ApiEncryptFilter(frameworkWebConfig.getApiEncrypt(),
|
||||
requestMappingHandlerMapping, globalExceptionHandler);
|
||||
FilterRegistrationBean<ApiEncryptFilter> bean = new FilterRegistrationBean<>(filter);
|
||||
bean.setOrder(WebFilterOrderEnum.API_ENCRYPT_FILTER);
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package cn.lingniu.framework.plugin.web.init;
|
||||
|
||||
import cn.lingniu.framework.plugin.core.base.WebFilterOrderEnum;
|
||||
import cn.lingniu.framework.plugin.web.apilog.filter.ApiAccessLogFilter;
|
||||
import cn.lingniu.framework.plugin.web.apilog.interceptor.ApiAccessLogInterceptor;
|
||||
import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogCommonApi;
|
||||
import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
|
||||
@AutoConfiguration(after = WebAutoConfiguration.class)
|
||||
public class ApiLogAutoConfiguration implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 创建 ApiAccessLogFilter Bean,记录 API 请求日志
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "framework.lingniu.web.apiLog", value = "enable", matchIfMissing = false)
|
||||
public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(FrameworkWebConfig frameworkWebConfig,
|
||||
@Value("${spring.application.name}") String applicationName,
|
||||
ApiAccessLogCommonApi apiAccessLogApi) {
|
||||
ApiAccessLogFilter filter = new ApiAccessLogFilter(frameworkWebConfig, applicationName, apiAccessLogApi);
|
||||
return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER);
|
||||
}
|
||||
|
||||
private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
|
||||
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
|
||||
bean.setOrder(order);
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new ApiAccessLogInterceptor());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.lingniu.framework.plugin.web.init;
|
||||
|
||||
import cn.lingniu.framework.plugin.web.bae.handler.GlobalResponseBodyHandler;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 待扩展
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE})
|
||||
@Import({GlobalResponseBodyHandler.class})
|
||||
public @interface EnableGlobalResponseBody {
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package cn.lingniu.framework.plugin.web.init;
|
||||
|
||||
import cn.lingniu.framework.plugin.util.json.JsonUtils;
|
||||
import cn.lingniu.framework.plugin.util.json.databind.NumberSerializer;
|
||||
import cn.lingniu.framework.plugin.util.json.databind.TimestampLocalDateTimeDeserializer;
|
||||
import cn.lingniu.framework.plugin.util.json.databind.TimestampLocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
|
||||
@AutoConfiguration(after = org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.class)
|
||||
@Slf4j
|
||||
public class JacksonAutoConfiguration {
|
||||
|
||||
/**
|
||||
* 从 Builder 源头定制(关键:使用 *ByType,避免 handledType 要求)
|
||||
*/
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer ldtEpochMillisCustomizer() {
|
||||
return builder -> builder
|
||||
// Long -> Number
|
||||
.serializerByType(Long.class, NumberSerializer.INSTANCE)
|
||||
.serializerByType(Long.TYPE, NumberSerializer.INSTANCE)
|
||||
// LocalDate / LocalTime
|
||||
.serializerByType(LocalDate.class, LocalDateSerializer.INSTANCE)
|
||||
.deserializerByType(LocalDate.class, LocalDateDeserializer.INSTANCE)
|
||||
.serializerByType(LocalTime.class, LocalTimeSerializer.INSTANCE)
|
||||
.deserializerByType(LocalTime.class, LocalTimeDeserializer.INSTANCE)
|
||||
// LocalDateTime < - > EpochMillis
|
||||
.serializerByType(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
|
||||
.deserializerByType(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 以 Bean 形式暴露 Module(Boot 会自动注册到所有 ObjectMapper)
|
||||
*/
|
||||
@Bean
|
||||
public Module timestampSupportModuleBean() {
|
||||
SimpleModule m = new SimpleModule("TimestampSupportModule");
|
||||
// Long -> Number,避免前端精度丢失
|
||||
m.addSerializer(Long.class, NumberSerializer.INSTANCE);
|
||||
m.addSerializer(Long.TYPE, NumberSerializer.INSTANCE);
|
||||
// LocalDate / LocalTime
|
||||
m.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE);
|
||||
m.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE);
|
||||
m.addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE);
|
||||
m.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE);
|
||||
// LocalDateTime < - > EpochMillis
|
||||
m.addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE);
|
||||
m.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化全局 JsonUtils,直接使用主 ObjectMapper
|
||||
*/
|
||||
@Bean
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
public JsonUtils jsonUtils(ObjectMapper objectMapper) {
|
||||
JsonUtils.init(objectMapper);
|
||||
log.debug("[init][初始化 JsonUtils 成功]");
|
||||
return new JsonUtils();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package cn.lingniu.framework.plugin.web.init;
|
||||
|
||||
import cn.lingniu.framework.plugin.core.config.CommonConstant;
|
||||
import cn.lingniu.framework.plugin.web.bae.interceptor.ResponseCheckInterceptor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ConditionalOnMissingClass({"org.springframework.security.authentication.AuthenticationManager"})
|
||||
public class ResponseDetectAutoconfiguration implements CommandLineRunner, WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
ResponseCheckInterceptor responseCheckInterceptor;
|
||||
|
||||
/**
|
||||
* 默认排除URL清单
|
||||
*/
|
||||
public static final Set<String> excludesPattern= new HashSet<>(
|
||||
Arrays.asList("*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*,/doc.html,/webjars/**,/swagger-ui.html/**,/swagger-resources/**,/v2/api-docs,/actuator/**,,/favicon.ico"
|
||||
.split("\\s*,\\s*")));
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
List<String> urls = new ArrayList<>();
|
||||
urls.addAll(excludesPattern.stream().collect(Collectors.toList()));
|
||||
registry.addInterceptor(responseCheckInterceptor)
|
||||
.addPathPatterns( Collections.singletonList("/**") )
|
||||
.excludePathPatterns(urls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("\r\n\t\tDEV/SIT/UAT/TEST 环境已开启返回响应体规范检测功能,请按lingniu-web框架规范使用响应体结构~~");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.lingniu.framework.plugin.web.init;
|
||||
|
||||
import cn.lingniu.framework.plugin.web.bae.interceptor.ResponseCheckInterceptor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ConditionalOnMissingClass({"org.springframework.security.authentication.AuthenticationManager"})
|
||||
public class ResponseInterceptorAutoconfiguration {
|
||||
@Bean
|
||||
public ResponseCheckInterceptor responseInterceptor(){
|
||||
return new ResponseCheckInterceptor();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package cn.lingniu.framework.plugin.web.init;
|
||||
|
||||
import cn.lingniu.framework.plugin.core.base.WebFilterOrderEnum;
|
||||
import cn.lingniu.framework.plugin.web.ApplicationStartEventListener;
|
||||
import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig;
|
||||
import cn.lingniu.framework.plugin.web.bae.filter.CacheRequestBodyFilter;
|
||||
import cn.lingniu.framework.plugin.web.bae.handler.GlobalExceptionHandler;
|
||||
import cn.lingniu.framework.plugin.web.bae.util.WebFrameworkUtils;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
import javax.servlet.Filter;
|
||||
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(FrameworkWebConfig.class)
|
||||
@Import({GlobalExceptionHandler.class, ApplicationStartEventListener.class})
|
||||
public class WebAutoConfiguration {
|
||||
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
public WebFrameworkUtils webFrameworkUtils(FrameworkWebConfig frameworkWebConfig) {
|
||||
// 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean
|
||||
return new WebFrameworkUtils(frameworkWebConfig);
|
||||
}
|
||||
|
||||
// ========== Filter 相关 ==========
|
||||
|
||||
/**
|
||||
* 创建 CorsFilter Bean,解决跨域问题
|
||||
*/
|
||||
@Bean
|
||||
@Order(value = WebFilterOrderEnum.CORS_FILTER) // 特殊:修复因执行顺序影响到跨域配置不生效问题
|
||||
public FilterRegistrationBean<CorsFilter> corsFilterBean() {
|
||||
// 创建 CorsConfiguration 对象
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowCredentials(true);
|
||||
config.addAllowedOriginPattern("*"); // 设置访问源地址
|
||||
config.addAllowedHeader("*"); // 设置访问源请求头
|
||||
config.addAllowedMethod("*"); // 设置访问源请求方法
|
||||
// 创建 UrlBasedCorsConfigurationSource 对象
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置
|
||||
return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 RequestBodyCacheFilter Bean,可重复读取请求内容
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<CacheRequestBodyFilter> requestBodyCacheFilter() {
|
||||
return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER);
|
||||
}
|
||||
|
||||
|
||||
public static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
|
||||
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
|
||||
bean.setOrder(order);
|
||||
return bean;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
cn.lingniu.framework.plugin.web.init.JacksonAutoConfiguration,\
|
||||
cn.lingniu.framework.plugin.web.init.WebAutoConfiguration,\
|
||||
cn.lingniu.framework.plugin.web.init.ResponseInterceptorAutoconfiguration,\
|
||||
cn.lingniu.framework.plugin.web.init.ResponseDetectAutoconfiguration,\
|
||||
cn.lingniu.framework.plugin.web.init.ApiLogAutoConfiguration,\
|
||||
cn.lingniu.framework.plugin.web.init.ApiEncryptAutoConfiguration
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# 框架核心web应用模块
|
||||
|
||||
|
||||
## 概述 (Overview)
|
||||
|
||||
1. 定位: 基于 springmvc封装的规范web组件
|
||||
2. 核心能力:
|
||||
* 统一返回结果检测:非生产环境接口返回参数不规范,控制台会打印error日志。
|
||||
* 请求日志:支持注解方式规范打印请求日志,可扩展调用db。
|
||||
* 参数加解密:可解密请求参数,加密响应结果。
|
||||
* 全局异常处理机制:GlobalExceptionHandler
|
||||
* 统一结果返回处理:GlobalResponseBodyHandler
|
||||
3. 适用场景:
|
||||
* Web应用公共功能
|
||||
|
||||
|
||||
## 统一返回结果检测
|
||||
- 检测条件:profile:test","dev","uat","sit
|
||||
- 检测实现类:ResponseCheckInterceptor
|
||||
- 加载类:
|
||||
ResponseDetectAutoconfiguration
|
||||
ResponseInterceptorAutoconfiguration
|
||||
|
||||
## 请求日志
|
||||
- 使用方式:@ApiAccessLog
|
||||
- 参数配置请查看:FrameworkWebConfig.ApiLogProperties
|
||||
- filter:ApiAccessLogFilter
|
||||
- 最终输出打印:ApiAccessLogServiceImpl
|
||||
|
||||
|
||||
## 统一返回结果处理:CommonResult
|
||||
- 实现参考:GlobalResponseBodyHandler
|
||||
|
||||
## 统一异常处理:GlobalExceptionHandler,并记录日志-ApiErrorLogServiceImpl
|
||||
|
||||
## api 加解密
|
||||
- 使用方式:@ApiEncrypt
|
||||
- 参数配置请查看:FrameworkWebConfig.ApiEncryptProperties
|
||||
- filter:ApiEncryptFilter
|
||||
- 逻辑处理:ApiDecryptRequestWrapper、ApiEncryptResponseWrapper
|
||||
|
||||
## 如何配置
|
||||
|
||||
```yaml
|
||||
framework:
|
||||
lingniu:
|
||||
web:
|
||||
apiLog:
|
||||
#开关
|
||||
enable: true
|
||||
apiEncrypt:
|
||||
#开关
|
||||
enable: false
|
||||
algorithm: "AES"
|
||||
requestKey: "xxx"
|
||||
responseKey: "xxx"
|
||||
```
|
||||
Reference in New Issue
Block a user