1. 统一包名修改

This commit is contained in:
chenbowen
2025-09-22 11:55:27 +08:00
parent a001fc8f16
commit 0d46897482
2739 changed files with 512 additions and 512 deletions

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.zt.plat</groupId>
<artifactId>zt-framework</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zt-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>com.zt.plat</groupId>
<artifactId>zt-common</artifactId>
</dependency>
<!-- Spring Boot 配置所需依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<scope>provided</scope> <!-- 设置为 provided主要是 GlobalExceptionHandler 使用 -->
</dependency>
<dependency>
<groupId>com.github.xingfudeshi</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-rpc</artifactId>
<optional>true</optional>
</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>

View File

@@ -0,0 +1,44 @@
package com.zt.plat.framework.apilog.config;
import com.zt.plat.framework.apilog.core.filter.ApiAccessLogFilter;
import com.zt.plat.framework.apilog.core.interceptor.ApiAccessLogInterceptor;
import com.zt.plat.framework.common.biz.infra.logger.ApiAccessLogCommonApi;
import com.zt.plat.framework.common.enums.WebFilterOrderEnum;
import com.zt.plat.framework.web.config.WebProperties;
import com.zt.plat.framework.web.config.CloudWebAutoConfiguration;
import jakarta.servlet.Filter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@AutoConfiguration(after = CloudWebAutoConfiguration.class)
public class CloudApiLogAutoConfiguration implements WebMvcConfigurer {
/**
* 创建 ApiAccessLogFilter Bean记录 API 请求日志
*/
@Bean
@ConditionalOnProperty(prefix = "cloud.access-log", value = "enable", matchIfMissing = true) // 允许使用 cloud.access-log.enable=false 禁用访问日志
public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties,
@Value("${spring.application.name}") String applicationName,
ApiAccessLogCommonApi apiAccessLogApi) {
ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogApi);
return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER);
}
private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
bean.setOrder(order);
return bean;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ApiAccessLogInterceptor());
}
}

View File

@@ -0,0 +1,16 @@
package com.zt.plat.framework.apilog.config;
import com.zt.plat.framework.common.biz.infra.logger.ApiAccessLogCommonApi;
import com.zt.plat.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* API 日志使用到 Feign 的配置项
*
* @author ZT
*/
@AutoConfiguration
@EnableFeignClients(clients = {ApiAccessLogCommonApi.class, ApiErrorLogCommonApi.class}) // 主要是引入相关的 API 服务
public class CloudApiLogRpcAutoConfiguration {
}

View File

@@ -0,0 +1,65 @@
package com.zt.plat.framework.apilog.core.annotation;
import com.zt.plat.framework.apilog.core.enums.OperateTypeEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 访问日志注解
*
* @author ZT
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiAccessLog {
// ========== 开关字段 ==========
/**
* 是否记录访问日志
*/
boolean enable() default true;
/**
* 是否记录请求参数
*
* 默认记录,主要考虑请求数据一般不大。可手动设置为 false 进行关闭
*/
boolean requestEnable() default true;
/**
* 是否记录响应结果
*
* 默认不记录,主要考虑响应数据可能比较大。可手动设置为 true 进行打开
*/
boolean responseEnable() default false;
/**
* 敏感参数数组
*
* 添加后,请求参数、响应结果不会记录该参数
*/
String[] sanitizeKeys() default {};
// ========== 模块字段 ==========
/**
* 操作模块
*
* 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.tags.Tag#name()} 属性
*/
String operateModule() default "";
/**
* 操作名
*
* 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.Operation#summary()} 属性
*/
String operateName() default "";
/**
* 操作分类
*
* 实际并不是数组,因为枚举不能设置 null 作为默认值
*/
OperateTypeEnum[] operateType() default {};
}

View File

@@ -0,0 +1,51 @@
package com.zt.plat.framework.apilog.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 操作日志的操作类型
*
* @author ruoyi
*/
@Getter
@AllArgsConstructor
public enum OperateTypeEnum {
/**
* 查询
*/
GET(1),
/**
* 新增
*/
CREATE(2),
/**
* 修改
*/
UPDATE(3),
/**
* 删除
*/
DELETE(4),
/**
* 导出
*/
EXPORT(5),
/**
* 导入
*/
IMPORT(6),
/**
* 其它
*
* 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识
*/
OTHER(0);
/**
* 类型
*/
private final Integer type;
}

View File

