初始化提交
This commit is contained in:
82
yudao-framework/yudao-spring-boot-starter-web/pom.xml
Normal file
82
yudao-framework/yudao-spring-boot-starter-web/pom.xml
Normal file
@ -0,0 +1,82 @@
|
||||
<?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>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-spring-boot-starter-web</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>Web 框架,全局异常、API 日志、脱敏、错误码等</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- spring boot 配置所需依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</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>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,主要是 GlobalExceptionHandler 使用 -->
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-module-infra-api</artifactId> <!-- 需要使用它,进行操作日志的记录 -->
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行错误码的记录 -->
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- xss -->
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,52 @@
|
||||
package cn.iocoder.yudao.framework.apilog.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter;
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService;
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl;
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl;
|
||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
import cn.iocoder.yudao.framework.web.config.WebProperties;
|
||||
import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration;
|
||||
import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
|
||||
import cn.iocoder.yudao.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 = YudaoWebAutoConfiguration.class)
|
||||
public class YudaoApiLogAutoConfiguration {
|
||||
|
||||
@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 = "yudao.access-log", value = "enable", matchIfMissing = true) // 允许使用 yudao.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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package cn.iocoder.yudao.framework.apilog.core.filter;
|
||||
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLog;
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.web.config.WebProperties;
|
||||
import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
|
||||
import cn.iocoder.yudao.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 cn.iocoder.yudao.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));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package cn.iocoder.yudao.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;
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package cn.iocoder.yudao.framework.apilog.core.service;
|
||||
|
||||
/**
|
||||
* API 访问日志 Framework Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface ApiAccessLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 创建 API 访问日志
|
||||
*
|
||||
* @param apiAccessLog API 访问日志
|
||||
*/
|
||||
void createApiAccessLog(ApiAccessLog apiAccessLog);
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package cn.iocoder.yudao.framework.apilog.core.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
|
||||
import cn.iocoder.yudao.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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package cn.iocoder.yudao.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;
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package cn.iocoder.yudao.framework.apilog.core.service;
|
||||
|
||||
/**
|
||||
* API 错误日志 Framework Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface ApiErrorLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 创建 API 错误日志
|
||||
*
|
||||
* @param apiErrorLog API 错误日志
|
||||
*/
|
||||
void createApiErrorLog(ApiErrorLog apiErrorLog);
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package cn.iocoder.yudao.framework.apilog.core.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
|
||||
import cn.iocoder.yudao.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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* API 日志:包含两类
|
||||
* 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。
|
||||
* 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.apilog;
|
@ -0,0 +1,20 @@
|
||||
package cn.iocoder.yudao.framework.banner.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.banner.core.BannerApplicationRunner;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* Banner 的自动配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@AutoConfiguration
|
||||
public class YudaoBannerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public BannerApplicationRunner bannerApplicationRunner() {
|
||||
return new BannerApplicationRunner();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package cn.iocoder.yudao.framework.banner.core;
|
||||
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 项目启动成功后,提供文档相关的地址
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class BannerApplicationRunner implements ApplicationRunner {
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
ThreadUtil.execute(() -> {
|
||||
ThreadUtil.sleep(1, TimeUnit.SECONDS); // 延迟 1 秒,保证输出到结尾
|
||||
log.info("\n----------------------------------------------------------\n\t" +
|
||||
"项目启动成功!\n\t" +
|
||||
"接口文档: \t{} \n\t" +
|
||||
"开发文档: \t{} \n\t" +
|
||||
"视频教程: \t{} \n" +
|
||||
"----------------------------------------------------------",
|
||||
"https://doc.iocoder.cn/api-doc/",
|
||||
"https://doc.iocoder.cn",
|
||||
"https://t.zsxq.com/02Yf6M7Qn");
|
||||
|
||||
// 数据报表
|
||||
if (isNotPresent("cn.iocoder.yudao.module.report.framework.security.config.SecurityConfiguration")) {
|
||||
System.out.println("[报表模块 yudao-module-report - 已禁用][参考 https://doc.iocoder.cn/report/ 开启]");
|
||||
}
|
||||
// 工作流
|
||||
if (isNotPresent("cn.iocoder.yudao.framework.flowable.config.YudaoFlowableConfiguration")) {
|
||||
System.out.println("[工作流模块 yudao-module-bpm - 已禁用][参考 https://doc.iocoder.cn/bpm/ 开启]");
|
||||
}
|
||||
// 商城系统
|
||||
if (isNotPresent("cn.iocoder.yudao.module.trade.framework.web.config.TradeWebConfiguration")) {
|
||||
System.out.println("[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]");
|
||||
}
|
||||
// ERP 系统
|
||||
if (isNotPresent("cn.iocoder.yudao.module.erp.framework.web.config.ErpWebConfiguration")) {
|
||||
System.out.println("[ERP 系统 yudao-module-erp - 已禁用][参考 https://doc.iocoder.cn/erp/build/ 开启]");
|
||||
}
|
||||
// CRM 系统
|
||||
if (isNotPresent("cn.iocoder.yudao.module.crm.framework.web.config.CrmWebConfiguration")) {
|
||||
System.out.println("[CRM 系统 yudao-module-crm - 已禁用][参考 https://doc.iocoder.cn/crm/build/ 开启]");
|
||||
}
|
||||
// 微信公众号
|
||||
if (isNotPresent("cn.iocoder.yudao.module.mp.framework.mp.config.MpConfiguration")) {
|
||||
System.out.println("[微信公众号 yudao-module-mp - 已禁用][参考 https://doc.iocoder.cn/mp/build/ 开启]");
|
||||
}
|
||||
// 支付平台
|
||||
if (isNotPresent("cn.iocoder.yudao.module.pay.framework.pay.config.PayConfiguration")) {
|
||||
System.out.println("[支付系统 yudao-module-pay - 已禁用][参考 https://doc.iocoder.cn/pay/build/ 开启]");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean isNotPresent(String className) {
|
||||
return !ClassUtils.isPresent(className, ClassUtils.getDefaultClassLoader());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Banner 用于在 console 控制台,打印开发文档、接口文档等
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.banner;
|
@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.base.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.serializer.StringDesensitizeSerializer;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 顶级脱敏注解,自定义注解需要使用此注解
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.ANNOTATION_TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside // 此注解是其他所有 jackson 注解的元注解,打上了此注解的注解表明是 jackson 注解的一部分
|
||||
@JsonSerialize(using = StringDesensitizeSerializer.class) // 指定序列化器
|
||||
public @interface DesensitizeBy {
|
||||
|
||||
/**
|
||||
* 脱敏处理器
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
Class<? extends DesensitizationHandler> handler();
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.base.handler;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
/**
|
||||
* 脱敏处理器接口
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public interface DesensitizationHandler<T extends Annotation> {
|
||||
|
||||
/**
|
||||
* 脱敏
|
||||
*
|
||||
* @param origin 原始字符串
|
||||
* @param annotation 注解信息
|
||||
* @return 脱敏后的字符串
|
||||
*/
|
||||
String desensitize(String origin, T annotation);
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.base.serializer;
|
||||
|
||||
import cn.hutool.core.annotation.AnnotationUtil;
|
||||
import cn.hutool.core.lang.Singleton;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.BeanProperty;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* 脱敏序列化器
|
||||
*
|
||||
* 实现 JSON 返回数据时,使用 {@link DesensitizationHandler} 对声明脱敏注解的字段,进行脱敏处理。
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class StringDesensitizeSerializer extends StdSerializer<String> implements ContextualSerializer {
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private DesensitizationHandler desensitizationHandler;
|
||||
|
||||
protected StringDesensitizeSerializer() {
|
||||
super(String.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) {
|
||||
DesensitizeBy annotation = beanProperty.getAnnotation(DesensitizeBy.class);
|
||||
if (annotation == null) {
|
||||
return this;
|
||||
}
|
||||
// 创建一个 StringDesensitizeSerializer 对象,使用 DesensitizeBy 对应的处理器
|
||||
StringDesensitizeSerializer serializer = new StringDesensitizeSerializer();
|
||||
serializer.setDesensitizationHandler(Singleton.get(annotation.handler()));
|
||||
return serializer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void serialize(String value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
gen.writeNull();
|
||||
return;
|
||||
}
|
||||
// 获取序列化字段
|
||||
Field field = getField(gen);
|
||||
|
||||
// 自定义处理器
|
||||
DesensitizeBy[] annotations = AnnotationUtil.getCombinationAnnotations(field, DesensitizeBy.class);
|
||||
if (ArrayUtil.isEmpty(annotations)) {
|
||||
gen.writeString(value);
|
||||
return;
|
||||
}
|
||||
for (Annotation annotation : field.getAnnotations()) {
|
||||
if (AnnotationUtil.hasAnnotation(annotation.annotationType(), DesensitizeBy.class)) {
|
||||
value = this.desensitizationHandler.desensitize(value, annotation);
|
||||
gen.writeString(value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
gen.writeString(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段
|
||||
*
|
||||
* @param generator JsonGenerator
|
||||
* @return 字段
|
||||
*/
|
||||
private Field getField(JsonGenerator generator) {
|
||||
String currentName = generator.getOutputContext().getCurrentName();
|
||||
Object currentValue = generator.getCurrentValue();
|
||||
Class<?> currentValueClass = currentValue.getClass();
|
||||
return ReflectUtil.getField(currentValueClass, currentName);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.regex.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.regex.handler.EmailDesensitizationHandler;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 邮箱脱敏注解
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside
|
||||
@DesensitizeBy(handler = EmailDesensitizationHandler.class)
|
||||
public @interface EmailDesensitize {
|
||||
|
||||
/**
|
||||
* 匹配的正则表达式
|
||||
*/
|
||||
String regex() default "(^.)[^@]*(@.*$)";
|
||||
|
||||
/**
|
||||
* 替换规则,邮箱;
|
||||
*
|
||||
* 比如:example@gmail.com 脱敏之后为 e****@gmail.com
|
||||
*/
|
||||
String replacer() default "$1****$2";
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.regex.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.regex.handler.DefaultRegexDesensitizationHandler;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 正则脱敏注解
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside
|
||||
@DesensitizeBy(handler = DefaultRegexDesensitizationHandler.class)
|
||||
public @interface RegexDesensitize {
|
||||
|
||||
/**
|
||||
* 匹配的正则表达式(默认匹配所有)
|
||||
*/
|
||||
String regex() default "^[\\s\\S]*$";
|
||||
|
||||
/**
|
||||
* 替换规则,会将匹配到的字符串全部替换成 replacer
|
||||
*
|
||||
* 例如:regex=123; replacer=******
|
||||
* 原始字符串 123456789
|
||||
* 脱敏后字符串 ******456789
|
||||
*/
|
||||
String replacer() default "******";
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.regex.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
/**
|
||||
* 正则表达式脱敏处理器抽象类,已实现通用的方法
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public abstract class AbstractRegexDesensitizationHandler<T extends Annotation>
|
||||
implements DesensitizationHandler<T> {
|
||||
|
||||
@Override
|
||||
public String desensitize(String origin, T annotation) {
|
||||
String regex = getRegex(annotation);
|
||||
String replacer = getReplacer(annotation);
|
||||
return origin.replaceAll(regex, replacer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注解上的 regex 参数
|
||||
*
|
||||
* @param annotation 注解信息
|
||||
* @return 正则表达式
|
||||
*/
|
||||
abstract String getRegex(T annotation);
|
||||
|
||||
/**
|
||||
* 获取注解上的 replacer 参数
|
||||
*
|
||||
* @param annotation 注解信息
|
||||
* @return 待替换的字符串
|
||||
*/
|
||||
abstract String getReplacer(T annotation);
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.regex.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.regex.annotation.RegexDesensitize;
|
||||
|
||||
/**
|
||||
* {@link RegexDesensitize} 的正则脱敏处理器
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public class DefaultRegexDesensitizationHandler extends AbstractRegexDesensitizationHandler<RegexDesensitize> {
|
||||
|
||||
@Override
|
||||
String getRegex(RegexDesensitize annotation) {
|
||||
return annotation.regex();
|
||||
}
|
||||
|
||||
@Override
|
||||
String getReplacer(RegexDesensitize annotation) {
|
||||
return annotation.replacer();
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.regex.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.regex.annotation.EmailDesensitize;
|
||||
|
||||
/**
|
||||
* {@link EmailDesensitize} 的脱敏处理器
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public class EmailDesensitizationHandler extends AbstractRegexDesensitizationHandler<EmailDesensitize> {
|
||||
|
||||
@Override
|
||||
String getRegex(EmailDesensitize annotation) {
|
||||
return annotation.regex();
|
||||
}
|
||||
|
||||
@Override
|
||||
String getReplacer(EmailDesensitize annotation) {
|
||||
return annotation.replacer();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.handler.BankCardDesensitization;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 银行卡号
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside
|
||||
@DesensitizeBy(handler = BankCardDesensitization.class)
|
||||
public @interface BankCardDesensitize {
|
||||
|
||||
/**
|
||||
* 前缀保留长度
|
||||
*/
|
||||
int prefixKeep() default 6;
|
||||
|
||||
/**
|
||||
* 后缀保留长度
|
||||
*/
|
||||
int suffixKeep() default 2;
|
||||
|
||||
/**
|
||||
* 替换规则,银行卡号; 比如:9988002866797031 脱敏之后为 998800********31
|
||||
*/
|
||||
String replacer() default "*";
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.handler.CarLicenseDesensitization;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 车牌号
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside
|
||||
@DesensitizeBy(handler = CarLicenseDesensitization.class)
|
||||
public @interface CarLicenseDesensitize {
|
||||
|
||||
/**
|
||||
* 前缀保留长度
|
||||
*/
|
||||
int prefixKeep() default 3;
|
||||
|
||||
/**
|
||||
* 后缀保留长度
|
||||
*/
|
||||
int suffixKeep() default 1;
|
||||
|
||||
/**
|
||||
* 替换规则,车牌号;比如:粤A66666 脱敏之后为粤A6***6
|
||||
*/
|
||||
String replacer() default "*";
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.handler.ChineseNameDesensitization;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 中文名
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside
|
||||
@DesensitizeBy(handler = ChineseNameDesensitization.class)
|
||||
public @interface ChineseNameDesensitize {
|
||||
|
||||
/**
|
||||
* 前缀保留长度
|
||||
*/
|
||||
int prefixKeep() default 1;
|
||||
|
||||
/**
|
||||
* 后缀保留长度
|
||||
*/
|
||||
int suffixKeep() default 0;
|
||||
|
||||
/**
|
||||
* 替换规则,中文名;比如:刘子豪脱敏之后为刘**
|
||||
*/
|
||||
String replacer() default "*";
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.handler.FixedPhoneDesensitization;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 固定电话
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside
|
||||
@DesensitizeBy(handler = FixedPhoneDesensitization.class)
|
||||
public @interface FixedPhoneDesensitize {
|
||||
|
||||
/**
|
||||
* 前缀保留长度
|
||||
*/
|
||||
int prefixKeep() default 4;
|
||||
|
||||
/**
|
||||
* 后缀保留长度
|
||||
*/
|
||||
int suffixKeep() default 2;
|
||||
|
||||
/**
|
||||
* 替换规则,固定电话;比如:01086551122 脱敏之后为 0108*****22
|
||||
*/
|
||||
String replacer() default "*";
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.handler.IdCardDesensitization;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 身份证
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside
|
||||
@DesensitizeBy(handler = IdCardDesensitization.class)
|
||||
public @interface IdCardDesensitize {
|
||||
|
||||
/**
|
||||
* 前缀保留长度
|
||||
*/
|
||||
int prefixKeep() default 6;
|
||||
|
||||
/**
|
||||
* 后缀保留长度
|
||||
*/
|
||||
int suffixKeep() default 2;
|
||||
|
||||
/**
|
||||
* 替换规则,身份证号码;比如:530321199204074611 脱敏之后为 530321**********11
|
||||
*/
|
||||
String replacer() default "*";
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.handler.MobileDesensitization;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside
|
||||
@DesensitizeBy(handler = MobileDesensitization.class)
|
||||
public @interface MobileDesensitize {
|
||||
|
||||
/**
|
||||
* 前缀保留长度
|
||||
*/
|
||||
int prefixKeep() default 3;
|
||||
|
||||
/**
|
||||
* 后缀保留长度
|
||||
*/
|
||||
int suffixKeep() default 4;
|
||||
|
||||
/**
|
||||
* 替换规则,手机号;比如:13248765917 脱敏之后为 132****5917
|
||||
*/
|
||||
String replacer() default "*";
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.handler.PasswordDesensitization;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside
|
||||
@DesensitizeBy(handler = PasswordDesensitization.class)
|
||||
public @interface PasswordDesensitize {
|
||||
|
||||
/**
|
||||
* 前缀保留长度
|
||||
*/
|
||||
int prefixKeep() default 0;
|
||||
|
||||
/**
|
||||
* 后缀保留长度
|
||||
*/
|
||||
int suffixKeep() default 0;
|
||||
|
||||
/**
|
||||
* 替换规则,密码;
|
||||
*
|
||||
* 比如:123456 脱敏之后为 ******
|
||||
*/
|
||||
String replacer() default "*";
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.handler.DefaultDesensitizationHandler;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 滑动脱敏注解
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside
|
||||
@DesensitizeBy(handler = DefaultDesensitizationHandler.class)
|
||||
public @interface SliderDesensitize {
|
||||
|
||||
/**
|
||||
* 后缀保留长度
|
||||
*/
|
||||
int suffixKeep() default 0;
|
||||
|
||||
/**
|
||||
* 替换规则,会将前缀后缀保留后,全部替换成 replacer
|
||||
*
|
||||
* 例如:prefixKeep = 1; suffixKeep = 2; replacer = "*";
|
||||
* 原始字符串 123456
|
||||
* 脱敏后 1***56
|
||||
*/
|
||||
String replacer() default "*";
|
||||
|
||||
/**
|
||||
* 前缀保留长度
|
||||
*/
|
||||
int prefixKeep() default 0;
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
/**
|
||||
* 滑动脱敏处理器抽象类,已实现通用的方法
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public abstract class AbstractSliderDesensitizationHandler<T extends Annotation>
|
||||
implements DesensitizationHandler<T> {
|
||||
|
||||
@Override
|
||||
public String desensitize(String origin, T annotation) {
|
||||
int prefixKeep = getPrefixKeep(annotation);
|
||||
int suffixKeep = getSuffixKeep(annotation);
|
||||
String replacer = getReplacer(annotation);
|
||||
int length = origin.length();
|
||||
|
||||
// 情况一:原始字符串长度小于等于保留长度,则原始字符串全部替换
|
||||
if (prefixKeep >= length || suffixKeep >= length) {
|
||||
return buildReplacerByLength(replacer, length);
|
||||
}
|
||||
|
||||
// 情况二:原始字符串长度小于等于前后缀保留字符串长度,则原始字符串全部替换
|
||||
if ((prefixKeep + suffixKeep) >= length) {
|
||||
return buildReplacerByLength(replacer, length);
|
||||
}
|
||||
|
||||
// 情况三:原始字符串长度大于前后缀保留字符串长度,则替换中间字符串
|
||||
int interval = length - prefixKeep - suffixKeep;
|
||||
return origin.substring(0, prefixKeep) +
|
||||
buildReplacerByLength(replacer, interval) +
|
||||
origin.substring(prefixKeep + interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据长度循环构建替换符
|
||||
*
|
||||
* @param replacer 替换符
|
||||
* @param length 长度
|
||||
* @return 构建后的替换符
|
||||
*/
|
||||
private String buildReplacerByLength(String replacer, int length) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < length; i++) {
|
||||
builder.append(replacer);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 前缀保留长度
|
||||
*
|
||||
* @param annotation 注解信息
|
||||
* @return 前缀保留长度
|
||||
*/
|
||||
abstract Integer getPrefixKeep(T annotation);
|
||||
|
||||
/**
|
||||
* 后缀保留长度
|
||||
*
|
||||
* @param annotation 注解信息
|
||||
* @return 后缀保留长度
|
||||
*/
|
||||
abstract Integer getSuffixKeep(T annotation);
|
||||
|
||||
/**
|
||||
* 替换符
|
||||
*
|
||||
* @param annotation 注解信息
|
||||
* @return 替换符
|
||||
*/
|
||||
abstract String getReplacer(T annotation);
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.BankCardDesensitize;
|
||||
|
||||
/**
|
||||
* {@link BankCardDesensitize} 的脱敏处理器
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public class BankCardDesensitization extends AbstractSliderDesensitizationHandler<BankCardDesensitize> {
|
||||
|
||||
@Override
|
||||
Integer getPrefixKeep(BankCardDesensitize annotation) {
|
||||
return annotation.prefixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
Integer getSuffixKeep(BankCardDesensitize annotation) {
|
||||
return annotation.suffixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
String getReplacer(BankCardDesensitize annotation) {
|
||||
return annotation.replacer();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.CarLicenseDesensitize;
|
||||
|
||||
/**
|
||||
* {@link CarLicenseDesensitize} 的脱敏处理器
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public class CarLicenseDesensitization extends AbstractSliderDesensitizationHandler<CarLicenseDesensitize> {
|
||||
@Override
|
||||
Integer getPrefixKeep(CarLicenseDesensitize annotation) {
|
||||
return annotation.prefixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
Integer getSuffixKeep(CarLicenseDesensitize annotation) {
|
||||
return annotation.suffixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
String getReplacer(CarLicenseDesensitize annotation) {
|
||||
return annotation.replacer();
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.ChineseNameDesensitize;
|
||||
|
||||
/**
|
||||
* {@link ChineseNameDesensitize} 的脱敏处理器
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public class ChineseNameDesensitization extends AbstractSliderDesensitizationHandler<ChineseNameDesensitize> {
|
||||
|
||||
@Override
|
||||
Integer getPrefixKeep(ChineseNameDesensitize annotation) {
|
||||
return annotation.prefixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
Integer getSuffixKeep(ChineseNameDesensitize annotation) {
|
||||
return annotation.suffixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
String getReplacer(ChineseNameDesensitize annotation) {
|
||||
return annotation.replacer();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.SliderDesensitize;
|
||||
|
||||
/**
|
||||
* {@link SliderDesensitize} 的脱敏处理器
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public class DefaultDesensitizationHandler extends AbstractSliderDesensitizationHandler<SliderDesensitize> {
|
||||
@Override
|
||||
Integer getPrefixKeep(SliderDesensitize annotation) {
|
||||
return annotation.prefixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
Integer getSuffixKeep(SliderDesensitize annotation) {
|
||||
return annotation.suffixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
String getReplacer(SliderDesensitize annotation) {
|
||||
return annotation.replacer();
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize;
|
||||
|
||||
/**
|
||||
* {@link FixedPhoneDesensitize} 的脱敏处理器
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public class FixedPhoneDesensitization extends AbstractSliderDesensitizationHandler<FixedPhoneDesensitize> {
|
||||
@Override
|
||||
Integer getPrefixKeep(FixedPhoneDesensitize annotation) {
|
||||
return annotation.prefixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
Integer getSuffixKeep(FixedPhoneDesensitize annotation) {
|
||||
return annotation.suffixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
String getReplacer(FixedPhoneDesensitize annotation) {
|
||||
return annotation.replacer();
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.IdCardDesensitize;
|
||||
|
||||
/**
|
||||
* {@link IdCardDesensitize} 的脱敏处理器
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public class IdCardDesensitization extends AbstractSliderDesensitizationHandler<IdCardDesensitize> {
|
||||
@Override
|
||||
Integer getPrefixKeep(IdCardDesensitize annotation) {
|
||||
return annotation.prefixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
Integer getSuffixKeep(IdCardDesensitize annotation) {
|
||||
return annotation.suffixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
String getReplacer(IdCardDesensitize annotation) {
|
||||
return annotation.replacer();
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.MobileDesensitize;
|
||||
|
||||
/**
|
||||
* {@link MobileDesensitize} 的脱敏处理器
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public class MobileDesensitization extends AbstractSliderDesensitizationHandler<MobileDesensitize> {
|
||||
|
||||
@Override
|
||||
Integer getPrefixKeep(MobileDesensitize annotation) {
|
||||
return annotation.prefixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
Integer getSuffixKeep(MobileDesensitize annotation) {
|
||||
return annotation.suffixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
String getReplacer(MobileDesensitize annotation) {
|
||||
return annotation.replacer();
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.slider.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.PasswordDesensitize;
|
||||
|
||||
/**
|
||||
* {@link PasswordDesensitize} 的码脱敏处理器
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
public class PasswordDesensitization extends AbstractSliderDesensitizationHandler<PasswordDesensitize> {
|
||||
@Override
|
||||
Integer getPrefixKeep(PasswordDesensitize annotation) {
|
||||
return annotation.prefixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
Integer getSuffixKeep(PasswordDesensitize annotation) {
|
||||
return annotation.suffixKeep();
|
||||
}
|
||||
|
||||
@Override
|
||||
String getReplacer(PasswordDesensitize annotation) {
|
||||
return annotation.replacer();
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 脱敏组件:支持 JSON 返回数据时,将邮箱、手机等字段进行脱敏
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.desensitize;
|
@ -0,0 +1,30 @@
|
||||
package cn.iocoder.yudao.framework.errorcode.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 错误码的配置属性类
|
||||
*
|
||||
* @author dlyan
|
||||
*/
|
||||
@ConfigurationProperties("yudao.error-code")
|
||||
@Data
|
||||
@Validated
|
||||
public class ErrorCodeProperties {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
private Boolean enable = true;
|
||||
/**
|
||||
* 错误码枚举类
|
||||
*/
|
||||
@NotNull(message = "错误码枚举类不能为空")
|
||||
private List<String> constantsClassList;
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package cn.iocoder.yudao.framework.errorcode.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.errorcode.core.generator.ErrorCodeAutoGenerator;
|
||||
import cn.iocoder.yudao.framework.errorcode.core.generator.ErrorCodeAutoGeneratorImpl;
|
||||
import cn.iocoder.yudao.framework.errorcode.core.loader.ErrorCodeLoader;
|
||||
import cn.iocoder.yudao.framework.errorcode.core.loader.ErrorCodeLoaderImpl;
|
||||
import cn.iocoder.yudao.module.system.api.errorcode.ErrorCodeApi;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* 错误码配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnProperty(prefix = "yudao.error-code", value = "enable", matchIfMissing = true) // 允许使用 yudao.error-code.enable=false 禁用访问日志
|
||||
@EnableConfigurationProperties(ErrorCodeProperties.class)
|
||||
@EnableScheduling // 开启调度任务的功能,因为 ErrorCodeRemoteLoader 通过定时刷新错误码
|
||||
public class YudaoErrorCodeAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public ErrorCodeAutoGenerator errorCodeAutoGenerator(@Value("${spring.application.name}") String applicationName,
|
||||
ErrorCodeProperties errorCodeProperties,
|
||||
ErrorCodeApi errorCodeApi) {
|
||||
return new ErrorCodeAutoGeneratorImpl(applicationName, errorCodeProperties.getConstantsClassList(), errorCodeApi);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ErrorCodeLoader errorCodeLoader(@Value("${spring.application.name}") String applicationName,
|
||||
ErrorCodeApi errorCodeApi) {
|
||||
return new ErrorCodeLoaderImpl(applicationName, errorCodeApi);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package cn.iocoder.yudao.framework.errorcode.core.generator;
|
||||
|
||||
/**
|
||||
* 错误码的自动生成器
|
||||
*
|
||||
* @author dylan
|
||||
*/
|
||||
public interface ErrorCodeAutoGenerator {
|
||||
|
||||
/**
|
||||
* 将配置类到错误码写入数据库
|
||||
*/
|
||||
void execute();
|
||||
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package cn.iocoder.yudao.framework.errorcode.core.generator;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
import cn.iocoder.yudao.module.system.api.errorcode.ErrorCodeApi;
|
||||
import cn.iocoder.yudao.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ErrorCodeAutoGenerator 的实现类
|
||||
* 目的是,扫描指定的 {@link #constantsClassList} 类,写入到 system 服务中
|
||||
*
|
||||
* @author dylan
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ErrorCodeAutoGeneratorImpl implements ErrorCodeAutoGenerator {
|
||||
|
||||
/**
|
||||
* 应用分组
|
||||
*/
|
||||
private final String applicationName;
|
||||
/**
|
||||
* 错误码枚举类
|
||||
*/
|
||||
private final List<String> constantsClassList;
|
||||
/**
|
||||
* 错误码 Api
|
||||
*/
|
||||
private final ErrorCodeApi errorCodeApi;
|
||||
|
||||
@Override
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
@Async // 异步,保证项目的启动过程,毕竟非关键流程
|
||||
public void execute() {
|
||||
// 第一步,解析错误码
|
||||
List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = parseErrorCode();
|
||||
log.info("[execute][解析到错误码数量为 ({}) 个]", autoGenerateDTOs.size());
|
||||
|
||||
// 第二步,写入到 system 服务
|
||||
try {
|
||||
errorCodeApi.autoGenerateErrorCodeList(autoGenerateDTOs);
|
||||
log.info("[execute][写入到 system 组件完成]");
|
||||
} catch (Exception ex) {
|
||||
log.error("[execute][写入到 system 组件失败({})]", ExceptionUtil.getRootCauseMessage(ex));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 constantsClassList 变量,转换成错误码数组
|
||||
*
|
||||
* @return 错误码数组
|
||||
*/
|
||||
private List<ErrorCodeAutoGenerateReqDTO> parseErrorCode() {
|
||||
// 校验 errorCodeConstantsClass 参数
|
||||
if (CollUtil.isEmpty(constantsClassList)) {
|
||||
log.info("[execute][未配置 yudao.error-code.constants-class-list 配置项,不进行自动写入到 system 服务中]");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 解析错误码
|
||||
List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = new ArrayList<>();
|
||||
constantsClassList.forEach(constantsClass -> {
|
||||
try {
|
||||
// 解析错误码枚举类
|
||||
Class<?> errorCodeConstantsClazz = ClassUtil.loadClass(constantsClass);
|
||||
// 解析错误码
|
||||
autoGenerateDTOs.addAll(parseErrorCode(errorCodeConstantsClazz));
|
||||
} catch (Exception ex) {
|
||||
log.warn("[parseErrorCode][constantsClass({}) 加载失败({})]", constantsClass,
|
||||
ExceptionUtil.getRootCauseMessage(ex));
|
||||
}
|
||||
});
|
||||
return autoGenerateDTOs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析错误码类,获得错误码数组
|
||||
*
|
||||
* @return 错误码数组
|
||||
*/
|
||||
private List<ErrorCodeAutoGenerateReqDTO> parseErrorCode(Class<?> constantsClass) {
|
||||
List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = new ArrayList<>();
|
||||
Arrays.stream(constantsClass.getFields()).forEach(field -> {
|
||||
if (field.getType() != ErrorCode.class) {
|
||||
return;
|
||||
}
|
||||
// 转换成 ErrorCodeAutoGenerateReqDTO 对象
|
||||
ErrorCode errorCode = (ErrorCode) ReflectUtil.getFieldValue(constantsClass, field);
|
||||
autoGenerateDTOs.add(new ErrorCodeAutoGenerateReqDTO().setApplicationName(applicationName)
|
||||
.setCode(errorCode.getCode()).setMessage(errorCode.getMsg()));
|
||||
});
|
||||
return autoGenerateDTOs;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
package cn.iocoder.yudao.framework.errorcode.core.loader;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
|
||||
|
||||
/**
|
||||
* 错误码加载器
|
||||
*
|
||||
* 注意,错误码最终加载到 {@link ServiceExceptionUtil} 的 MESSAGES 变量中!
|
||||
*
|
||||
* @author dlyan
|
||||
*/
|
||||
public interface ErrorCodeLoader {
|
||||
|
||||
/**
|
||||
* 添加错误码
|
||||
*
|
||||
* @param code 错误码的编号
|
||||
* @param msg 错误码的提示
|
||||
*/
|
||||
default void putErrorCode(Integer code, String msg) {
|
||||
ServiceExceptionUtil.put(code, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新错误码
|
||||
*/
|
||||
void refreshErrorCodes();
|
||||
|
||||
/**
|
||||
* 加载错误码
|
||||
*/
|
||||
void loadErrorCodes();
|
||||
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package cn.iocoder.yudao.framework.errorcode.core.loader;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
|
||||
import cn.iocoder.yudao.module.system.api.errorcode.ErrorCodeApi;
|
||||
import cn.iocoder.yudao.module.system.api.errorcode.dto.ErrorCodeRespDTO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ErrorCodeLoader 的实现类,从 infra 的数据库中,加载错误码。
|
||||
*
|
||||
* 考虑到错误码会刷新,所以按照 {@link #REFRESH_ERROR_CODE_PERIOD} 频率,增量加载错误码。
|
||||
*
|
||||
* @author dlyan
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ErrorCodeLoaderImpl implements ErrorCodeLoader {
|
||||
|
||||
/**
|
||||
* 刷新错误码的频率,单位:毫秒
|
||||
*/
|
||||
private static final int REFRESH_ERROR_CODE_PERIOD = 60 * 1000;
|
||||
|
||||
/**
|
||||
* 应用分组
|
||||
*/
|
||||
private final String applicationName;
|
||||
/**
|
||||
* 错误码 Api
|
||||
*/
|
||||
private final ErrorCodeApi errorCodeApi;
|
||||
|
||||
/**
|
||||
* 缓存错误码的最大更新时间,用于后续的增量轮询,判断是否有更新
|
||||
*/
|
||||
private LocalDateTime maxUpdateTime;
|
||||
|
||||
@Override
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
@Async // 异步,保证项目的启动过程,毕竟非关键流程
|
||||
public void loadErrorCodes() {
|
||||
loadErrorCodes0();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Scheduled(fixedDelay = REFRESH_ERROR_CODE_PERIOD, initialDelay = REFRESH_ERROR_CODE_PERIOD)
|
||||
public void refreshErrorCodes() {
|
||||
loadErrorCodes0();
|
||||
}
|
||||
|
||||
private void loadErrorCodes0() {
|
||||
try {
|
||||
// 加载错误码
|
||||
List<ErrorCodeRespDTO> errorCodeRespDTOs = errorCodeApi.getErrorCodeList(applicationName, maxUpdateTime);
|
||||
if (CollUtil.isEmpty(errorCodeRespDTOs)) {
|
||||
return;
|
||||
}
|
||||
log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size());
|
||||
|
||||
// 刷新错误码的缓存
|
||||
errorCodeRespDTOs.forEach(errorCodeRespDTO -> {
|
||||
// 写入到错误码的缓存
|
||||
putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage());
|
||||
// 记录下更新时间,方便增量更新
|
||||
maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime());
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
log.error("[loadErrorCodes0][加载错误码失败({})]", ExceptionUtil.getRootCauseMessage(ex));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 错误码 ErrorCode 的自动配置功能,提供如下功能:
|
||||
*
|
||||
* 1. 远程读取:项目启动时,从 system-service 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提水可配置;
|
||||
* 2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-service 服务加载最新的 ErrorCode 错误码;
|
||||
* 3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑;
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.errorcode;
|
@ -0,0 +1,52 @@
|
||||
package cn.iocoder.yudao.framework.jackson.config;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.jackson.core.databind.LocalDateTimeDeserializer;
|
||||
import cn.iocoder.yudao.framework.jackson.core.databind.LocalDateTimeSerializer;
|
||||
import cn.iocoder.yudao.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 YudaoJacksonAutoConfiguration {
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package cn.iocoder.yudao.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());
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.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());
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.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());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
package cn.iocoder.yudao.framework.jackson.core;
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Web 框架,全局异常、API 日志等
|
||||
*/
|
||||
package cn.iocoder.yudao.framework;
|
@ -0,0 +1,60 @@
|
||||
package cn.iocoder.yudao.framework.swagger.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* Swagger 配置属性
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@ConfigurationProperties("yudao.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;
|
||||
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
package cn.iocoder.yudao.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 cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_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 YudaoSwaggerAutoConfiguration {
|
||||
|
||||
// ========== 全局 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_TENANT_ID) // header 名
|
||||
.description("租户编号") // 描述
|
||||
.in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header
|
||||
.schema(new IntegerSchema()._default(1L).name(HEADER_TENANT_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_TENANT_ID).description("认证 Token")); // 默认:使用用户编号为 1
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 基于 Swagger + Knife4j 实现 API 接口文档
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.swagger;
|
@ -0,0 +1,66 @@
|
||||
package cn.iocoder.yudao.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 = "yudao.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 YudaoWebAutoConfiguration#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;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
package cn.iocoder.yudao.framework.web.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter;
|
||||
import cn.iocoder.yudao.framework.web.core.filter.DemoFilter;
|
||||
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
|
||||
import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
|
||||
import cn.iocoder.yudao.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 YudaoWebAutoConfiguration 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 DemoFilter Bean,演示模式
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "yudao.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();
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package cn.iocoder.yudao.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.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());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package cn.iocoder.yudao.framework.web.core.filter;
|
||||
|
||||
import cn.iocoder.yudao.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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package cn.iocoder.yudao.framework.web.core.filter;
|
||||
|
||||
import cn.iocoder.yudao.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;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package cn.iocoder.yudao.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.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 cn.iocoder.yudao.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));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,337 @@
|
||||
package cn.iocoder.yudao.framework.web.core.handler;
|
||||
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLog;
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
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 cn.iocoder.yudao.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(request, (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(HttpServletRequest req, 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(ExceptionUtils.getStackTrace(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", 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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 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("[报表模块 yudao-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[报表模块 yudao-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]");
|
||||
}
|
||||
// 2. 工作流
|
||||
if (message.contains("bpm_")) {
|
||||
log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]");
|
||||
}
|
||||
// 3. 微信公众号
|
||||
if (message.contains("mp_")) {
|
||||
log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[微信公众号 yudao-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]");
|
||||
}
|
||||
// 4. 商城系统
|
||||
if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) {
|
||||
log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]");
|
||||
}
|
||||
// 5. ERP 系统
|
||||
if (message.contains("erp_")) {
|
||||
log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://doc.iocoder.cn/erp/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://doc.iocoder.cn/erp/build/ 开启]");
|
||||
}
|
||||
// 6. CRM 系统
|
||||
if (message.contains("crm_")) {
|
||||
log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://doc.iocoder.cn/crm/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://doc.iocoder.cn/crm/build/ 开启]");
|
||||
}
|
||||
// 7. 支付平台
|
||||
if (message.contains("pay_")) {
|
||||
log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]");
|
||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||
"[支付模块 yudao-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package cn.iocoder.yudao.framework.web.core.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.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 cn.iocoder.yudao.framework.apilog.core.filter.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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
package cn.iocoder.yudao.framework.web.core.util;
|
||||
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
|
||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.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_TENANT_ID = "tenant-id";
|
||||
|
||||
/**
|
||||
* 终端的 Header
|
||||
*
|
||||
* @see cn.iocoder.yudao.framework.common.enums.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 租户编号
|
||||
*/
|
||||
public static Long getTenantId(HttpServletRequest request) {
|
||||
String tenantId = request.getHeader(HEADER_TENANT_ID);
|
||||
return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : 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();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 针对 SpringMVC 的基础封装
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.web;
|
@ -0,0 +1,29 @@
|
||||
package cn.iocoder.yudao.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 = "yudao.xss")
|
||||
@Validated
|
||||
@Data
|
||||
public class XssProperties {
|
||||
|
||||
/**
|
||||
* 是否开启,默认为 true
|
||||
*/
|
||||
private boolean enable = true;
|
||||
/**
|
||||
* 需要排除的 URL,默认为空
|
||||
*/
|
||||
private List<String> excludeUrls = Collections.emptyList();
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package cn.iocoder.yudao.framework.xss.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
import cn.iocoder.yudao.framework.xss.core.clean.JsoupXssCleaner;
|
||||
import cn.iocoder.yudao.framework.xss.core.clean.XssCleaner;
|
||||
import cn.iocoder.yudao.framework.xss.core.filter.XssFilter;
|
||||
import cn.iocoder.yudao.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 cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration.createFilterBean;
|
||||
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(XssProperties.class)
|
||||
@ConditionalOnProperty(prefix = "yudao.xss", name = "enable", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
|
||||
public class YudaoXssAutoConfiguration 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 = "yudao.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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package cn.iocoder.yudao.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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
package cn.iocoder.yudao.framework.xss.core.clean;
|
||||
|
||||
/**
|
||||
* 对 html 文本中的有 Xss 风险的数据进行清理
|
||||
*/
|
||||
public interface XssCleaner {
|
||||
|
||||
/**
|
||||
* 清理有 Xss 风险的文本
|
||||
*
|
||||
* @param html 原 html
|
||||
* @return 清理后的 html
|
||||
*/
|
||||
String clean(String html);
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package cn.iocoder.yudao.framework.xss.core.filter;
|
||||
|
||||
import cn.iocoder.yudao.framework.xss.config.XssProperties;
|
||||
import cn.iocoder.yudao.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));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package cn.iocoder.yudao.framework.xss.core.filter;
|
||||
|
||||
import cn.iocoder.yudao.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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package cn.iocoder.yudao.framework.xss.core.json;
|
||||
|
||||
import cn.iocoder.yudao.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);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 针对 XSS 的基础封装
|
||||
*
|
||||
* XSS 说明:https://tech.meituan.com/2018/09/27/fe-security.html
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.xss;
|
@ -0,0 +1,7 @@
|
||||
cn.iocoder.yudao.framework.apilog.config.YudaoApiLogAutoConfiguration
|
||||
cn.iocoder.yudao.framework.jackson.config.YudaoJacksonAutoConfiguration
|
||||
cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration
|
||||
cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration
|
||||
cn.iocoder.yudao.framework.xss.config.YudaoXssAutoConfiguration
|
||||
cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration
|
||||
cn.iocoder.yudao.framework.errorcode.config.YudaoErrorCodeAutoConfiguration
|
@ -0,0 +1,17 @@
|
||||
芋道源码 http://www.iocoder.cn
|
||||
Application Version: ${yudao.info.version}
|
||||
Spring Boot Version: ${spring-boot.version}
|
||||
|
||||
.__ __. ______ .______ __ __ _______
|
||||
| \ | | / __ \ | _ \ | | | | / _____|
|
||||
| \| | | | | | | |_) | | | | | | | __
|
||||
| . ` | | | | | | _ < | | | | | | |_ |
|
||||
| |\ | | `--' | | |_) | | `--' | | |__| |
|
||||
|__| \__| \______/ |______/ \______/ \______|
|
||||
|
||||
███╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗
|
||||
████╗ ██║██╔═══██╗ ██╔══██╗██║ ██║██╔════╝
|
||||
██╔██╗ ██║██║ ██║ ██████╔╝██║ ██║██║ ███╗
|
||||
██║╚██╗██║██║ ██║ ██╔══██╗██║ ██║██║ ██║
|
||||
██║ ╚████║╚██████╔╝ ██████╔╝╚██████╔╝╚██████╔╝
|
||||
╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝
|
@ -0,0 +1,94 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.annotation.Address;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.regex.annotation.EmailDesensitize;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.regex.annotation.RegexDesensitize;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.*;
|
||||
import lombok.Data;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* {@link DesensitizeTest} 的单元测试
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class DesensitizeTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
// 准备参数
|
||||
DesensitizeDemo desensitizeDemo = new DesensitizeDemo();
|
||||
desensitizeDemo.setNickname("芋道源码");
|
||||
desensitizeDemo.setBankCard("9988002866797031");
|
||||
desensitizeDemo.setCarLicense("粤A66666");
|
||||
desensitizeDemo.setFixedPhone("01086551122");
|
||||
desensitizeDemo.setIdCard("530321199204074611");
|
||||
desensitizeDemo.setPassword("123456");
|
||||
desensitizeDemo.setPhoneNumber("13248765917");
|
||||
desensitizeDemo.setSlider1("ABCDEFG");
|
||||
desensitizeDemo.setSlider2("ABCDEFG");
|
||||
desensitizeDemo.setSlider3("ABCDEFG");
|
||||
desensitizeDemo.setEmail("1@email.com");
|
||||
desensitizeDemo.setRegex("你好,我是芋道源码");
|
||||
desensitizeDemo.setAddress("北京市海淀区上地十街10号");
|
||||
desensitizeDemo.setOrigin("芋道源码");
|
||||
|
||||
// 调用
|
||||
DesensitizeDemo d = JsonUtils.parseObject(JsonUtils.toJsonString(desensitizeDemo), DesensitizeDemo.class);
|
||||
// 断言
|
||||
assertNotNull(d);
|
||||
assertEquals("芋***", d.getNickname());
|
||||
assertEquals("998800********31", d.getBankCard());
|
||||
assertEquals("粤A6***6", d.getCarLicense());
|
||||
assertEquals("0108*****22", d.getFixedPhone());
|
||||
assertEquals("530321**********11", d.getIdCard());
|
||||
assertEquals("******", d.getPassword());
|
||||
assertEquals("132****5917", d.getPhoneNumber());
|
||||
assertEquals("#######", d.getSlider1());
|
||||
assertEquals("ABC*EFG", d.getSlider2());
|
||||
assertEquals("*******", d.getSlider3());
|
||||
assertEquals("1****@email.com", d.getEmail());
|
||||
assertEquals("你好,我是*", d.getRegex());
|
||||
assertEquals("北京市海淀区上地十街10号*", d.getAddress());
|
||||
assertEquals("芋道源码", d.getOrigin());
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class DesensitizeDemo {
|
||||
|
||||
@ChineseNameDesensitize
|
||||
private String nickname;
|
||||
@BankCardDesensitize
|
||||
private String bankCard;
|
||||
@CarLicenseDesensitize
|
||||
private String carLicense;
|
||||
@FixedPhoneDesensitize
|
||||
private String fixedPhone;
|
||||
@IdCardDesensitize
|
||||
private String idCard;
|
||||
@PasswordDesensitize
|
||||
private String password;
|
||||
@MobileDesensitize
|
||||
private String phoneNumber;
|
||||
@SliderDesensitize(prefixKeep = 6, suffixKeep = 1, replacer = "#")
|
||||
private String slider1;
|
||||
@SliderDesensitize(prefixKeep = 3, suffixKeep = 3)
|
||||
private String slider2;
|
||||
@SliderDesensitize(prefixKeep = 10)
|
||||
private String slider3;
|
||||
@EmailDesensitize
|
||||
private String email;
|
||||
@RegexDesensitize(regex = "芋道源码", replacer = "*")
|
||||
private String regex;
|
||||
@Address
|
||||
private String address;
|
||||
private String origin;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.annotation;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.DesensitizeTest;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.handler.AddressHandler;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 地址
|
||||
*
|
||||
* 用于 {@link DesensitizeTest} 测试使用
|
||||
*
|
||||
* @author gaibu
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside
|
||||
@DesensitizeBy(handler = AddressHandler.class)
|
||||
public @interface Address {
|
||||
|
||||
String replacer() default "*";
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package cn.iocoder.yudao.framework.desensitize.core.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.desensitize.core.DesensitizeTest;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.annotation.Address;
|
||||
|
||||
/**
|
||||
* {@link Address} 的脱敏处理器
|
||||
*
|
||||
* 用于 {@link DesensitizeTest} 测试使用
|
||||
*/
|
||||
public class AddressHandler implements DesensitizationHandler<Address> {
|
||||
|
||||
@Override
|
||||
public String desensitize(String origin, Address annotation) {
|
||||
return origin + annotation.replacer();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/Swagger/?yudao>
|
@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/SpringMVC/?yudao>
|
Reference in New Issue
Block a user