mirror of
http://192.168.1.205:9980/cf_devdept2/cf_imes_server.git
synced 2026-08-12 21:02:08 +08:00
init
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>com.cf.imes</groupId>
|
||||
<artifactId>cf-framework</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>cf-spring-boot-starter-web</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>Web 框架,全局异常、API 日志等</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.cf.imes</groupId>
|
||||
<artifactId>cf-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot 配置所需依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,主要是 GlobalExceptionHandler 使用 -->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId> <!-- 接口文档 -->
|
||||
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId> <!-- 接口文档 -->
|
||||
<artifactId>springdoc-openapi-ui</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- RPC 远程调用相关 -->
|
||||
<dependency>
|
||||
<groupId>com.cf.imes</groupId>
|
||||
<artifactId>cf-spring-boot-starter-rpc</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
<groupId>com.cf.imes</groupId>
|
||||
<artifactId>cf-module-infra-api</artifactId> <!-- 需要使用它,进行操作日志的记录 -->
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.cf.imes.framework.apilog.config;
|
||||
|
||||
import com.cf.imes.framework.apilog.core.filter.ApiAccessLogFilter;
|
||||
import com.cf.imes.framework.apilog.core.service.ApiAccessLogFrameworkService;
|
||||
import com.cf.imes.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl;
|
||||
import com.cf.imes.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import com.cf.imes.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl;
|
||||
import com.cf.imes.framework.common.enums.WebFilterOrderEnum;
|
||||
import com.cf.imes.framework.web.config.WebProperties;
|
||||
import com.cf.imes.framework.web.config.ChenfengWebAutoConfiguration;
|
||||
import com.cf.imes.module.infra.api.logger.ApiAccessLogApi;
|
||||
import com.cf.imes.module.infra.api.logger.ApiErrorLogApi;
|
||||
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 javax.servlet.Filter;
|
||||
|
||||
@AutoConfiguration(after = ChenfengWebAutoConfiguration.class)
|
||||
public class ChenfengApiLogAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) {
|
||||
return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) {
|
||||
return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 ApiAccessLogFilter Bean,记录 API 请求日志
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "chenfeng.access-log", value = "enable", matchIfMissing = true) // 允许使用 chenfeng.access-log.enable=false 禁用访问日志
|
||||
public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties,
|
||||
@Value("${spring.application.name}") String applicationName,
|
||||
ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
|
||||
ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService);
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.cf.imes.framework.apilog.config;
|
||||
|
||||
import com.cf.imes.module.infra.api.logger.ApiAccessLogApi;
|
||||
import com.cf.imes.module.infra.api.logger.ApiErrorLogApi;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
/**
|
||||
* API 日志使用到 Feign 的配置项
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableFeignClients(clients = {ApiAccessLogApi.class, // 主要是引入相关的 API 服务
|
||||
ApiErrorLogApi.class})
|
||||
public class ChenfengApiLogRpcAutoConfiguration {
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package com.cf.imes.framework.apilog.core.filter;
|
||||
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import com.cf.imes.framework.apilog.core.service.ApiAccessLog;
|
||||
import com.cf.imes.framework.apilog.core.service.ApiAccessLogFrameworkService;
|
||||
import com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.cf.imes.framework.common.pojo.CommonResult;
|
||||
import com.cf.imes.framework.common.util.monitor.TracerUtils;
|
||||
import com.cf.imes.framework.common.util.servlet.ServletUtils;
|
||||
import com.cf.imes.framework.web.config.WebProperties;
|
||||
import com.cf.imes.framework.web.core.filter.ApiRequestFilter;
|
||||
import com.cf.imes.framework.web.core.util.WebFrameworkUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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.Map;
|
||||
|
||||
import static com.cf.imes.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* API 访问日志 Filter
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
@Slf4j
|
||||
public class ApiAccessLogFilter extends ApiRequestFilter {
|
||||
|
||||
private final String applicationName;
|
||||
|
||||
private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
|
||||
|
||||
public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
|
||||
super(webProperties);
|
||||
this.applicationName = applicationName;
|
||||
this.apiAccessLogFrameworkService = apiAccessLogFrameworkService;
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {
|
||||
ApiAccessLog accessLog = new ApiAccessLog();
|
||||
try {
|
||||
this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex);
|
||||
apiAccessLogFrameworkService.createApiAccessLog(accessLog);
|
||||
} catch (Throwable th) {
|
||||
log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
|
||||
}
|
||||
}
|
||||
|
||||
private void buildApiAccessLogDTO(ApiAccessLog accessLog, HttpServletRequest request, LocalDateTime beginTime,
|
||||
Map<String, String> queryString, String requestBody, Exception ex) {
|
||||
// 处理用户信息
|
||||
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(0);
|
||||
accessLog.setResultMsg("");
|
||||
}
|
||||
// 设置其它字段
|
||||
accessLog.setTraceId(TracerUtils.getTraceId());
|
||||
accessLog.setApplicationName(applicationName);
|
||||
accessLog.setRequestUrl(request.getRequestURI());
|
||||
Map<String, Object> requestParams = MapUtil.<String, Object>builder().put("query", queryString).put("body", requestBody).build();
|
||||
accessLog.setRequestParams(toJsonString(requestParams));
|
||||
accessLog.setRequestMethod(request.getMethod());
|
||||
accessLog.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
accessLog.setUserIp(ServletUtils.getClientIP(request));
|
||||
// 持续时间
|
||||
accessLog.setBeginTime(beginTime);
|
||||
accessLog.setEndTime(LocalDateTime.now());
|
||||
accessLog.setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS));
|
||||
}
|
||||
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package com.cf.imes.framework.apilog.core.service;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 访问日志
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
@Data
|
||||
public class ApiAccessLog {
|
||||
|
||||
/**
|
||||
* 链路追踪编号
|
||||
*/
|
||||
private String traceId;
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@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 beginTime;
|
||||
/**
|
||||
* 结束请求时间
|
||||
*/
|
||||
@NotNull(message = "结束请求时间不能为空")
|
||||
private LocalDateTime endTime;
|
||||
/**
|
||||
* 执行时长,单位:毫秒
|
||||
*/
|
||||
@NotNull(message = "执行时长不能为空")
|
||||
private Integer duration;
|
||||
/**
|
||||
* 结果码
|
||||
*/
|
||||
@NotNull(message = "错误码不能为空")
|
||||
private Integer resultCode;
|
||||
/**
|
||||
* 结果提示
|
||||
*/
|
||||
private String resultMsg;
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.cf.imes.framework.apilog.core.service;
|
||||
|
||||
/**
|
||||
* API 访问日志 Framework Service 接口
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
public interface ApiAccessLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 创建 API 访问日志
|
||||
*
|
||||
* @param apiAccessLog API 访问日志
|
||||
*/
|
||||
void createApiAccessLog(ApiAccessLog apiAccessLog);
|
||||
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.cf.imes.framework.apilog.core.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.cf.imes.module.infra.api.logger.ApiAccessLogApi;
|
||||
import com.cf.imes.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
/**
|
||||
* API 访问日志 Framework Service 实现类
|
||||
*
|
||||
* 基于 {@link ApiAccessLogApi} 远程服务,记录访问日志
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService {
|
||||
|
||||
private final ApiAccessLogApi apiAccessLogApi;
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void createApiAccessLog(ApiAccessLog apiAccessLog) {
|
||||
ApiAccessLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiAccessLog, ApiAccessLogCreateReqDTO.class);
|
||||
apiAccessLogApi.createApiAccessLog(reqDTO).checkError();
|
||||
}
|
||||
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package com.cf.imes.framework.apilog.core.service;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 错误日志
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
@Data
|
||||
public class ApiErrorLog {
|
||||
|
||||
/**
|
||||
* 链路编号
|
||||
*/
|
||||
private String traceId;
|
||||
/**
|
||||
* 账号编号
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@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;
|
||||
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.cf.imes.framework.apilog.core.service;
|
||||
|
||||
/**
|
||||
* API 错误日志 Framework Service 接口
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
public interface ApiErrorLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 创建 API 错误日志
|
||||
*
|
||||
* @param apiErrorLog API 错误日志
|
||||
*/
|
||||
void createApiErrorLog(ApiErrorLog apiErrorLog);
|
||||
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.cf.imes.framework.apilog.core.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.cf.imes.module.infra.api.logger.ApiErrorLogApi;
|
||||
import com.cf.imes.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
/**
|
||||
* API 错误日志 Framework Service 实现类
|
||||
*
|
||||
* 基于 {@link ApiErrorLogApi} 远程服务,记录错误日志
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService {
|
||||
|
||||
private final ApiErrorLogApi apiErrorLogApi;
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void createApiErrorLog(ApiErrorLog apiErrorLog) {
|
||||
ApiErrorLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiErrorLog, ApiErrorLogCreateReqDTO.class);
|
||||
apiErrorLogApi.createApiErrorLog(reqDTO).checkError();
|
||||
}
|
||||
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* API 日志:包含两类
|
||||
* 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。
|
||||
* 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
package com.cf.imes.framework.apilog;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.cf.imes.framework.jackson.config;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.cf.imes.framework.common.util.json.JsonUtils;
|
||||
import com.cf.imes.framework.jackson.core.databind.LocalDateTimeDeserializer;
|
||||
import com.cf.imes.framework.jackson.core.databind.LocalDateTimeSerializer;
|
||||
import com.cf.imes.framework.jackson.core.databind.NumberSerializer;
|
||||
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.context.annotation.Bean;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
|
||||
@AutoConfiguration
|
||||
@Slf4j
|
||||
public class ChenfengJacksonAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
public JsonUtils jsonUtils(List<ObjectMapper> objectMappers) {
|
||||
// 1.1 创建 SimpleModule 对象
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
simpleModule
|
||||
// 新增 Long 类型序列化规则,数值超过 2^53-1,在 JS 会出现精度丢失问题,因此 Long 自动序列化为字符串类型
|
||||
.addSerializer(Long.class, NumberSerializer.INSTANCE)
|
||||
.addSerializer(Long.TYPE, NumberSerializer.INSTANCE)
|
||||
.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE)
|
||||
.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE)
|
||||
.addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE)
|
||||
.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE)
|
||||
// 新增 LocalDateTime 序列化、反序列化规则
|
||||
.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE)
|
||||
.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE);
|
||||
// 1.2 注册到 objectMapper
|
||||
objectMappers.forEach(objectMapper -> objectMapper.registerModule(simpleModule));
|
||||
|
||||
// 2. 设置 objectMapper 到 JsonUtils {
|
||||
JsonUtils.init(CollUtil.getFirst(objectMappers));
|
||||
log.info("[init][初始化 JsonUtils 成功]");
|
||||
return new JsonUtils();
|
||||
}
|
||||
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.cf.imes.framework.jackson.core.databind;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/**
|
||||
* LocalDateTime反序列化规则
|
||||
* <p>
|
||||
* 会将毫秒级时间戳反序列化为LocalDateTime
|
||||
*/
|
||||
public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
|
||||
|
||||
public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer();
|
||||
|
||||
@Override
|
||||
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.cf.imes.framework.jackson.core.databind;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/**
|
||||
* LocalDateTime序列化规则
|
||||
* <p>
|
||||
* 会将LocalDateTime序列化为毫秒级时间戳
|
||||
*/
|
||||
public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
|
||||
|
||||
public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer();
|
||||
|
||||
@Override
|
||||
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.cf.imes.framework.jackson.core.databind;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Long 序列化规则
|
||||
*
|
||||
* 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题
|
||||
*
|
||||
* @author 星语
|
||||
*/
|
||||
@JacksonStdImpl
|
||||
public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer {
|
||||
|
||||
private static final long MAX_SAFE_INTEGER = 9007199254740991L;
|
||||
private static final long MIN_SAFE_INTEGER = -9007199254740991L;
|
||||
|
||||
public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class);
|
||||
|
||||
public NumberSerializer(Class<? extends Number> rawType) {
|
||||
super(rawType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
// 超出范围 序列化位字符串
|
||||
if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
|
||||
super.serialize(value, gen, serializers);
|
||||
} else {
|
||||
gen.writeString(value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
package com.cf.imes.framework.jackson.core;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Web 框架,全局异常、API 日志等
|
||||
*/
|
||||
package com.cf.imes.framework;
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
package com.cf.imes.framework.swagger.config;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.media.IntegerSchema;
|
||||
import io.swagger.v3.oas.models.media.StringSchema;
|
||||
import io.swagger.v3.oas.models.parameters.Parameter;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import org.springdoc.core.*;
|
||||
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
|
||||
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
|
||||
import org.springdoc.core.providers.JavadocProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.cf.imes.framework.web.core.util.WebFrameworkUtils.HEADER_ORGAN_ID;
|
||||
|
||||
/**
|
||||
* Swagger 自动配置类,基于 OpenAPI + Springdoc 实现。
|
||||
*
|
||||
* 友情提示:
|
||||
* 1. Springdoc 文档地址:<a href="https://github.com/springdoc/springdoc-openapi">仓库</a>
|
||||
* 2. Swagger 规范,于 2015 更名为 OpenAPI 规范,本质是一个东西
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass({OpenAPI.class})
|
||||
@EnableConfigurationProperties(SwaggerProperties.class)
|
||||
@ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
|
||||
public class ChenfengSwaggerAutoConfiguration {
|
||||
|
||||
// ========== 全局 OpenAPI 配置 ==========
|
||||
|
||||
@Bean
|
||||
public OpenAPI createApi(SwaggerProperties properties) {
|
||||
Map<String, SecurityScheme> securitySchemas = buildSecuritySchemes();
|
||||
OpenAPI openAPI = new OpenAPI()
|
||||
// 接口信息
|
||||
.info(buildInfo(properties))
|
||||
// 接口安全配置
|
||||
.components(new Components().securitySchemes(securitySchemas))
|
||||
.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION));
|
||||
securitySchemas.keySet().forEach(key -> openAPI.addSecurityItem(new SecurityRequirement().addList(key)));
|
||||
return openAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 摘要信息
|
||||
*/
|
||||
private Info buildInfo(SwaggerProperties properties) {
|
||||
return new Info()
|
||||
.title(properties.getTitle())
|
||||
.description(properties.getDescription())
|
||||
.version(properties.getVersion())
|
||||
.contact(new Contact().name(properties.getAuthor()).url(properties.getUrl()).email(properties.getEmail()))
|
||||
.license(new License().name(properties.getLicense()).url(properties.getLicenseUrl()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全模式,这里配置通过请求头 Authorization 传递 token 参数
|
||||
*/
|
||||
private Map<String, SecurityScheme> buildSecuritySchemes() {
|
||||
Map<String, SecurityScheme> securitySchemes = new HashMap<>();
|
||||
SecurityScheme securityScheme = new SecurityScheme()
|
||||
.type(SecurityScheme.Type.APIKEY) // 类型
|
||||
.name(HttpHeaders.AUTHORIZATION) // 请求头的 name
|
||||
.in(SecurityScheme.In.HEADER); // token 所在位置
|
||||
securitySchemes.put(HttpHeaders.AUTHORIZATION, securityScheme);
|
||||
return securitySchemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义 OpenAPI 处理器
|
||||
*/
|
||||
@Bean
|
||||
public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI,
|
||||
SecurityService securityParser,
|
||||
SpringDocConfigProperties springDocConfigProperties,
|
||||
PropertyResolverUtils propertyResolverUtils,
|
||||
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
|
||||
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
|
||||
Optional<JavadocProvider> javadocProvider) {
|
||||
|
||||
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties,
|
||||
propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
|
||||
}
|
||||
|
||||
// ========== 分组 OpenAPI 配置 ==========
|
||||
|
||||
/**
|
||||
* 所有模块的 API 分组
|
||||
*/
|
||||
@Bean
|
||||
public GroupedOpenApi allGroupedOpenApi() {
|
||||
return buildGroupedOpenApi("all", "");
|
||||
}
|
||||
|
||||
public static GroupedOpenApi buildGroupedOpenApi(String group) {
|
||||
return buildGroupedOpenApi(group, group);
|
||||
}
|
||||
|
||||
public static GroupedOpenApi buildGroupedOpenApi(String group, String path) {
|
||||
return GroupedOpenApi.builder()
|
||||
.group(group)
|
||||
.pathsToMatch("/admin-api/" + path + "/**", "/app-api/" + path + "/**")
|
||||
.addOperationCustomizer((operation, handlerMethod) -> operation
|
||||
.addParametersItem(buildTenantHeaderParameter())
|
||||
.addParametersItem(buildSecurityHeaderParameter()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Tenant 组织编号请求头参数
|
||||
*
|
||||
* @return 多组织参数
|
||||
*/
|
||||
private static Parameter buildTenantHeaderParameter() {
|
||||
return new Parameter()
|
||||
.name(HEADER_ORGAN_ID) // header 名
|
||||
.description("组织编号") // 描述
|
||||
.in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header
|
||||
.schema(new IntegerSchema()._default(1L).name(HEADER_ORGAN_ID).description("组织编号")); // 默认:使用组织编号为 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Authorization 认证请求头参数
|
||||
*
|
||||
* 解决 Knife4j <a href="https://gitee.com/xiaoym/knife4j/issues/I69QBU">Authorize 未生效,请求header里未包含参数</a>
|
||||
*
|
||||
* @return 认证参数
|
||||
*/
|
||||
private static Parameter buildSecurityHeaderParameter() {
|
||||
return new Parameter()
|
||||
.name(HttpHeaders.AUTHORIZATION) // header 名
|
||||
.description("认证 Token") // 描述
|
||||
.in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header
|
||||
.schema(new StringSchema()._default("Bearer test1").name(HEADER_ORGAN_ID).description("认证 Token")); // 默认:使用用户编号为 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.cf.imes.framework.swagger.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* Swagger 配置属性
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
@ConfigurationProperties("chenfeng.swagger")
|
||||
@Data
|
||||
public class SwaggerProperties {
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
@NotEmpty(message = "标题不能为空")
|
||||
private String title;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@NotEmpty(message = "描述不能为空")
|
||||
private String description;
|
||||
/**
|
||||
* 作者
|
||||
*/
|
||||
@NotEmpty(message = "作者不能为空")
|
||||
private String author;
|
||||
/**
|
||||
* 版本
|
||||
*/
|
||||
@NotEmpty(message = "版本不能为空")
|
||||
private String version;
|
||||
/**
|
||||
* url
|
||||
*/
|
||||
@NotEmpty(message = "扫描的 package 不能为空")
|
||||
private String url;
|
||||
/**
|
||||
* email
|
||||
*/
|
||||
@NotEmpty(message = "扫描的 email 不能为空")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* license
|
||||
*/
|
||||
@NotEmpty(message = "扫描的 license 不能为空")
|
||||
private String license;
|
||||
|
||||
/**
|
||||
* license-url
|
||||
*/
|
||||
@NotEmpty(message = "扫描的 license-url 不能为空")
|
||||
private String licenseUrl;
|
||||
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
//package com.cf.imes.framework.swagger.core;
|
||||
//
|
||||
//import cn.hutool.core.util.ReflectUtil;
|
||||
//import com.cf.imes.framework.common.util.collection.CollectionUtils;
|
||||
//import org.springframework.beans.BeansException;
|
||||
//import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
//import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
|
||||
//import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
|
||||
//
|
||||
//import java.util.List;
|
||||
//
|
||||
///**
|
||||
// * 解决 SpringFox 与 SpringBoot 2.6.x 不兼容的问题
|
||||
// * 该问题对应的 issue 为 https://github.com/springfox/springfox/issues/3462
|
||||
// *
|
||||
// * @author 晨丰科技
|
||||
// */
|
||||
//public class SpringFoxHandlerProviderBeanPostProcessor implements BeanPostProcessor {
|
||||
//
|
||||
// @Override
|
||||
// public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
// if (bean instanceof WebMvcRequestHandlerProvider) {
|
||||
// customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
|
||||
// }
|
||||
// return bean;
|
||||
// }
|
||||
//
|
||||
// private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
|
||||
// // 移除,只保留 patternParser
|
||||
// List<T> copy = CollectionUtils.filterList(mappings, mapping -> mapping.getPatternParser() == null);
|
||||
// // 添加到 mappings 中
|
||||
// mappings.clear();
|
||||
// mappings.addAll(copy);
|
||||
// }
|
||||
//
|
||||
// @SuppressWarnings("unchecked")
|
||||
// private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
|
||||
// return (List<RequestMappingInfoHandlerMapping>)
|
||||
// ReflectUtil.getFieldValue(bean, "handlerMappings");
|
||||
// }
|
||||
//
|
||||
//}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 基于 Swagger + Knife4j 实现 API 接口文档
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
package com.cf.imes.framework.swagger;
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package com.cf.imes.framework.web.config;
|
||||
|
||||
import com.cf.imes.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import com.cf.imes.framework.common.enums.WebFilterOrderEnum;
|
||||
import com.cf.imes.framework.web.core.filter.CacheRequestBodyFilter;
|
||||
import com.cf.imes.framework.web.core.filter.DataSourceFilter;
|
||||
import com.cf.imes.framework.web.core.filter.DemoFilter;
|
||||
import com.cf.imes.framework.web.core.handler.GlobalExceptionHandler;
|
||||
import com.cf.imes.framework.web.core.handler.GlobalResponseBodyHandler;
|
||||
import com.cf.imes.framework.web.core.util.WebFrameworkUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
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.util.AntPathMatcher;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
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 org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.Filter;
|
||||
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(WebProperties.class)
|
||||
public class ChenfengWebAutoConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Resource
|
||||
private WebProperties webProperties;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@Value("${spring.application.name}")
|
||||
private String applicationName;
|
||||
|
||||
@Override
|
||||
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||
configurePathMatch(configurer, webProperties.getAdminApi());
|
||||
configurePathMatch(configurer, webProperties.getAppApi());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 API 前缀,仅仅匹配 controller 包下的
|
||||
*
|
||||
* @param configurer 配置
|
||||
* @param api API 配置
|
||||
*/
|
||||
private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {
|
||||
AntPathMatcher antPathMatcher = new AntPathMatcher(".");
|
||||
configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class)
|
||||
&& antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) {
|
||||
return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GlobalResponseBodyHandler globalResponseBodyHandler() {
|
||||
return new GlobalResponseBodyHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
public WebFrameworkUtils webFrameworkUtils(WebProperties webProperties) {
|
||||
// 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean
|
||||
return new WebFrameworkUtils(webProperties);
|
||||
}
|
||||
|
||||
// ========== Filter 相关 ==========
|
||||
|
||||
/**
|
||||
* 创建 CorsFilter Bean,解决跨域问题
|
||||
*/
|
||||
@Bean
|
||||
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);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "chenfeng.dynamicDataSource.enabled", havingValue = "true")
|
||||
public FilterRegistrationBean<DataSourceFilter> dataSourceFilter() {
|
||||
return createFilterBean(new DataSourceFilter(webProperties), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER + 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 DemoFilter Bean,演示模式
|
||||
*/
|
||||
/*@Bean
|
||||
@ConditionalOnProperty(value = "chenfeng.demo", havingValue = "true")
|
||||
public FilterRegistrationBean<DemoFilter> demoFilter() {
|
||||
return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER);
|
||||
}*/
|
||||
|
||||
public static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
|
||||
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
|
||||
bean.setOrder(order);
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 RestTemplate 实例
|
||||
*
|
||||
* @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder}
|
||||
*/
|
||||
@Bean
|
||||
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
|
||||
return restTemplateBuilder.build();
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.cf.imes.framework.web.config;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@ConfigurationProperties(prefix = "chenfeng.web")
|
||||
@Validated
|
||||
@Data
|
||||
public class WebProperties {
|
||||
|
||||
@NotNull(message = "APP API 不能为空")
|
||||
private Api appApi = new Api("/app-api", "**.controller.app.**");
|
||||
@NotNull(message = "Admin API 不能为空")
|
||||
private Api adminApi = new Api("/admin-api", "**.controller.admin.**");
|
||||
|
||||
@NotNull(message = "Admin UI 不能为空")
|
||||
private Ui adminUi;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Valid
|
||||
public static class Api {
|
||||
|
||||
/**
|
||||
* API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀
|
||||
*
|
||||
*
|
||||
* 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题
|
||||
* 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。
|
||||
*
|
||||
* @see chenfengWebAutoConfiguration#configurePathMatch(PathMatchConfigurer)
|
||||
*/
|
||||
@NotEmpty(message = "API 前缀不能为空")
|
||||
private String prefix;
|
||||
|
||||
/**
|
||||
* Controller 所在包的 Ant 路径规则
|
||||
*
|
||||
* 主要目的是,给该 Controller 设置指定的 {@link #prefix}
|
||||
*/
|
||||
@NotEmpty(message = "Controller 所在包不能为空")
|
||||
private String controller;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@Valid
|
||||
public static class Ui {
|
||||
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
private String url;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.cf.imes.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.cf.imes.framework.web.config.WebProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 过滤 /admin-api、/app-api 等 API 请求的过滤器
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public abstract class ApiRequestFilter extends OncePerRequestFilter {
|
||||
|
||||
protected final WebProperties webProperties;
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 只过滤 API 请求的地址
|
||||
return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAdminApi().getPrefix(),
|
||||
webProperties.getAppApi().getPrefix());
|
||||
}
|
||||
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.cf.imes.framework.web.core.filter;
|
||||
|
||||
import com.cf.imes.framework.common.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,实现它的可重复读取
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
public class CacheRequestBodyFilter extends OncePerRequestFilter {
|
||||
|
||||
@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) {
|
||||
// 只处理 json 请求内容
|
||||
return !ServletUtils.isJsonRequest(request);
|
||||
}
|
||||
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.cf.imes.framework.web.core.filter;
|
||||
|
||||
import com.cf.imes.framework.common.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.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* Request Body 缓存 Wrapper
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
public class CacheRequestBodyWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
/**
|
||||
* 缓存的内容
|
||||
*/
|
||||
private final byte[] body;
|
||||
|
||||
public CacheRequestBodyWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
body = ServletUtils.getBodyBytes(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
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;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.cf.imes.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
|
||||
import com.cf.imes.framework.common.pojo.CommonResult;
|
||||
import com.cf.imes.framework.common.util.servlet.ServletUtils;
|
||||
import com.cf.imes.framework.web.config.WebProperties;
|
||||
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;
|
||||
import static com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants.DATA_SOURCE_CODE_NOT_FOUND;
|
||||
|
||||
/**
|
||||
* @author there
|
||||
* 多数据源过滤器
|
||||
*/
|
||||
public class DataSourceFilter extends OncePerRequestFilter {
|
||||
//数据源编码
|
||||
private static final String DATA_SOURCE_CODE = "dataCode";
|
||||
|
||||
private WebProperties webProperties;
|
||||
|
||||
public DataSourceFilter (WebProperties webProperties) {
|
||||
this.webProperties = webProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 只过滤 API 请求的地址
|
||||
return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAdminApi().getPrefix(),
|
||||
webProperties.getAppApi().getPrefix());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
String dataSourceCode = request.getHeader(DATA_SOURCE_CODE);
|
||||
if(StrUtil.isBlank(dataSourceCode)) {
|
||||
ServletUtils.writeJSON(response, CommonResult.error(DATA_SOURCE_CODE_NOT_FOUND));
|
||||
return;
|
||||
}
|
||||
DynamicDataSourceContextHolder.push(dataSourceCode);
|
||||
filterChain.doFilter(request, response);
|
||||
DynamicDataSourceContextHolder.clear();
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.cf.imes.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.cf.imes.framework.common.pojo.CommonResult;
|
||||
import com.cf.imes.framework.common.util.servlet.ServletUtils;
|
||||
import com.cf.imes.framework.web.core.util.WebFrameworkUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import static com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY;
|
||||
|
||||
/**
|
||||
* 演示 Filter,禁止用户发起写操作,避免影响测试数据
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
public class DemoFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String method = request.getMethod();
|
||||
return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE") // 写操作时,不进行过滤率
|
||||
|| WebFrameworkUtils.getLoginUserId(request) == null; // 非登录用户时,不进行过滤
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
|
||||
// 直接返回 DEMO_DENY 的结果。即,请求不继续
|
||||
ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY));
|
||||
}
|
||||
|
||||
}
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
package com.cf.imes.framework.web.core.handler;
|
||||
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import com.cf.imes.framework.apilog.core.service.ApiErrorLog;
|
||||
import com.cf.imes.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import com.cf.imes.framework.common.exception.ServiceException;
|
||||
import com.cf.imes.framework.common.pojo.CommonResult;
|
||||
import com.cf.imes.framework.common.util.monitor.TracerUtils;
|
||||
import com.cf.imes.framework.web.core.util.WebFrameworkUtils;
|
||||
import com.cf.imes.framework.common.util.json.JsonUtils;
|
||||
import com.cf.imes.framework.common.util.servlet.ServletUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
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.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private final String applicationName;
|
||||
|
||||
private final ApiErrorLogFrameworkService apiErrorLogFrameworkService;
|
||||
|
||||
/**
|
||||
* 处理所有异常,主要是提供给 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 NoHandlerFoundException) {
|
||||
return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
|
||||
}
|
||||
if (ex instanceof HttpRequestMethodNotSupportedException) {
|
||||
return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) 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("[missingServletRequestParameterExceptionHandler]", 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);
|
||||
FieldError fieldError = ex.getBindingResult().getFieldError();
|
||||
assert fieldError != null; // 断言,避免告警
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 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()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 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()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Resilience4j 限流抛出的异常
|
||||
*/
|
||||
public CommonResult<?> requestNotPermittedExceptionHandler(HttpServletRequest req, Throwable ex) {
|
||||
log.warn("[requestNotPermittedExceptionHandler][url({}) 访问过于频繁]", req.getRequestURL(), ex);
|
||||
return CommonResult.error(TOO_MANY_REQUESTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常 ServiceException
|
||||
*
|
||||
* 例如说,商品库存不足,用户手机号已存在。
|
||||
*/
|
||||
@ExceptionHandler(value = ServiceException.class)
|
||||
public CommonResult<?> serviceExceptionHandler(ServiceException ex) {
|
||||
log.info("[serviceExceptionHandler]", ex);
|
||||
return CommonResult.error(ex.getCode(), ex.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统异常,兜底处理所有的一切
|
||||
*/
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) {
|
||||
// 情况一:处理表不存在的异常
|
||||
CommonResult<?> tableNotExistsResult = handleTableNotExists(ex);
|
||||
if (tableNotExistsResult != null) {
|
||||
return tableNotExistsResult;
|
||||
}
|
||||
|
||||
// 情况二:部分特殊的库的处理
|
||||
if (Objects.equals("io.github.resilience4j.ratelimiter.RequestNotPermitted", ex.getClass().getName())) {
|
||||
return requestNotPermittedExceptionHandler(req, ex);
|
||||
}
|
||||
|
||||
// 情况三:处理异常
|
||||
log.error("[defaultExceptionHandler]", ex);
|
||||
// 插入异常日志
|
||||
this.createExceptionLog(req, ex);
|
||||
// 返回 ERROR CommonResult
|
||||
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
|
||||
}
|
||||
|
||||
private void createExceptionLog(HttpServletRequest req, Throwable e) {
|
||||
// 插入错误日志
|
||||
ApiErrorLog errorLog = new ApiErrorLog();
|
||||
try {
|
||||
// 初始化 errorLog
|
||||
initExceptionLog(errorLog, req, e);
|
||||
// 执行插入 errorLog
|
||||
apiErrorLogFrameworkService.createApiErrorLog(errorLog);
|
||||
} catch (Throwable th) {
|
||||
log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th);
|
||||
}
|
||||
}
|
||||
|
||||
private void initExceptionLog(ApiErrorLog 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.setTraceId(TracerUtils.getTraceId());
|
||||
errorLog.setApplicationName(applicationName);
|
||||
errorLog.setRequestUrl(request.getRequestURI());
|
||||
Map<String, Object> requestParams = MapUtil.<String, Object>builder()
|
||||
.put("query", ServletUtil.getParamMap(request))
|
||||
.put("body", ServletUtil.getBody(request)).build();
|
||||
errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
|
||||
errorLog.setRequestMethod(request.getMethod());
|
||||
errorLog.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
errorLog.setUserIp(ServletUtil.getClientIP(request));
|
||||
errorLog.setExceptionTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Table 不存在的异常情况
|
||||
*
|
||||
* @param ex 异常
|
||||
* @return 如果是 Table 不存在的异常,则返回对应的 CommonResult
|
||||
*/
|
||||
private CommonResult<?> handleTableNotExists(Throwable ex) {
|
||||
String message = ExceptionUtil.getRootCauseMessage(ex);
|
||||
if (!message.contains("doesn't exist")) {
|
||||
return null;
|
||||
}
|
||||
// 1. 数据报表
|
||||
if (message.contains("report_")) {
|
||||
log.error("[报表模块 cf-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[报表模块 cf-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]");
|
||||
}
|
||||
// 2. 工作流
|
||||
if (message.contains("bpm_")) {
|
||||
log.error("[工作流模块 cf-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[工作流模块 cf-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]");
|
||||
}
|
||||
// 3. 微信公众号
|
||||
if (message.contains("mp_")) {
|
||||
log.error("[微信公众号 cf-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[微信公众号 cf-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]");
|
||||
}
|
||||
// 4. 商城系统
|
||||
if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) {
|
||||
log.error("[商城系统 cf-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[商城系统 cf-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]");
|
||||
}
|
||||
// 5. 支付平台
|
||||
if (message.contains("pay_")) {
|
||||
log.error("[支付模块 cf-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[支付模块 cf-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.cf.imes.framework.web.core.handler;
|
||||
|
||||
import com.cf.imes.framework.apilog.core.filter.ApiAccessLogFilter;
|
||||
import com.cf.imes.framework.common.pojo.CommonResult;
|
||||
import com.cf.imes.framework.web.core.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)处理器
|
||||
*
|
||||
* 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult},
|
||||
* 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。
|
||||
* 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构
|
||||
*
|
||||
* 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果,
|
||||
* 方便 {@link ApiAccessLogFilter} 记录访问日志
|
||||
*/
|
||||
@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 结果
|
||||
WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult<?>) body);
|
||||
return body;
|
||||
}
|
||||
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package com.cf.imes.framework.web.core.util;
|
||||
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import com.cf.imes.framework.common.enums.RpcConstants;
|
||||
import com.cf.imes.framework.common.enums.TerminalEnum;
|
||||
import com.cf.imes.framework.common.enums.UserTypeEnum;
|
||||
import com.cf.imes.framework.common.pojo.CommonResult;
|
||||
import com.cf.imes.framework.web.config.WebProperties;
|
||||
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 包的工具类
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
public class WebFrameworkUtils {
|
||||
|
||||
private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id";
|
||||
private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type";
|
||||
|
||||
private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
|
||||
|
||||
public static final String HEADER_ORGAN_ID = "organ-id";
|
||||
|
||||
/**
|
||||
* 终端的 Header
|
||||
*
|
||||
* @see TerminalEnum
|
||||
*/
|
||||
public static final String HEADER_TERMINAL = "terminal";
|
||||
|
||||
private static WebProperties properties;
|
||||
|
||||
public WebFrameworkUtils(WebProperties webProperties) {
|
||||
WebFrameworkUtils.properties = webProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得组织编号,从 header 中
|
||||
* 考虑到其它 framework 组件也会使用到组织编号,所以不得不放在 WebFrameworkUtils 统一提供
|
||||
*
|
||||
* @param request 请求
|
||||
* @return 组织id
|
||||
*/
|
||||
public static Long getOrganId(HttpServletRequest request) {
|
||||
String organId = request.getHeader(HEADER_ORGAN_ID);
|
||||
return NumberUtil.isNumber(organId) ? Long.valueOf(organId) : null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// 2. 其次,基于 URL 前缀的约定
|
||||
if (request.getServletPath().startsWith(properties.getAdminApi().getPrefix())) {
|
||||
return UserTypeEnum.ADMIN.getValue();
|
||||
}
|
||||
if (request.getServletPath().startsWith(properties.getAppApi().getPrefix())) {
|
||||
return UserTypeEnum.MEMBER.getValue();
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
public static HttpServletRequest getRequest() {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (!(requestAttributes instanceof ServletRequestAttributes)) {
|
||||
return null;
|
||||
}
|
||||
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
|
||||
return servletRequestAttributes.getRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 RPC 请求
|
||||
*
|
||||
* @param request 请求
|
||||
* @return 是否为 RPC 请求
|
||||
*/
|
||||
public static boolean isRpcRequest(HttpServletRequest request) {
|
||||
return request.getRequestURI().startsWith(RpcConstants.RPC_API_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 RPC 请求
|
||||
*
|
||||
* 约定大于配置,只要以 Api 结尾,都认为是 RPC 接口
|
||||
*
|
||||
* @param className 类名
|
||||
* @return 是否为 RPC 请求
|
||||
*/
|
||||
public static boolean isRpcRequest(String className) {
|
||||
return className.endsWith("Api");
|
||||
}
|
||||
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 针对 SpringMVC 的基础封装
|
||||
*/
|
||||
package com.cf.imes.framework.web;
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.cf.imes.framework.xss.config;
|
||||
|
||||
import com.cf.imes.framework.common.enums.WebFilterOrderEnum;
|
||||
import com.cf.imes.framework.xss.core.clean.JsoupXssCleaner;
|
||||
import com.cf.imes.framework.xss.core.clean.XssCleaner;
|
||||
import com.cf.imes.framework.xss.core.filter.XssFilter;
|
||||
import com.cf.imes.framework.xss.core.json.XssStringJsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import static com.cf.imes.framework.web.config.ChenfengWebAutoConfiguration.createFilterBean;
|
||||
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(XssProperties.class)
|
||||
@ConditionalOnProperty(prefix = "chenfeng.xss", name = "enable", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
|
||||
public class ChenfengXssAutoConfiguration implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* Xss 清理者
|
||||
*
|
||||
* @return XssCleaner
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(XssCleaner.class)
|
||||
public XssCleaner xssCleaner() {
|
||||
return new JsoupXssCleaner();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤
|
||||
*
|
||||
* @return Jackson2ObjectMapperBuilderCustomizer
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "xssJacksonCustomizer")
|
||||
@ConditionalOnBean(ObjectMapper.class)
|
||||
@ConditionalOnProperty(value = "chenfeng.xss.enable", havingValue = "true")
|
||||
public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssCleaner xssCleaner) {
|
||||
// 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理
|
||||
return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(xssCleaner));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 XssFilter Bean,解决 Xss 安全问题
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnBean(XssCleaner.class)
|
||||
public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) {
|
||||
return createFilterBean(new XssFilter(properties, pathMatcher, xssCleaner), WebFilterOrderEnum.XSS_FILTER);
|
||||
}
|
||||
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.cf.imes.framework.xss.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Xss 配置属性
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "chenfeng.xss")
|
||||
@Validated
|
||||
@Data
|
||||
public class XssProperties {
|
||||
|
||||
/**
|
||||
* 是否开启,默认为 true
|
||||
*/
|
||||
private boolean enable = true;
|
||||
/**
|
||||
* 需要排除的 URL,默认为空
|
||||
*/
|
||||
private List<String> excludeUrls = Collections.emptyList();
|
||||
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.cf.imes.framework.xss.core.clean;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.safety.Safelist;
|
||||
|
||||
/**
|
||||
* 基于 JSONP 实现 XSS 过滤字符串
|
||||
*/
|
||||
public class JsoupXssCleaner implements XssCleaner {
|
||||
|
||||
private final Safelist safelist;
|
||||
|
||||
/**
|
||||
* 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分)
|
||||
*/
|
||||
private final String baseUri;
|
||||
|
||||
/**
|
||||
* 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表
|
||||
*/
|
||||
public JsoupXssCleaner() {
|
||||
this.safelist = buildSafelist();
|
||||
this.baseUri = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建一个 Xss 清理的 Safelist 规则。
|
||||
* 基于 Safelist#relaxed() 的基础上:
|
||||
* 1. 扩展支持了 style 和 class 属性
|
||||
* 2. a 标签额外支持了 target 属性
|
||||
* 3. img 标签额外支持了 data 协议,便于支持 base64
|
||||
*
|
||||
* @return Safelist
|
||||
*/
|
||||
private Safelist buildSafelist() {
|
||||
// 使用 jsoup 提供的默认的
|
||||
Safelist relaxedSafelist = Safelist.relaxed();
|
||||
// 富文本编辑时一些样式是使用 style 来进行实现的
|
||||
// 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性
|
||||
// 注意:style 属性会有注入风险 <img STYLE="background-image:url(javascript:alert('XSS'))">
|
||||
relaxedSafelist.addAttributes(":all", "style", "class");
|
||||
// 保留 a 标签的 target 属性
|
||||
relaxedSafelist.addAttributes("a", "target");
|
||||
// 支持img 为base64
|
||||
relaxedSafelist.addProtocols("img", "src", "data");
|
||||
|
||||
// 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除
|
||||
// WHITELIST.preserveRelativeLinks(false);
|
||||
|
||||
// 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 <img src=javascript:alert("xss")>
|
||||
// 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径
|
||||
// WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto");
|
||||
// WHITELIST.removeProtocols("img", "src", "http", "https");
|
||||
return relaxedSafelist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String clean(String html) {
|
||||
return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.cf.imes.framework.xss.core.clean;
|
||||
|
||||
/**
|
||||
* 对 html 文本中的有 Xss 风险的数据进行清理
|
||||
*/
|
||||
public interface XssCleaner {
|
||||
|
||||
/**
|
||||
* 清理有 Xss 风险的文本
|
||||
*
|
||||
* @param html 原 html
|
||||
* @return 清理后的 html
|
||||
*/
|
||||
String clean(String html);
|
||||
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.cf.imes.framework.xss.core.filter;
|
||||
|
||||
import com.cf.imes.framework.xss.config.XssProperties;
|
||||
import com.cf.imes.framework.xss.core.clean.XssCleaner;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.util.PathMatcher;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Xss 过滤器
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class XssFilter extends OncePerRequestFilter {
|
||||
|
||||
/**
|
||||
* 属性
|
||||
*/
|
||||
private final XssProperties properties;
|
||||
/**
|
||||
* 路径匹配器
|
||||
*/
|
||||
private final PathMatcher pathMatcher;
|
||||
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 如果关闭,则不过滤
|
||||
if (!properties.isEnable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果匹配到无需过滤,则不过滤
|
||||
String uri = request.getRequestURI();
|
||||
return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri));
|
||||
}
|
||||
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.cf.imes.framework.xss.core.filter;
|
||||
|
||||
import com.cf.imes.framework.xss.core.clean.XssCleaner;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Xss 请求 Wrapper
|
||||
*
|
||||
* @author 晨丰科技
|
||||
*/
|
||||
public class XssRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) {
|
||||
super(request);
|
||||
this.xssCleaner = xssCleaner;
|
||||
}
|
||||
|
||||
// ============================ parameter ============================
|
||||
@Override
|
||||
public Map<String, String[]> getParameterMap() {
|
||||
Map<String, String[]> map = new LinkedHashMap<>();
|
||||
Map<String, String[]> parameters = super.getParameterMap();
|
||||
for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
|
||||
String[] values = entry.getValue();
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = xssCleaner.clean(values[i]);
|
||||
}
|
||||
map.put(entry.getKey(), values);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] values = super.getParameterValues(name);
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
int count = values.length;
|
||||
String[] encodedValues = new String[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
encodedValues[i] = xssCleaner.clean(values[i]);
|
||||
}
|
||||
return encodedValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return xssCleaner.clean(value);
|
||||
}
|
||||
|
||||
// ============================ attribute ============================
|
||||
@Override
|
||||
public Object getAttribute(String name) {
|
||||
Object value = super.getAttribute(name);
|
||||
if (value instanceof String) {
|
||||
return xssCleaner.clean((String) value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// ============================ header ============================
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
String value = super.getHeader(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return xssCleaner.clean(value);
|
||||
}
|
||||
|
||||
// ============================ queryString ============================
|
||||
@Override
|
||||
public String getQueryString() {
|
||||
String value = super.getQueryString();
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return xssCleaner.clean(value);
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.cf.imes.framework.xss.core.json;
|
||||
|
||||
import com.cf.imes.framework.xss.core.clean.XssCleaner;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* XSS 过滤 jackson 反序列化器。
|
||||
* 在反序列化的过程中,会对字符串进行 XSS 过滤。
|
||||
*
|
||||
* @author Hccake
|
||||
*/
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class XssStringJsonDeserializer extends StringDeserializer {
|
||||
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
@Override
|
||||
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
if (p.hasToken(JsonToken.VALUE_STRING)) {
|
||||
return xssCleaner.clean(p.getText());
|
||||
}
|
||||
JsonToken t = p.currentToken();
|
||||
// [databind#381]
|
||||
if (t == JsonToken.START_ARRAY) {
|
||||
return _deserializeFromArray(p, ctxt);
|
||||
}
|
||||
// need to gracefully handle byte[] data, as base64
|
||||
if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
|
||||
Object ob = p.getEmbeddedObject();
|
||||
if (ob == null) {
|
||||
return null;
|
||||
}
|
||||
if (ob instanceof byte[]) {
|
||||
return ctxt.getBase64Variant().encode((byte[]) ob, false);
|
||||
}
|
||||
// otherwise, try conversion using toString()...
|
||||
return ob.toString();
|
||||
}
|
||||
// 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
|
||||
if (t == JsonToken.START_OBJECT) {
|
||||
return ctxt.extractScalarFromObject(p, this, _valueClass);
|
||||
}
|
||||
|
||||
if (t.isScalarValue()) {
|
||||
String text = p.getValueAsString();
|
||||
return xssCleaner.clean(text);
|
||||
}
|
||||
return (String) ctxt.handleUnexpectedToken(_valueClass, p);
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 针对 XSS 的基础封装
|
||||
*
|
||||
* XSS 说明:https://tech.meituan.com/2018/09/27/fe-security.html
|
||||
*/
|
||||
package com.cf.imes.framework.xss;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
com.cf.imes.framework.apilog.config.ChenfengApiLogAutoConfiguration
|
||||
com.cf.imes.framework.jackson.config.ChenfengJacksonAutoConfiguration
|
||||
com.cf.imes.framework.swagger.config.ChenfengSwaggerAutoConfiguration
|
||||
com.cf.imes.framework.web.config.ChenfengWebAutoConfiguration
|
||||
com.cf.imes.framework.apilog.config.ChenfengApiLogRpcAutoConfiguration
|
||||
Reference in New Issue
Block a user