@@ -0,0 +1,252 @@
package com.zt.plat.framework.apilog.core.filter;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.apilog.core.annotation.ApiAccessLog;
import com.zt.plat.framework.apilog.core.enums.OperateTypeEnum;
import com.zt.plat.framework.common.biz.infra.logger.ApiAccessLogCommonApi;
import com.zt.plat.framework.common.biz.infra.logger.dto.ApiAccessLogCreateReqDTO;
import com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.util.json.JsonUtils;
import com.zt.plat.framework.common.util.monitor.TracerUtils;
import com.zt.plat.framework.common.util.servlet.ServletUtils;
import com.zt.plat.framework.web.config.WebProperties;
import com.zt.plat.framework.web.core.filter.ApiRequestFilter;
import com.zt.plat.framework.web.core.util.WebFrameworkUtils;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Iterator;
import java.util.Map;
import static com.zt.plat.framework.apilog.core.interceptor.ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD;
import static com.zt.plat.framework.common.util.json.JsonUtils.toJsonString;
/**
* API 访问日志 Filter
*
* 目的:记录 API 访问日志到数据库中
*
* @author ZT
*/
@Slf4j
public class ApiAccessLogFilter extends ApiRequestFilter {
private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"};
private final String applicationName;
private final ApiAccessLogCommonApi apiAccessLogApi;
public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogCommonApi apiAccessLogApi) {
super(webProperties);
this.applicationName = applicationName;
this.apiAccessLogApi = apiAccessLogApi;
}
@Override
@SuppressWarnings("NullableProblems")
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 获得开始时间
LocalDateTime beginTime = LocalDateTime.now();
// 提前获得参数,避免 XssFilter 过滤处理
Map<String, String> queryString = ServletUtils.getParamMap(request);
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
try {
// 继续过滤器
filterChain.doFilter(request, response);
// 正常执行,记录日志
createApiAccessLog(request, beginTime, queryString, requestBody, null);
} catch (Exception ex) {
// 异常执行,记录日志
createApiAccessLog(request, beginTime, queryString, requestBody, ex);
throw ex;
}
}
private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime,
Map<String, String> queryString, String requestBody, Exception ex) {
ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO();
try {
boolean enable = buildApiAccessLog(accessLog, request, beginTime, queryString, requestBody, ex);
if (!enable) {
return;
}
apiAccessLogApi.createApiAccessLogAsync(accessLog);
} catch (Throwable th) {
log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
}
}
private boolean buildApiAccessLog(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, LocalDateTime beginTime,
Map<String, String> queryString, String requestBody, Exception ex) {
// 判断:是否要记录操作日志
HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(ATTRIBUTE_HANDLER_METHOD);
ApiAccessLog accessLogAnnotation = null;
if (handlerMethod != null) {
accessLogAnnotation = handlerMethod.getMethodAnnotation(ApiAccessLog.class);
if (accessLogAnnotation != null && BooleanUtil.isFalse(accessLogAnnotation.enable())) {
return false;
}
}
// 处理用户信息
accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request))
.setUserType(WebFrameworkUtils.getLoginUserType(request));
// 设置访问结果
CommonResult<?> result = WebFrameworkUtils.getCommonResult(request);
if (result != null) {
accessLog.setResultCode(result.getCode()).setResultMsg(result.getMsg());
} else if (ex != null) {
accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode())
.setResultMsg(ExceptionUtil.getRootCauseMessage(ex));
} else {
accessLog.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()).setResultMsg("");
}
// 设置请求字段
accessLog.setTraceId(TracerUtils.getTraceId()).setApplicationName(applicationName)
.setRequestUrl(request.getRequestURI()).setRequestMethod(request.getMethod())
.setUserAgent(ServletUtils.getUserAgent(request)).setUserIp(ServletUtils.getClientIP(request));
String[] sanitizeKeys = accessLogAnnotation != null ? accessLogAnnotation.sanitizeKeys() : null;
Boolean requestEnable = accessLogAnnotation != null ? accessLogAnnotation.requestEnable() : Boolean.TRUE;
if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录,所以判断 !false
Map<String, Object> requestParams = MapUtil.<String, Object>builder()
.put("query", sanitizeMap(queryString, sanitizeKeys))
.put("body", sanitizeJson(requestBody, sanitizeKeys)).build();
accessLog.setRequestParams(toJsonString(requestParams));
}
Boolean responseEnable = accessLogAnnotation != null ? accessLogAnnotation.responseEnable() : Boolean.FALSE;
if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录,默认强制要求 true
accessLog.setResponseBody(sanitizeJson(result, sanitizeKeys));
}
// 持续时间
accessLog.setBeginTime(beginTime).setEndTime(LocalDateTime.now())
.setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS));
// 操作模块
if (handlerMethod != null) {
Tag tagAnnotation = handlerMethod.getBeanType().getAnnotation(Tag.class);
Operation operationAnnotation = handlerMethod.getMethodAnnotation(Operation.class);
String operateModule = accessLogAnnotation != null && StrUtil.isNotBlank(accessLogAnnotation.operateModule()) ?
accessLogAnnotation.operateModule() :
tagAnnotation != null ? StrUtil.nullToDefault(tagAnnotation.name(), tagAnnotation.description()) : null;
String operateName = accessLogAnnotation != null && StrUtil.isNotBlank(accessLogAnnotation.operateName()) ?
accessLogAnnotation.operateName() :
operationAnnotation != null ? operationAnnotation.summary() : null;
OperateTypeEnum operateType = accessLogAnnotation != null && accessLogAnnotation.operateType().length > 0 ?
accessLogAnnotation.operateType()[0] : parseOperateLogType(request);
accessLog.setOperateModule(operateModule).setOperateName(operateName).setOperateType(operateType.getType());
}
return true;
}
// ========== 解析 @ApiAccessLog、@Swagger 注解 ==========
private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) {
RequestMethod requestMethod = RequestMethod.resolve(request.getMethod());
if (requestMethod == null) {
return OperateTypeEnum.OTHER;
}
switch (requestMethod) {
case GET:
return OperateTypeEnum.GET;
case POST:
return OperateTypeEnum.CREATE;
case PUT:
return OperateTypeEnum.UPDATE;
case DELETE:
return OperateTypeEnum.DELETE;
default:
return OperateTypeEnum.OTHER;
}
}
// ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ==========
private static String sanitizeMap(Map<String, ?> map, String[] sanitizeKeys) {
if (CollUtil.isEmpty(map)) {
return null;
}
if (sanitizeKeys != null) {
MapUtil.removeAny(map, sanitizeKeys);
}
MapUtil.removeAny(map, SANITIZE_KEYS);
return JsonUtils.toJsonString(map);
}
private static String sanitizeJson(String jsonString, String[] sanitizeKeys) {
if (StrUtil.isEmpty(jsonString)) {
return null;
}
try {
JsonNode rootNode = JsonUtils.parseTree(jsonString);
sanitizeJson(rootNode, sanitizeKeys);
return JsonUtils.toJsonString(rootNode);
} catch (Exception e) {
// 脱敏失败的情况下,直接忽略异常,避免影响用户请求
log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
return jsonString;
}
}
private static String sanitizeJson(CommonResult<?> commonResult, String[] sanitizeKeys) {
if (commonResult == null) {
return null;
}
String jsonString = toJsonString(commonResult);
try {
JsonNode rootNode = JsonUtils.parseTree(jsonString);
sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉
return JsonUtils.toJsonString(rootNode);
} catch (Exception e) {
// 脱敏失败的情况下,直接忽略异常,避免影响用户请求
log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
return jsonString;
}
}
private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) {
// 情况一:数组,遍历处理
if (node.isArray()) {
for (JsonNode childNode : node) {
sanitizeJson(childNode, sanitizeKeys);
}
return;
}
// 情况二:非 Object只是某个值直接返回
if (!node.isObject()) {
return;
}
// 情况三Object遍历处理
Iterator<Map.Entry<String, JsonNode>> iterator = node.properties().iterator();
while (iterator.hasNext()) {
Map.Entry<String, JsonNode> entry = iterator.next();
if (ArrayUtil.contains(sanitizeKeys, entry.getKey())
|| ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) {
iterator.remove();
continue;
}
sanitizeJson(entry.getValue(), sanitizeKeys);
}
}
}

View File

