脱敏处理
This commit is contained in:
parent
eb4823bf5a
commit
93854ec29f
40
src/main/java/com/zhgd/annotation/Desensitize.java
Normal file
40
src/main/java/com/zhgd/annotation/Desensitize.java
Normal file
@ -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;
|
||||
|
||||
}
|
||||
23
src/main/java/com/zhgd/annotation/IgnoreMaskedValue.java
Normal file
23
src/main/java/com/zhgd/annotation/IgnoreMaskedValue.java
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
44
src/main/java/com/zhgd/enums/DesensitizeType.java
Normal file
44
src/main/java/com/zhgd/enums/DesensitizeType.java
Normal file
@ -0,0 +1,44 @@
|
||||
package com.zhgd.enums;
|
||||
|
||||
/**
|
||||
* 脱敏类型枚举类
|
||||
*/
|
||||
public enum DesensitizeType {
|
||||
|
||||
/**
|
||||
* 默认脱敏
|
||||
*/
|
||||
DEFAULT,
|
||||
/**
|
||||
* 自定义脱敏
|
||||
*/
|
||||
CUSTOM_RULE,
|
||||
/**
|
||||
* 手机号脱敏
|
||||
*/
|
||||
PHONE,
|
||||
/**
|
||||
* 电子邮件脱敏
|
||||
*/
|
||||
EMAIL,
|
||||
/**
|
||||
* 身份证号脱敏
|
||||
*/
|
||||
ID_CARD,
|
||||
/**
|
||||
* 银行卡号脱敏
|
||||
*/
|
||||
BANK_CARD,
|
||||
/**
|
||||
* 地址脱敏
|
||||
*/
|
||||
ADDRESS,
|
||||
/**
|
||||
* 中文姓名脱敏
|
||||
*/
|
||||
CHINESE_NAME,
|
||||
/**
|
||||
* 密码脱敏
|
||||
*/
|
||||
PASSWORD,
|
||||
}
|
||||
@ -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<? extends HttpMessageConverter<?>> converterType) {
|
||||
// 只处理带有@RequestBody注解的方法参数
|
||||
return methodParameter.hasParameterAnnotation(RequestBody.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> 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); // 恢复访问权限
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/main/java/com/zhgd/ser/DesensitizeSerializer.java
Normal file
109
src/main/java/com/zhgd/ser/DesensitizeSerializer.java
Normal file
@ -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<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -136,6 +136,7 @@ public class OperLogAspect {
|
||||
List<Object> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前后数据变化
|
||||
*
|
||||
|
||||
@ -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<SystemUser> queryById(@RequestBody Map<String, Object> map) {
|
||||
return Result.success(systemUserService.queryById(map));
|
||||
public Result<SystemUserVo> queryById(@RequestBody Map<String, Object> 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<Page<SystemUser>> getSystemUserBySnPage(@RequestBody Map<String, Object> map) {
|
||||
return Result.success(systemUserService.getSystemUserBySnPage(map));
|
||||
public Result<Page<SystemUserVo>> getSystemUserBySnPage(@RequestBody Map<String, Object> map) {
|
||||
Page<SystemUser> page = systemUserService.getSystemUserBySnPage(map);
|
||||
return Result.success(PageUtil.copyProperties(page, SystemUserVo.class));
|
||||
}
|
||||
|
||||
@OperLog(operModul = "账号管理", operType = "查找项目子账号列表", operDesc = "查找项目子账号列表")
|
||||
|
||||
@ -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;
|
||||
/**
|
||||
* 所属部门
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -241,6 +241,7 @@ public interface SystemUserMapper extends BaseMapper<SystemUser> {
|
||||
* @param map
|
||||
* @return
|
||||
*/
|
||||
@DataScope(includeTable = {"enterprise_info", "system_user"})
|
||||
SystemUser queryById(@Param("param") Map<String, Object> map);
|
||||
|
||||
/**
|
||||
|
||||
37
src/main/java/com/zhgd/xmgl/util/MaskedDataChecker.java
Normal file
37
src/main/java/com/zhgd/xmgl/util/MaskedDataChecker.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user