diff --git a/src/main/java/com/zhgd/annotation/Desensitize.java b/src/main/java/com/zhgd/annotation/Desensitize.java new file mode 100644 index 000000000..26f2c4a4d --- /dev/null +++ b/src/main/java/com/zhgd/annotation/Desensitize.java @@ -0,0 +1,40 @@ +package com.zhgd.annotation; + +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.zhgd.enums.DesensitizeType; +import com.zhgd.ser.DesensitizeSerializer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 用于标记字段需要进行脱敏处理的注解 + * + * @author shijun + * @date 2024/07/09 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@JacksonAnnotationsInside +@JsonSerialize(using = DesensitizeSerializer.class) +public @interface Desensitize { + + /** + * 脱敏类型 + */ + DesensitizeType type() default DesensitizeType.DEFAULT; + + /** + * 脱敏起始位置 + */ + int startInclude() default 0; + + /** + * 脱敏结束位置 + */ + int endExclude() default 0; + +} diff --git a/src/main/java/com/zhgd/annotation/IgnoreMaskedValue.java b/src/main/java/com/zhgd/annotation/IgnoreMaskedValue.java new file mode 100644 index 000000000..5bd0f1862 --- /dev/null +++ b/src/main/java/com/zhgd/annotation/IgnoreMaskedValue.java @@ -0,0 +1,23 @@ +package com.zhgd.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 标记该字段需要被检查,如果值是脱敏格式(如包含****),则在反序列化时将其设置为null + * 仅用于String类型字段 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface IgnoreMaskedValue { + /** + * 脱敏模式,默认为手机号 + */ + DesensitizeType type() default DesensitizeType.MOBILE_PHONE; + + enum DesensitizeType { + MOBILE_PHONE, ID_CARD, BANK_CARD, CUSTOM + } +} diff --git a/src/main/java/com/zhgd/enums/DesensitizeType.java b/src/main/java/com/zhgd/enums/DesensitizeType.java new file mode 100644 index 000000000..9faae4fd6 --- /dev/null +++ b/src/main/java/com/zhgd/enums/DesensitizeType.java @@ -0,0 +1,44 @@ +package com.zhgd.enums; + +/** + * 脱敏类型枚举类 + */ +public enum DesensitizeType { + + /** + * 默认脱敏 + */ + DEFAULT, + /** + * 自定义脱敏 + */ + CUSTOM_RULE, + /** + * 手机号脱敏 + */ + PHONE, + /** + * 电子邮件脱敏 + */ + EMAIL, + /** + * 身份证号脱敏 + */ + ID_CARD, + /** + * 银行卡号脱敏 + */ + BANK_CARD, + /** + * 地址脱敏 + */ + ADDRESS, + /** + * 中文姓名脱敏 + */ + CHINESE_NAME, + /** + * 密码脱敏 + */ + PASSWORD, +} diff --git a/src/main/java/com/zhgd/masked/MaskedValueRequestBodyAdvice.java b/src/main/java/com/zhgd/masked/MaskedValueRequestBodyAdvice.java new file mode 100644 index 000000000..612324399 --- /dev/null +++ b/src/main/java/com/zhgd/masked/MaskedValueRequestBodyAdvice.java @@ -0,0 +1,89 @@ +package com.zhgd.masked; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zhgd.annotation.IgnoreMaskedValue; +import com.zhgd.xmgl.util.MaskedDataChecker; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter; + +import java.lang.reflect.Field; +import java.lang.reflect.Type; + +/** + * 脱敏后的数据不保存到数据库 + */ +@ControllerAdvice +public class MaskedValueRequestBodyAdvice extends RequestBodyAdviceAdapter { + + private final ObjectMapper objectMapper; + + public MaskedValueRequestBodyAdvice(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public boolean supports(MethodParameter methodParameter, Type targetType, Class> converterType) { + // 只处理带有@RequestBody注解的方法参数 + return methodParameter.hasParameterAnnotation(RequestBody.class); + } + + @Override + public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class> converterType) { + // 在body被反序列化后,Controller方法执行前,进行后处理 + return processMaskedValues(body); + } + + /** + * 递归处理对象中的被@IgnoreMaskedValue标记的字段 + */ + private Object processMaskedValues(Object obj) { + if (obj == null) { + return null; + } + + Class clazz = obj.getClass(); + // 检查是否是Java标准类或集合等(简单处理,实际可更复杂) + if (clazz.getName().startsWith("java.")) { + return obj; + } + + // 遍历所有字段 + for (Field field : clazz.getDeclaredFields()) { + // 检查字段是否被@IgnoreMaskedValue标记 + IgnoreMaskedValue annotation = field.getAnnotation(IgnoreMaskedValue.class); + if (annotation != null && field.getType().equals(String.class)) { + processField(obj, field, annotation); + } + } + return obj; + } + + /** + * 处理单个字段 + */ + private void processField(Object obj, Field field, IgnoreMaskedValue annotation) { + try { + field.setAccessible(true); // 允许访问私有字段 + String currentValue = (String) field.get(obj); + + if (currentValue != null) { + // 判断当前值是否是脱敏格式 + if (MaskedDataChecker.isMaskedValue(currentValue, annotation.type())) { + // 如果是脱敏格式,则设置为null + field.set(obj, null); + // 可以在这里记录日志 + // log.debug("检测到脱敏字段 {},值已设置为null", field.getName()); + } + } + } catch (IllegalAccessException e) { + // 忽略访问异常,或者记录日志 + // log.warn("处理脱敏字段时发生异常: {}", field.getName(), e); + } finally { + field.setAccessible(false); // 恢复访问权限 + } + } +} diff --git a/src/main/java/com/zhgd/ser/DesensitizeSerializer.java b/src/main/java/com/zhgd/ser/DesensitizeSerializer.java new file mode 100644 index 000000000..94f6983a6 --- /dev/null +++ b/src/main/java/com/zhgd/ser/DesensitizeSerializer.java @@ -0,0 +1,109 @@ +package com.zhgd.ser; + +import cn.hutool.core.util.DesensitizedUtil; +import cn.hutool.core.util.StrUtil; +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.zhgd.annotation.Desensitize; +import com.zhgd.enums.DesensitizeType; + +import java.io.IOException; + +/** + * 脱敏序列化器,用于在序列化字符串时根据不同的脱敏类型进行数据脱敏。 + * + * @author shijun + * @date 2024/07/08 + */ +public class DesensitizeSerializer extends JsonSerializer implements ContextualSerializer { + + /** + * 脱敏类型,默认为DEFAULT + */ + private DesensitizeType type; + /** + * 脱敏起始位置 + */ + private int startInclude; + /** + * 脱敏结束位置 + */ + private int endExclude; + + public DesensitizeSerializer() { + this.type = DesensitizeType.DEFAULT; + } + + + public DesensitizeSerializer(DesensitizeType type) { + this.type = type; + } + + /** + * 序列化字符串时调用,根据脱敏类型对字符串进行相应的脱敏处理。 + * + * @param value 待序列化的字符串 + * @param gen JSON生成器,用于写入处理后的字符串 + * @param serializers 序列化器提供者,用于获取其他序列化器 + * @throws IOException 如果序列化过程中发生I/O错误 + */ + @Override + public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + switch (type) { + case CUSTOM_RULE: + // 这里是对字符串的startInclude到endExclude字段进行隐藏处理,如果想要实现两端保留,可以考虑使用StrUtil的replace方法 + gen.writeString(StrUtil.hide(value, startInclude, endExclude)); + break; + case PHONE: + gen.writeString(DesensitizedUtil.mobilePhone(value)); + break; + case EMAIL: + gen.writeString(DesensitizedUtil.email(value)); + break; + case ID_CARD: + gen.writeString(DesensitizedUtil.idCardNum(value, 1, 2)); + break; + case BANK_CARD: + gen.writeString(DesensitizedUtil.bankCard(value)); + break; + case ADDRESS: + gen.writeString(DesensitizedUtil.address(value, 8)); + break; + case CHINESE_NAME: + gen.writeString(DesensitizedUtil.chineseName(value)); + break; + case PASSWORD: + gen.writeString(DesensitizedUtil.password(value)); + break; + default: + gen.writeString(value); + break; + } + } + + /** + * 根据上下文信息创建自定义的序列化器,用于处理带有@Desensitize注解的属性。 + * + * @param prov 序列化器提供者,用于获取其他序列化器 + * @param property 当前属性的信息,用于获取注解和属性类型 + * @return 自定义的序列化器实例 + */ + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) { + if (property != null) { + Desensitize annotation = property.getAnnotation(Desensitize.class); + if (annotation != null) { + this.type = annotation.type(); + if (annotation.type() == DesensitizeType.CUSTOM_RULE) { + this.startInclude = annotation.startInclude(); + this.endExclude = annotation.endExclude(); + } + } + } + return this; + } + +} diff --git a/src/main/java/com/zhgd/xmgl/config/OperLogAspect.java b/src/main/java/com/zhgd/xmgl/config/OperLogAspect.java index b241b9cbf..92aa50f67 100644 --- a/src/main/java/com/zhgd/xmgl/config/OperLogAspect.java +++ b/src/main/java/com/zhgd/xmgl/config/OperLogAspect.java @@ -136,6 +136,7 @@ public class OperLogAspect { List logArgs = Arrays.stream(args).filter(arg -> (!(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse) && !(arg instanceof MultipartFile))).collect(Collectors.toList()); // 判断中文字符数量(一个中文字符占 1 个长度) String body = JSON.toJSONString(logArgs); + body = getHideSensitiveParamBody(body, request.getRequestURI()); if (body.length() > 1000) { body = body.substring(0, 1000) + "..."; } @@ -232,6 +233,22 @@ public class OperLogAspect { } } + /** + * 获取隐藏敏感参数的body + * + * @param body + * @param requestURI + * @return + */ + private String getHideSensitiveParamBody(String body, String requestURI) { + if (Objects.equals(requestURI, "/xmgl/base/md5/login") || Objects.equals(requestURI, "/xmgl/base/verify/login")) { + JSONArray jsonArray = JSONArray.parseArray(body); + jsonArray.getJSONObject(0).put("md5Password", Cts.SENSITIVE_CHAR); + return JSON.toJSONString(jsonArray); + } + return body; + } + /** * 获取前后数据变化 * diff --git a/src/main/java/com/zhgd/xmgl/modules/basicdata/controller/SystemUserController.java b/src/main/java/com/zhgd/xmgl/modules/basicdata/controller/SystemUserController.java index 6ead22b43..692184376 100644 --- a/src/main/java/com/zhgd/xmgl/modules/basicdata/controller/SystemUserController.java +++ b/src/main/java/com/zhgd/xmgl/modules/basicdata/controller/SystemUserController.java @@ -1,12 +1,15 @@ package com.zhgd.xmgl.modules.basicdata.controller; +import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.zhgd.annotation.OperLog; import com.zhgd.jeecg.common.api.vo.Result; import com.zhgd.xmgl.modules.basicdata.entity.SystemUser; +import com.zhgd.xmgl.modules.basicdata.entity.vo.SystemUserVo; import com.zhgd.xmgl.modules.basicdata.service.ISystemUserService; +import com.zhgd.xmgl.util.PageUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; @@ -119,8 +122,9 @@ public class SystemUserController { @ApiOperation(value = "通过id查询账号信息", notes = "通过id查询账号信息", httpMethod = "POST") @ApiImplicitParam(name = "id", value = "账号ID", paramType = "body", required = true, dataType = "Integer") @PostMapping(value = "/queryById") - public Result queryById(@RequestBody Map map) { - return Result.success(systemUserService.queryById(map)); + public Result queryById(@RequestBody Map map) { + SystemUser systemUser = systemUserService.queryById(map); + return Result.success(BeanUtil.toBean(systemUser, SystemUserVo.class)); } @OperLog(operModul = "账号管理", operType = "修改用户密码", operDesc = "修改用户密码") @@ -154,8 +158,9 @@ public class SystemUserController { @ApiImplicitParam(name = "queryType", required = true, value = "default:默认企业或项目SN查找账号列表,projectLevel:查询项目级别账号,projectLevelAndChildren:查询项目级别和项目子账号", paramType = "body"), }) @PostMapping(value = "/getSystemUserBySnPage") - public Result> getSystemUserBySnPage(@RequestBody Map map) { - return Result.success(systemUserService.getSystemUserBySnPage(map)); + public Result> getSystemUserBySnPage(@RequestBody Map map) { + Page page = systemUserService.getSystemUserBySnPage(map); + return Result.success(PageUtil.copyProperties(page, SystemUserVo.class)); } @OperLog(operModul = "账号管理", operType = "查找项目子账号列表", operDesc = "查找项目子账号列表") diff --git a/src/main/java/com/zhgd/xmgl/modules/basicdata/entity/SystemUser.java b/src/main/java/com/zhgd/xmgl/modules/basicdata/entity/SystemUser.java index cd20ae962..0a6e90cc8 100644 --- a/src/main/java/com/zhgd/xmgl/modules/basicdata/entity/SystemUser.java +++ b/src/main/java/com/zhgd/xmgl/modules/basicdata/entity/SystemUser.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; +import com.zhgd.annotation.IgnoreMaskedValue; import com.zhgd.xmgl.modules.worker.entity.WorkerInfo; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @@ -53,6 +54,7 @@ public class SystemUser implements Serializable { */ @Excel(name = "明文密码", width = 15) @ApiModelProperty(value = "明文密码") + @IgnoreMaskedValue private java.lang.String showPassword; /** @@ -77,6 +79,7 @@ public class SystemUser implements Serializable { */ @Excel(name = "人员电话", width = 15) @ApiModelProperty(value = "人员电话") + @IgnoreMaskedValue private java.lang.String userTel; /** * 所属部门 diff --git a/src/main/java/com/zhgd/xmgl/modules/basicdata/entity/vo/SystemUserVo.java b/src/main/java/com/zhgd/xmgl/modules/basicdata/entity/vo/SystemUserVo.java new file mode 100644 index 000000000..c36d31a8b --- /dev/null +++ b/src/main/java/com/zhgd/xmgl/modules/basicdata/entity/vo/SystemUserVo.java @@ -0,0 +1,20 @@ +package com.zhgd.xmgl.modules.basicdata.entity.vo; + +import com.zhgd.annotation.Desensitize; +import com.zhgd.enums.DesensitizeType; +import com.zhgd.xmgl.modules.basicdata.entity.SystemUser; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class SystemUserVo extends SystemUser { + @Desensitize(type = DesensitizeType.PASSWORD) + @ApiModelProperty(value = "明文密码") + private java.lang.String showPassword; + /** + * 人员电话 + */ + @Desensitize(type = DesensitizeType.PHONE) + @ApiModelProperty(value = "人员电话") + private java.lang.String userTel; +} diff --git a/src/main/java/com/zhgd/xmgl/modules/basicdata/mapper/SystemUserMapper.java b/src/main/java/com/zhgd/xmgl/modules/basicdata/mapper/SystemUserMapper.java index e6d8514cd..877def1e5 100644 --- a/src/main/java/com/zhgd/xmgl/modules/basicdata/mapper/SystemUserMapper.java +++ b/src/main/java/com/zhgd/xmgl/modules/basicdata/mapper/SystemUserMapper.java @@ -241,6 +241,7 @@ public interface SystemUserMapper extends BaseMapper { * @param map * @return */ + @DataScope(includeTable = {"enterprise_info", "system_user"}) SystemUser queryById(@Param("param") Map map); /** diff --git a/src/main/java/com/zhgd/xmgl/util/MaskedDataChecker.java b/src/main/java/com/zhgd/xmgl/util/MaskedDataChecker.java new file mode 100644 index 000000000..f9bcbc6b5 --- /dev/null +++ b/src/main/java/com/zhgd/xmgl/util/MaskedDataChecker.java @@ -0,0 +1,37 @@ +package com.zhgd.xmgl.util; + +import com.zhgd.annotation.IgnoreMaskedValue; +import org.springframework.util.StringUtils; + +import java.util.regex.Pattern; + +public class MaskedDataChecker { + + // 常见脱敏格式的正则表达式模式 + private static final Pattern MOBILE_MASKED_PATTERN = Pattern.compile("^1[3-9]\\d{1}\\*{4}\\d{4}$"); + private static final Pattern ID_CARD_MASKED_PATTERN = Pattern.compile("^[1-9]\\d{5}\\*{8,10}\\d{3}[0-9Xx]?$"); + private static final Pattern BANK_CARD_MASKED_PATTERN = Pattern.compile("^\\d{4}\\*{8,12}\\d{4}$"); + private static final Pattern GENERIC_MASKED_PATTERN = Pattern.compile(".*\\*{3,}.*"); + + /** + * 根据类型判断字符串是否是脱敏格式 + */ + public static boolean isMaskedValue(String value, IgnoreMaskedValue.DesensitizeType type) { + if (!StringUtils.hasText(value)) { + return false; + } + + switch (type) { + case MOBILE_PHONE: + return MOBILE_MASKED_PATTERN.matcher(value).matches(); + case ID_CARD: + return ID_CARD_MASKED_PATTERN.matcher(value).matches(); + case BANK_CARD: + return BANK_CARD_MASKED_PATTERN.matcher(value).matches(); + case CUSTOM: + return GENERIC_MASKED_PATTERN.matcher(value).matches(); + default: + return GENERIC_MASKED_PATTERN.matcher(value).matches(); + } + } +}