@@ -0,0 +1,103 @@
package com.zt.plat.framework.apilog.core.interceptor;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.common.util.servlet.ServletUtils;
import com.zt.plat.framework.common.util.spring.SpringUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.IntStream;
/**
* API 访问日志 Interceptor
*
* 目的:在非 prod 环境时,打印 request 和 response 两条日志到日志文件(控制台)中。
*
* @author ZT
*/
@Slf4j
public class ApiAccessLogInterceptor implements HandlerInterceptor {
public static final String ATTRIBUTE_HANDLER_METHOD = "HANDLER_METHOD";
private static final String ATTRIBUTE_STOP_WATCH = "ApiAccessLogInterceptor.StopWatch";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 记录 HandlerMethod提供给 ApiAccessLogFilter 使用
HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod) handler : null;
if (handlerMethod != null) {
request.setAttribute(ATTRIBUTE_HANDLER_METHOD, handlerMethod);
}
// 打印 request 日志
if (!SpringUtils.isProd()) {
Map<String, String> queryString = ServletUtils.getParamMap(request);
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) {
log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI());
} else {
log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(),
StrUtil.blankToDefault(requestBody, queryString.toString()));
}
// 计时
StopWatch stopWatch = new StopWatch();
stopWatch.start();
request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch);
// 打印 Controller 路径
printHandlerMethodPosition(handlerMethod);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 打印 response 日志
if (!SpringUtils.isProd()) {
StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH);
stopWatch.stop();
log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]",
request.getRequestURI(), stopWatch.getTotalTimeMillis());
}
}
/**
* 打印 Controller 方法路径
*/
private void printHandlerMethodPosition(HandlerMethod handlerMethod) {
if (handlerMethod == null) {
return;
}
Method method = handlerMethod.getMethod();
Class<?> clazz = method.getDeclaringClass();
try {
// 获取 method 的 lineNumber
List<String> clazzContents = FileUtil.readUtf8Lines(
ResourceUtil.getResource(null, clazz).getPath().replace("/target/classes/", "/src/main/java/")
+ clazz.getSimpleName() + ".java");
Optional<Integer> lineNumber = IntStream.range(0, clazzContents.size())
.filter(i -> clazzContents.get(i).contains(" " + method.getName() + "(")) // 简单匹配,不考虑方法重名
.mapToObj(i -> i + 1) // 行号从 1 开始
.findFirst();
if (!lineNumber.isPresent()) {
return;
}
// 打印结果
System.out.printf("\tController 方法路径:%s(%s.java:%d)\n", clazz.getName(), clazz.getSimpleName(), lineNumber.get());
} catch (Exception ignore) {
// 忽略异常。原因:仅仅打印,非重要逻辑
}
}
}

View File

@@ -0,0 +1,8 @@
/**
* API 日志:包含两类
* 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。
* 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。
*
* @author ZT
*/
package com.zt.plat.framework.apilog;

View File

@@ -0,0 +1,20 @@
package com.zt.plat.framework.banner.config;
import com.zt.plat.framework.banner.core.BannerApplicationRunner;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
/**
* Banner 的自动配置类
*
* @author ZT
*/
@AutoConfiguration
public class CloudBannerAutoConfiguration {
@Bean
public BannerApplicationRunner bannerApplicationRunner() {
return new BannerApplicationRunner();
}
}

View File

@@ -0,0 +1,43 @@
package com.zt.plat.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 java.util.concurrent.TimeUnit;
/**
* 项目启动成功后,提供文档相关的地址
*
* @author ZT
*/
@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" +
"----------------------------------------------------------",
"http://172.16.46.63:30888/api-doc/",
"http://172.16.46.63:30888");
// 数据报表
System.out.println("[报表模块 cloud-module-report 教程][参考 http://172.16.46.63:30888/report/ 开启]");
// 工作流
System.out.println("[工作流模块 cloud-module-bpm 教程][参考 http://172.16.46.63:30888/bpm/ 开启]");
// 微信公众号
System.out.println("[微信公众号 cloud-module-mp 教程][参考 http://172.16.46.63:30888/mp/build/ 开启]");
// AI 大模型
System.out.println("[AI 大模型 cloud-module-ai - 教程][参考 http://172.16.46.63:30888/ai/build/ 开启]");
});
}
}

View File

@@ -0,0 +1,6 @@
/**
* Banner 用于在 console 控制台,打印开发文档、接口文档等
*
* @author ZT
*/
package com.zt.plat.framework.banner;

View File

@@ -0,0 +1,28 @@
package com.zt.plat.framework.desensitize.core.base.annotation;
import com.zt.plat.framework.desensitize.core.base.handler.DesensitizationHandler;
import com.zt.plat.framework.desensitize.core.base.serializer.StringDesensitizeSerializer;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.*;
/**
* 顶级脱敏注解,自定义注解需要使用此注解
*
* @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();
}

View File

@@ -0,0 +1,40 @@
package com.zt.plat.framework.desensitize.core.base.handler;
import cn.hutool.core.util.ReflectUtil;
import java.lang.annotation.Annotation;
/**
* 脱敏处理器接口
*
* @author gaibu
*/
public interface DesensitizationHandler<T extends Annotation> {
/**
* 脱敏
*
* @param origin 原始字符串
* @param annotation 注解信息
* @return 脱敏后的字符串
*/
String desensitize(String origin, T annotation);
/**
* 是否禁用脱敏的 Spring EL 表达式
*
* 如果返回 true 则跳过脱敏
*
* @param annotation 注解信息
* @return 是否禁用脱敏的 Spring EL 表达式
*/
default String getDisable(T annotation) {
// 约定:默认就是 enable() 属性。如果不符合,子类重写
try {
return (String) ReflectUtil.invoke(annotation, "disable");
} catch (Exception ex) {
return "";
}
}
}

View File

@@ -0,0 +1,92 @@
package com.zt.plat.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 com.zt.plat.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.zt.plat.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);
}
}

View File

@@ -0,0 +1,40 @@
package com.zt.plat.framework.desensitize.core.regex.annotation;
import com.zt.plat.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.zt.plat.framework.desensitize.core.regex.handler.EmailDesensitizationHandler;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.*;
/**
* 邮箱脱敏注解
*
* @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";
/**
* 是否禁用脱敏
*
* 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
*/
String disable() default "";
}

View File

@@ -0,0 +1,42 @@
package com.zt.plat.framework.desensitize.core.regex.annotation;
import com.zt.plat.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.zt.plat.framework.desensitize.core.regex.handler.DefaultRegexDesensitizationHandler;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.*;
/**
* 正则脱敏注解
*
* @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 "******";
/**
* 是否禁用脱敏
*
* 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
*/
String disable() default "";
}

View File

@@ -0,0 +1,46 @@
package com.zt.plat.framework.desensitize.core.regex.handler;
import com.zt.plat.framework.common.util.spring.SpringExpressionUtils;
import com.zt.plat.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) {
// 1. 判断是否禁用脱敏
Object disable = SpringExpressionUtils.parseExpression(getDisable(annotation));
if (Boolean.TRUE.equals(disable)) {
return origin;
}
// 2. 执行脱敏
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);
}

View File

