Initial commit

This commit is contained in:
Eric
2026-01-16 18:51:16 +08:00
commit 98c057de11
280 changed files with 16665 additions and 0 deletions

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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 {};
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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) {
// 忽略异常。原因:仅仅打印,非重要逻辑
}
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
};
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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 {
}
}

View File

@@ -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();
}
}

View File

@@ -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 = "";
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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) {
}
};
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
};
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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 {
}

View File

@@ -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 形式暴露 ModuleBoot 会自动注册到所有 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();
}
}

View File

@@ -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框架规范使用响应体结构~~");
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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"
```