@@ -0,0 +1,27 @@
package com.zt.plat.framework.desensitize.core.regex.handler;
import com.zt.plat.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();
}
@Override
public String getDisable(RegexDesensitize annotation) {
return annotation.disable();
}
}

View File

@@ -0,0 +1,22 @@
package com.zt.plat.framework.desensitize.core.regex.handler;
import com.zt.plat.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();
}
}

View File

@@ -0,0 +1,43 @@
package com.zt.plat.framework.desensitize.core.slider.annotation;
import com.zt.plat.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.zt.plat.framework.desensitize.core.slider.handler.BankCardDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.*;
/**
* 银行卡号
*
* @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 "*";
/**
* 是否禁用脱敏
*
* 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
*/
String disable() default "";
}

View File

@@ -0,0 +1,43 @@
package com.zt.plat.framework.desensitize.core.slider.annotation;
import com.zt.plat.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.zt.plat.framework.desensitize.core.slider.handler.CarLicenseDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.*;
/**
* 车牌号
*
* @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 "*";
/**
* 是否禁用脱敏
*
* 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
*/
String disable() default "";
}

View File

@@ -0,0 +1,43 @@
package com.zt.plat.framework.desensitize.core.slider.annotation;
import com.zt.plat.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.zt.plat.framework.desensitize.core.slider.handler.ChineseNameDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.*;
/**
* 中文名
*
* @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 "*";
/**
* 是否禁用脱敏
*
* 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
*/
String disable() default "";
}

View File

@@ -0,0 +1,43 @@
package com.zt.plat.framework.desensitize.core.slider.annotation;
import com.zt.plat.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.zt.plat.framework.desensitize.core.slider.handler.FixedPhoneDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.*;
/**
* 固定电话
*
* @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 "*";
/**
* 是否禁用脱敏
*
* 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
*/
String disable() default "";
}

View File

@@ -0,0 +1,43 @@
package com.zt.plat.framework.desensitize.core.slider.annotation;
import com.zt.plat.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.zt.plat.framework.desensitize.core.slider.handler.IdCardDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.*;
/**
* 身份证
*
* @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 "*";
/**
* 是否禁用脱敏
*
* 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
*/
String disable() default "";
}

View File

@@ -0,0 +1,43 @@
package com.zt.plat.framework.desensitize.core.slider.annotation;
import com.zt.plat.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.zt.plat.framework.desensitize.core.slider.handler.MobileDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.*;
/**
* 手机号
*
* @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 "*";
/**
* 是否禁用脱敏
*
* 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
*/
String disable() default "";
}

View File

@@ -0,0 +1,45 @@
package com.zt.plat.framework.desensitize.core.slider.annotation;
import com.zt.plat.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.zt.plat.framework.desensitize.core.slider.handler.PasswordDesensitization;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.*;
/**
* 密码
*
* @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 "*";
/**
* 是否禁用脱敏
*
* 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
*/
String disable() default "";
}

View File

@@ -0,0 +1,47 @@
package com.zt.plat.framework.desensitize.core.slider.annotation;
import com.zt.plat.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.zt.plat.framework.desensitize.core.slider.handler.DefaultDesensitizationHandler;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.*;
/**
* 滑动脱敏注解
*
* @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;
/**
* 是否禁用脱敏
*
* 支持 Spring EL 表达式,如果返回 true 则跳过脱敏
*/
String disable() default "";
}

View File

@@ -0,0 +1,77 @@
package com.zt.plat.framework.desensitize.core.slider.handler;
import com.zt.plat.framework.common.util.spring.SpringExpressionUtils;
import com.zt.plat.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) {
// 1. 判断是否禁用脱敏
Object disable = SpringExpressionUtils.parseExpression(getDisable(annotation));
if (Boolean.TRUE.equals(disable)) {
return origin;
}
// 2. 执行脱敏
int prefixKeep = getPrefixKeep(annotation);
int suffixKeep = getSuffixKeep(annotation);
String replacer = getReplacer(annotation);
int length = origin.length();
int interval = length - prefixKeep - suffixKeep;
// 情况一:原始字符串长度小于等于前后缀保留字符串长度,则原始字符串全部替换
if (interval <= 0) {
return buildReplacerByLength(replacer, length);
}
// 情况二:原始字符串长度大于前后缀保留字符串长度,则替换中间字符串
return origin.substring(0, prefixKeep) +
buildReplacerByLength(replacer, interval) +
origin.substring(prefixKeep + interval);
}
/**
* 根据长度循环构建替换符
*
* @param replacer 替换符
* @param length 长度
* @return 构建后的替换符
*/
private String buildReplacerByLength(String replacer, int length) {
return replacer.repeat(length);
}
/**
* 前缀保留长度
*
* @param annotation 注解信息
* @return 前缀保留长度
*/
abstract Integer getPrefixKeep(T annotation);
/**
* 后缀保留长度
*
* @param annotation 注解信息
* @return 后缀保留长度
*/
abstract Integer getSuffixKeep(T annotation);
/**
* 替换符
*
* @param annotation 注解信息
* @return 替换符
*/
abstract String getReplacer(T annotation);
}

View File

@@ -0,0 +1,32 @@
package com.zt.plat.framework.desensitize.core.slider.handler;
import com.zt.plat.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();
}
@Override
public String getDisable(BankCardDesensitize annotation) {
return "";
}
}

View File

@@ -0,0 +1,32 @@
package com.zt.plat.framework.desensitize.core.slider.handler;
import com.zt.plat.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();
}
@Override
public String getDisable(CarLicenseDesensitize annotation) {
return annotation.disable();
}
}

View File

@@ -0,0 +1,27 @@
package com.zt.plat.framework.desensitize.core.slider.handler;
import com.zt.plat.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();
}
}

View File

@@ -0,0 +1,27 @@
package com.zt.plat.framework.desensitize.core.slider.handler;
import com.zt.plat.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();
}
}

View File

@@ -0,0 +1,27 @@
package com.zt.plat.framework.desensitize.core.slider.handler;
import com.zt.plat.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();
}
}

View File

@@ -0,0 +1,26 @@
package com.zt.plat.framework.desensitize.core.slider.handler;
import com.zt.plat.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();
}
}

View File

@@ -0,0 +1,27 @@
package com.zt.plat.framework.desensitize.core.slider.handler;
import com.zt.plat.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();
}
}

View File

@@ -0,0 +1,26 @@
package com.zt.plat.framework.desensitize.core.slider.handler;
import com.zt.plat.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();
}
}

View File

@@ -0,0 +1,4 @@
/**
* 脱敏组件:支持 JSON 返回数据时,将邮箱、手机等字段进行脱敏
*/
package com.zt.plat.framework.desensitize;

View File

@@ -0,0 +1,52 @@
package com.zt.plat.framework.jackson.config;
import cn.hutool.core.collection.CollUtil;
import com.zt.plat.framework.common.util.json.JsonUtils;
import com.zt.plat.framework.common.util.json.databind.NumberSerializer;
import com.zt.plat.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
import com.zt.plat.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
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 CloudJacksonAutoConfiguration {
@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 序列化、反序列化规则,使用 Long 时间戳
.addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.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();
}
}

View File

@@ -0,0 +1 @@
package com.zt.plat.framework.jackson.core;

View File

@@ -0,0 +1,4 @@
/**
* Web 框架全局异常、API 日志等
*/
package com.zt.plat.framework;

View File

@@ -0,0 +1,161 @@
package com.zt.plat.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.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.models.GroupedOpenApi;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.providers.JavadocProvider;
import org.springdoc.core.service.OpenAPIService;
import org.springdoc.core.service.SecurityService;
import org.springdoc.core.utils.PropertyResolverUtils;
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.context.annotation.Primary;
import org.springframework.http.HttpHeaders;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.zt.plat.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 ZT
*/
@AutoConfiguration
@ConditionalOnClass({OpenAPI.class})
@EnableConfigurationProperties(SwaggerProperties.class)
@ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
public class CloudSwaggerAutoConfiguration {
// ========== 全局 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
@Primary // 目的:以我们创建的 OpenAPIService 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
}
}

View File

@@ -0,0 +1,60 @@
package com.zt.plat.framework.swagger.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import jakarta.validation.constraints.NotEmpty;
/**
* Swagger 配置属性
*
* @author ZT
*/
@ConfigurationProperties("cloud.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;
}

View File

@@ -0,0 +1,6 @@
/**
* 基于 Swagger + Knife4j 实现 API 接口文档
*
* @author ZT
*/
package com.zt.plat.framework.swagger;

View File

@@ -0,0 +1,131 @@
package com.zt.plat.framework.web.config;
import com.zt.plat.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
import com.zt.plat.framework.common.enums.WebFilterOrderEnum;
import com.zt.plat.framework.web.core.filter.CacheRequestBodyFilter;
import com.zt.plat.framework.web.core.filter.DemoFilter;
import com.zt.plat.framework.web.core.handler.GlobalExceptionHandler;
import com.zt.plat.framework.web.core.handler.GlobalResponseBodyHandler;
import com.zt.plat.framework.web.core.util.WebFrameworkUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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 jakarta.annotation.Resource;
import jakarta.servlet.Filter;
@AutoConfiguration
@EnableConfigurationProperties(WebProperties.class)
public class CloudWebAutoConfiguration 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(ApiErrorLogCommonApi apiErrorLogApi) {
return new GlobalExceptionHandler(applicationName, apiErrorLogApi);
}
@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 = "cloud.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
@ConditionalOnMissingBean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
}

View File

@@ -0,0 +1,66 @@
package com.zt.plat.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 jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
@ConfigurationProperties(prefix = "cloud.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 CloudWebAutoConfiguration#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;
}
}

View File

@@ -0,0 +1,27 @@
package com.zt.plat.framework.web.core.filter;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.web.config.WebProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.http.HttpServletRequest;
/**
* 过滤 /admin-api、/app-api 等 API 请求的过滤器
*
* @author ZT
*/
@RequiredArgsConstructor
public abstract class ApiRequestFilter extends OncePerRequestFilter {
protected final WebProperties webProperties;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// 只过滤 API 请求的地址
String apiUri = request.getRequestURI().substring(request.getContextPath().length());
return !StrUtil.startWithAny(apiUri, webProperties.getAdminApi().getPrefix(), webProperties.getAppApi().getPrefix());
}
}

View File

@@ -0,0 +1,31 @@
package com.zt.plat.framework.web.core.filter;
import com.zt.plat.framework.common.util.servlet.ServletUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Request Body 缓存 Filter实现它的可重复读取
*
* @author ZT
*/
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);
}
}

View File

@@ -0,0 +1,68 @@
package com.zt.plat.framework.web.core.filter;
import com.zt.plat.framework.common.util.servlet.ServletUtils;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* Request Body 缓存 Wrapper
*
* @author ZT
*/
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;
}
};
}
}

View File

@@ -0,0 +1,35 @@
package com.zt.plat.framework.web.core.filter;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.util.servlet.ServletUtils;
import com.zt.plat.framework.web.core.util.WebFrameworkUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import static com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY;
/**
* 演示 Filter禁止用户发起写操作避免影响测试数据
*
* @author ZT
*/
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));
}
}

View File

@@ -0,0 +1,407 @@
package com.zt.plat.framework.web.core.handler;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import com.zt.plat.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
import com.zt.plat.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.util.collection.SetUtils;
import com.zt.plat.framework.common.util.json.JsonUtils;
import com.zt.plat.framework.common.util.monitor.TracerUtils;
import com.zt.plat.framework.common.util.servlet.ServletUtils;
import com.zt.plat.framework.web.core.util.WebFrameworkUtils;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.ValidationException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.util.Assert;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
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 org.springframework.web.servlet.resource.NoResourceFoundException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants.*;
/**
* 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
*
* @author ZT
*/
@RestControllerAdvice
@AllArgsConstructor
@Slf4j
public class GlobalExceptionHandler {
/**
* 忽略的 ServiceException 错误提示,避免打印过多 logger
*/
public static final Set<String> IGNORE_ERROR_MESSAGES = SetUtils.asSet("无效的刷新令牌");
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
private final String applicationName;
private final ApiErrorLogCommonApi apiErrorLogApi;
/**
* 处理所有异常,主要是提供给 Filter 使用
* 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。
*
* @param request 请求
* @param ex 异常
* @return 通用返回
*/
public CommonResult<?> allExceptionHandler(HttpServletRequest request, Throwable ex) {
if (ex instanceof MissingServletRequestParameterException) {
return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex);
}
if (ex instanceof MethodArgumentTypeMismatchException) {
return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex);
}
if (ex instanceof MethodArgumentNotValidException) {
return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex);
}
if (ex instanceof BindException) {
return bindExceptionHandler((BindException) ex);
}
if (ex instanceof ConstraintViolationException) {
return constraintViolationExceptionHandler((ConstraintViolationException) ex);
}
if (ex instanceof ValidationException) {
return validationException((ValidationException) ex);
}
if (ex instanceof NoHandlerFoundException) {
return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
}
if (ex instanceof NoResourceFoundException) {
return noResourceFoundExceptionHandler(request, (NoResourceFoundException) 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("[methodArgumentTypeMismatchExceptionHandler]", ex);
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage()));
}
/**
* 处理 SpringMVC 参数校验不正确
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public CommonResult<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) {
log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex);
// 获取 errorMessage
String errorMessage = null;
FieldError fieldError = ex.getBindingResult().getFieldError();
if (fieldError == null) {
// 组合校验,参考自 https://t.zsxq.com/3HVTx
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
if (CollUtil.isNotEmpty(allErrors)) {
errorMessage = allErrors.get(0).getDefaultMessage();
}
} else {
errorMessage = fieldError.getDefaultMessage();
}
// 转换 CommonResult
if (StrUtil.isEmpty(errorMessage)) {
return CommonResult.error(BAD_REQUEST);
}
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", errorMessage));
}
/**
* 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验
*/
@ExceptionHandler(BindException.class)
public CommonResult<?> bindExceptionHandler(BindException ex) {
log.warn("[handleBindException]", ex);
FieldError fieldError = ex.getFieldError();
assert fieldError != null; // 断言,避免告警
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
}
/**
* 处理 SpringMVC 请求参数类型错误
*
* 例如说,接口上设置了 @RequestBody实体中 xx 属性类型为 Integer结果传递 xx 参数类型为 String
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) {
log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex);
if(ex.getCause() instanceof InvalidFormatException) {
InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause();
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue()));
}else {
return defaultExceptionHandler(ServletUtils.getRequest(), ex);
}
}
/**
* 处理 Validator 校验不通过产生的异常
*/
@ExceptionHandler(value = ConstraintViolationException.class)
public CommonResult<?> constraintViolationExceptionHandler(ConstraintViolationException ex) {
log.warn("[constraintViolationExceptionHandler]", ex);
ConstraintViolation<?> constraintViolation = ex.getConstraintViolations().iterator().next();
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage()));
}
/**
* 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常
*/
@ExceptionHandler(value = ValidationException.class)
public CommonResult<?> validationException(ValidationException ex) {
log.warn("[constraintViolationExceptionHandler]", ex);
// 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读
return CommonResult.error(BAD_REQUEST);
}
/**
* 处理 SpringMVC 请求地址不存在
*
* 注意,它需要设置如下两个配置项:
* 1. spring.mvc.throw-exception-if-no-handler-found 为 true
* 2. spring.mvc.static-path-pattern 为 /statics/**
*/
@ExceptionHandler(NoHandlerFoundException.class)
public CommonResult<?> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) {
log.warn("[noHandlerFoundExceptionHandler]", ex);
return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL()));
}
/**
* 处理 SpringMVC 请求地址不存在
*/
@ExceptionHandler(NoResourceFoundException.class)
private CommonResult<?> noResourceFoundExceptionHandler(HttpServletRequest req, NoResourceFoundException ex) {
log.warn("[noResourceFoundExceptionHandler]", ex);
return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getResourcePath()));
}
/**
* 处理 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()));
}
/**
* 处理 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) {
// 不包含的时候,才进行打印,避免 ex 堆栈过多
if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) {
// 即使打印,也只打印第一层 StackTraceElement并且使用 warn 在控制台输出,更容易看到
try {
StackTraceElement[] stackTraces = ex.getStackTrace();
for (StackTraceElement stackTrace : stackTraces) {
if (ObjUtil.notEqual(stackTrace.getClassName(), ServiceExceptionUtil.class.getName())) {
log.warn("[serviceExceptionHandler]\n\t{}", stackTrace);
break;
}
}
} catch (Exception ignored) {
// 忽略日志,避免影响主流程
}
}
return CommonResult.error(ex.getCode(), ex.getMessage());
}
/**
* 处理系统异常,兜底处理所有的一切
*/
@ExceptionHandler(value = Exception.class)
public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) {
// 情况一:处理表不存在的异常
CommonResult<?> tableNotExistsResult = handleTableNotExists(ex);
if (tableNotExistsResult != null) {
return tableNotExistsResult;
}
// 情况二:处理异常
log.error("[defaultExceptionHandler]", ex);
// 插入异常日志
createExceptionLog(req, ex);
// 返回 ERROR CommonResult
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
}
private void createExceptionLog(HttpServletRequest req, Throwable e) {
// 插入错误日志
ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO();
try {
// 初始化 errorLog
buildExceptionLog(errorLog, req, e);
// 执行插入 errorLog
apiErrorLogApi.createApiErrorLogAsync(errorLog);
} catch (Throwable th) {
log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th);
}
}
private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) {
// 处理用户信息
errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request));
// 设置异常字段
errorLog.setExceptionName(e.getClass().getName());
errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));
errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e));
StackTraceElement[] stackTraceElements = e.getStackTrace();
Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
StackTraceElement stackTraceElement = stackTraceElements[0];
errorLog.setExceptionClassName(stackTraceElement.getClassName());
errorLog.setExceptionFileName(stackTraceElement.getFileName());
errorLog.setExceptionMethodName(stackTraceElement.getMethodName());
errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
// 设置其它字段
errorLog.setTraceId(TracerUtils.getTraceId());
errorLog.setApplicationName(applicationName);
errorLog.setRequestUrl(request.getRequestURI());
Map<String, Object> requestParams = MapUtil.<String, Object>builder()
.put("query", JakartaServletUtil.getParamMap(request))
.put("body", JakartaServletUtil.getBody(request)).build();
errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
errorLog.setRequestMethod(request.getMethod());
errorLog.setUserAgent(ServletUtils.getUserAgent(request));
errorLog.setUserIp(JakartaServletUtil.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("[报表模块 cloud-module-report - 表结构未导入][参考 http://172.16.46.63:30888/report/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[报表模块 cloud-module-report - 表结构未导入][参考 http://172.16.46.63:30888/report/ 开启]");
}
// 2. 工作流
if (message.contains("bpm_")) {
log.error("[工作流模块 cloud-module-bpm - 表结构未导入][参考 http://172.16.46.63:30888/bpm/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[工作流模块 cloud-module-bpm - 表结构未导入][参考 http://172.16.46.63:30888/bpm/ 开启]");
}
// 3. 微信公众号
if (message.contains("mp_")) {
log.error("[微信公众号 cloud-module-mp - 表结构未导入][参考 http://172.16.46.63:30888/mp/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[微信公众号 cloud-module-mp - 表结构未导入][参考 http://172.16.46.63:30888/mp/build/ 开启]");
}
// 4. 商城系统
if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) {
log.error("[商城系统 cloud-module-mall - 已禁用][参考 http://172.16.46.63:30888/mall/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[商城系统 cloud-module-mall - 已禁用][参考 http://172.16.46.63:30888/mall/build/ 开启]");
}
// 5. ERP 系统
if (message.contains("erp_")) {
log.error("[ERP 系统 cloud-module-erp - 表结构未导入][参考 http://172.16.46.63:30888/erp/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[ERP 系统 cloud-module-erp - 表结构未导入][参考 http://172.16.46.63:30888/erp/build/ 开启]");
}
// 6. CRM 系统
if (message.contains("crm_")) {
log.error("[CRM 系统 cloud-module-crm - 表结构未导入][参考 http://172.16.46.63:30888/crm/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[CRM 系统 cloud-module-crm - 表结构未导入][参考 http://172.16.46.63:30888/crm/build/ 开启]");
}
// 7. 支付平台
if (message.contains("pay_")) {
log.error("[支付模块 cloud-module-pay - 表结构未导入][参考 http://172.16.46.63:30888/pay/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[支付模块 cloud-module-pay - 表结构未导入][参考 http://172.16.46.63:30888/pay/build/ 开启]");
}
// 8. AI 大模型
if (message.contains("ai_")) {
log.error("[AI 大模型 cloud-module-ai - 表结构未导入][参考 http://172.16.46.63:30888/ai/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[AI 大模型 cloud-module-ai - 表结构未导入][参考 http://172.16.46.63:30888/ai/build/ 开启]");
}
// 9. IOT 物联网
if (message.contains("iot_")) {
log.error("[IoT 物联网 cloud-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[IoT 物联网 cloud-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
}
return null;
}
}

View File

@@ -0,0 +1,45 @@
package com.zt.plat.framework.web.core.handler;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.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 com.zt.plat.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;
}
}

View File

@@ -0,0 +1,259 @@
package com.zt.plat.framework.web.core.util;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.common.enums.RpcConstants;
import com.zt.plat.framework.common.enums.TerminalEnum;
import com.zt.plat.framework.common.enums.UserTypeEnum;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.web.config.WebProperties;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 专属于 web 包的工具类
*
* @author ZT
*/
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";
public static final String HEADER_VISIT_TENANT_ID = "visit-tenant-id";
public static final String HEADER_VISIT_COMPANY_ID = "visit-company-id";
public static final String HEADER_VISIT_COMPANY_NAME = "visit-company-name";
public static final String HEADER_VISIT_DEPT_ID = "visit-dept-id";
public static final String HEADER_VISIT_DEPT_NAME = "visit-dept-name";
/**
* 终端的 Header
*
* @see com.zt.plat.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;
}
/**
* 获得访问的租户编号,从 header 中
* 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供
*
* @param request 请求
* @return 租户编号
*/
public static Long getVisitTenantId(HttpServletRequest request) {
String tenantId = request.getHeader(HEADER_VISIT_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();
}
/**
* 判断是否为 RPC 请求
*
* @param request 请求
* @return 是否为 RPC 请求
*/
public static boolean isRpcRequest(HttpServletRequest request) {
return request.getRequestURI().startsWith(RpcConstants.RPC_API_PREFIX);
}
/**
* 判断是否为 RPC 请求
*
* 约定大于配置,只要以 Api 结尾,都认为是 RPC 接口
*
* @param className 类名
* @return 是否为 RPC 请求
*/
public static boolean isRpcRequest(String className) {
return className.endsWith("Api");
}
/**
* 获得访问的公司编号,从 header 中
* @param request 请求
* @return 公司部门编号,解析失败或无效时返回 0
*/
public static Long getCompanyId(HttpServletRequest request) {
String companyIdHeader = request.getHeader(HEADER_VISIT_COMPANY_ID);
if (StrUtil.isBlank(companyIdHeader)) {
return 0L;
}
try {
// 解析部门编号列表
return Long.parseLong(companyIdHeader);
} catch (Exception e) {
// 解析失败
return 0L;
}
}
/**
* 获得访问的公司名称,从 header 中,并进行 URL 解码
* @param request 请求
* @return 公司名称,解析失败或无效时返回空字符串
*/
public static String getCompanyName(HttpServletRequest request) {
String companyName = request.getHeader(HEADER_VISIT_COMPANY_NAME);
if (StrUtil.isBlank(companyName)) {
return StrUtil.EMPTY;
}
try {
// URL 解码
return java.net.URLDecoder.decode(companyName, java.nio.charset.StandardCharsets.UTF_8);
} catch (Exception e) {
// 解码失败,返回原始值
return companyName;
}
}
/**
* 获得访问的部门编号,从 header 中
* @param request 请求
* @return 部门编号,解析失败或无效时返回 0
*/
public static Long getDeptId(HttpServletRequest request) {
String deptIdHeader = request.getHeader(WebFrameworkUtils.HEADER_VISIT_DEPT_ID);
if (StrUtil.isBlank(deptIdHeader)) {
return 0L;
}
try {
// 解析部门编号
return Long.parseLong(deptIdHeader);
} catch (Exception e) {
// 解析失败
return 0L;
}
}
/**
* 获得访问的部门名称,从 header 中,并进行 URL 解码
* @param request 请求
* @return 部门名称,解析失败或无效时返回空字符串
*/
public static String getDeptName(HttpServletRequest request) {
String deptName = request.getHeader(WebFrameworkUtils.HEADER_VISIT_DEPT_NAME);
if (StrUtil.isBlank(deptName)) {
return StrUtil.EMPTY;
}
try {
// URL 解码
return java.net.URLDecoder.decode(deptName, java.nio.charset.StandardCharsets.UTF_8);
} catch (Exception e) {
// 解码失败,返回原始值
return deptName;
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* 针对 SpringMVC 的基础封装
*/
package com.zt.plat.framework.web;

View File

@@ -0,0 +1,63 @@
package com.zt.plat.framework.xss.config;
import com.zt.plat.framework.common.enums.WebFilterOrderEnum;
import com.zt.plat.framework.xss.core.clean.JsoupXssCleaner;
import com.zt.plat.framework.xss.core.clean.XssCleaner;
import com.zt.plat.framework.xss.core.filter.XssFilter;
import com.zt.plat.framework.xss.core.json.XssStringJsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import static com.zt.plat.framework.web.config.CloudWebAutoConfiguration.createFilterBean;
@AutoConfiguration
@EnableConfigurationProperties(XssProperties.class)
@ConditionalOnProperty(prefix = "cloud.xss", name = "enable", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
public class CloudXssAutoConfiguration 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 = "cloud.xss.enable", havingValue = "true")
public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties,
PathMatcher pathMatcher,
XssCleaner xssCleaner) {
// 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer在序列化时进行处理
return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, 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);
}
}

View File

@@ -0,0 +1,29 @@
package com.zt.plat.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 ZT
*/
@ConfigurationProperties(prefix = "cloud.xss")
@Validated
@Data
public class XssProperties {
/**
* 是否开启,默认为 true
*/
private boolean enable = true;
/**
* 需要排除的 URL默认为空
*/
private List<String> excludeUrls = Collections.emptyList();
}

View File

@@ -0,0 +1,64 @@
package com.zt.plat.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));
}
}

View File

@@ -0,0 +1,16 @@
package com.zt.plat.framework.xss.core.clean;
/**
* 对 html 文本中的有 Xss 风险的数据进行清理
*/
public interface XssCleaner {
/**
* 清理有 Xss 风险的文本
*
* @param html 原 html
* @return 清理后的 html
*/
String clean(String html);
}

View File

@@ -0,0 +1,52 @@
package com.zt.plat.framework.xss.core.filter;
import com.zt.plat.framework.xss.config.XssProperties;
import com.zt.plat.framework.xss.core.clean.XssCleaner;
import lombok.AllArgsConstructor;
import org.springframework.util.PathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Xss 过滤器
*
* @author ZT
*/
@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));
}
}

View File

@@ -0,0 +1,92 @@
package com.zt.plat.framework.xss.core.filter;
import com.zt.plat.framework.xss.core.clean.XssCleaner;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Xss 请求 Wrapper
*
* @author ZT
*/
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);
}
}

View File

@@ -0,0 +1,82 @@
package com.zt.plat.framework.xss.core.json;
import com.zt.plat.framework.common.util.servlet.ServletUtils;
import com.zt.plat.framework.xss.config.XssProperties;
import com.zt.plat.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 jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PathMatcher;
import java.io.IOException;
/**
* XSS 过滤 jackson 反序列化器。
* 在反序列化的过程中,会对字符串进行 XSS 过滤。
*
* @author Hccake
*/
@Slf4j
@AllArgsConstructor
public class XssStringJsonDeserializer extends StringDeserializer {
/**
* 属性
*/
private final XssProperties properties;
/**
* 路径匹配器
*/
private final PathMatcher pathMatcher;
private final XssCleaner xssCleaner;
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 1. 白名单 URL 的处理
HttpServletRequest request = ServletUtils.getRequest();
if (request != null) {
String uri = ServletUtils.getRequest().getRequestURI();
if (properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri))) {
return p.getText();
}
}
// 2. 真正使用 xssCleaner 进行过滤
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);
}
}

View File

@@ -0,0 +1,6 @@
/**
* 针对 XSS 的基础封装
*
* XSS 说明https://tech.meituan.com/2018/09/27/fe-security.html
*/
package com.zt.plat.framework.xss;

View File

@@ -0,0 +1,6 @@
com.zt.plat.framework.apilog.config.CloudApiLogAutoConfiguration
com.zt.plat.framework.jackson.config.CloudJacksonAutoConfiguration
com.zt.plat.framework.swagger.config.CloudSwaggerAutoConfiguration
com.zt.plat.framework.web.config.CloudWebAutoConfiguration
com.zt.plat.framework.apilog.config.CloudApiLogRpcAutoConfiguration
com.zt.plat.framework.banner.config.CloudBannerAutoConfiguration

View File

@@ -0,0 +1,17 @@
参考文档 http://172.16.46.63:30888/
Application Version: ${cloud.info.version}
Spring Boot Version: ${spring-boot.version}
.__ __. ______ .______ __ __ _______
| \ | | / __ \ | _ \ | | | | / _____|
| \| | | | | | | |_) | | | | | | | __
| . ` | | | | | | _ < | | | | | | |_ |
| |\ | | `--' | | |_) | | `--' | | |__| |
|__| \__| \______/ |______/ \______/ \______|
███╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗
████╗ ██║██╔═══██╗ ██╔══██╗██║ ██║██╔════╝
██╔██╗ ██║██║ ██║ ██████╔╝██║ ██║██║ ███╗
██║╚██╗██║██║ ██║ ██╔══██╗██║ ██║██║ ██║
██║ ╚████║╚██████╔╝ ██████╔╝╚██████╔╝╚██████╔╝
╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝

View File

@@ -0,0 +1,100 @@
package com.zt.plat.framework.desensitize.core;
import com.zt.plat.framework.common.util.json.JsonUtils;
import com.zt.plat.framework.desensitize.core.regex.annotation.EmailDesensitize;
import com.zt.plat.framework.desensitize.core.regex.annotation.RegexDesensitize;
import com.zt.plat.framework.desensitize.core.annotation.Address;
import com.zt.plat.framework.desensitize.core.slider.annotation.BankCardDesensitize;
import com.zt.plat.framework.desensitize.core.slider.annotation.CarLicenseDesensitize;
import com.zt.plat.framework.desensitize.core.slider.annotation.ChineseNameDesensitize;
import com.zt.plat.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize;
import com.zt.plat.framework.desensitize.core.slider.annotation.IdCardDesensitize;
import com.zt.plat.framework.desensitize.core.slider.annotation.PasswordDesensitize;
import com.zt.plat.framework.desensitize.core.slider.annotation.MobileDesensitize;
import com.zt.plat.framework.desensitize.core.slider.annotation.SliderDesensitize;
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.*;
/**
* {@link DesensitizeTest} 的单元测试
*/
@ExtendWith(MockitoExtension.class)
public class DesensitizeTest {
@Test
public void test() {
// 准备参数
DesensitizeDemo desensitizeDemo = new DesensitizeDemo();
desensitizeDemo.setNickname("ZT源码");
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("你好我是ZT源码");
desensitizeDemo.setAddress("北京市海淀区上地十街10号");
desensitizeDemo.setOrigin("ZT源码");
// 调用
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("ZT源码", 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 = "ZT源码", replacer = "*")
private String regex;
@Address
private String address;
private String origin;
}
}

View File

@@ -0,0 +1,30 @@
package com.zt.plat.framework.desensitize.core.annotation;
import com.zt.plat.framework.desensitize.core.DesensitizeTest;
import com.zt.plat.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.zt.plat.framework.desensitize.core.handler.AddressHandler;
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 "*";
}

View File

@@ -0,0 +1,19 @@
package com.zt.plat.framework.desensitize.core.handler;
import com.zt.plat.framework.desensitize.core.DesensitizeTest;
import com.zt.plat.framework.desensitize.core.base.handler.DesensitizationHandler;
import com.zt.plat.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();
}
}

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/Swagger/?cloud>

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/SpringMVC/?cloud>