赞
踩
前言:对敏感数据进行隐藏的做法成为脱敏,实际项目中为了符合等保要求,需要对重要数据进行脱敏显示,例如非管理员不能查看其他用户的手机号登信息。在实现脱敏的方式中,最直观就是通过在接口中对敏感字符串进行替换,但此方式会增加代码量,耦合大,增加维护成本,因此采用自定义序列化方式。
springboot中在controller返回后,向前端发送数据前,需要经过序列化,将数据转化为前端能接收的数据类型,例如json或者xml,通过修改属性的序列化方法,将敏感字段做处理,实现数据脱敏操作。
以下内容基本搬运自文章【SpringBoot中利用自定义注解优雅地实现隐私数据脱敏(加密显示)】
来源链接: https://blog.csdn.net/qq_36737803/article/details/122366043
public enum DesensitizeTypeEnum { /** 自定义(此项需设置脱敏的范围)*/ CUSTOMER, /** 姓名(有默认脱敏规则,设置脱敏范围参数不生效) */ NAME, /** 身份证号(有默认脱敏规则,设置脱敏范围参数不生效) */ ID_CARD, /** 手机号(有默认脱敏规则,设置脱敏范围参数不生效) */ PHONE, /** 邮箱(有默认脱敏规则,设置脱敏范围参数不生效) */ EMAIL; }
这里枚举类的作用是定义默认脱敏行为,实现枚举类型的脱敏方式,因为像邮箱这种特殊格式,通过特定的正则表达式脱敏效果会更好。
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside // 告诉jackson这是个自定义Jackson注解,在jackson序列化时被关注 @JsonSerialize(using = DesensitizeSerializer.class) //告诉jackson被改注解修饰的字段,应该使用自定义序列化器 public @interface DesensitizeField { /** * 脱敏数据类型(没给默认值,所以使用时必须指定type) */ DesensitizeTypeEnum type(); /** * 前置不需要打码的长度 */ int prefixNoMaskLen() default 1; /** * 后置不需要打码的长度 */ int suffixNoMaskLen() default 1; /** * 用什么打码 */ String symbol() default "*"; }
@NoArgsConstructor @AllArgsConstructor @Getter public class DesensitizeSerializer extends JsonSerializer<String> implements ContextualSerializer { // 脱敏类型 private DesensitizeTypeEnum type; // 前几位不脱敏 private Integer prefixNoMaskLen; // 最后几位不脱敏 private Integer suffixNoMaskLen; // 用什么打码 private String symbol; /** * 标有脱敏注解的字段调用的序列化方法 * @param value 脱敏前字符串 * @param gen 用于输出生成的Json内容的生成器 * @param serializers 提供序列化对象的工厂,可提供当前序列化器和其他序列化器 * @throws IOException */ @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { // 对序列化对象判空处理,只有非空对象才进行json输出 if (!ObjectUtils.isEmpty(value)) { // 写入序列化结果,DesensitizeUtils.desensitize()为自定义字符串脱敏方法 gen.writeString(DesensitizeUtils.desensitize(value,this)); } } /** * 构建自定义序列化器的上下文,判断序列化对象是否走自定义序列化器(通过判断是否带有自定义序列化注解) * @param serializerProvider 提供序列化对象的工厂 * @param beanProperty 要序列化的对象 * @return * @throws JsonMappingException */ @Override public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException { if (null != beanProperty) { if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) { // 获取序列化对象上的自定义序列化注解 DesensitizeField desensitizeField = beanProperty.getAnnotation(DesensitizeField.class); if (null == desensitizeField) { desensitizeField = beanProperty.getContextAnnotation(DesensitizeField.class); } // 如果注解对象不为空,需要走自定义脱敏序列化器 if (null != desensitizeField) { return new DesensitizeSerializer(desensitizeField.type(), desensitizeField.prefixNoMaskLen(), desensitizeField.suffixNoMaskLen(), desensitizeField.symbol()); } } // 如果对象不为空或者没有自定义脱敏注解标注,走其他类型序列化器 return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); } // 如果对象为空,走空序列化器 return serializerProvider.findNullValueSerializer(null); } }
public class DesensitizeUtils { /** * 判断脱敏类型,根据不同脱敏类型选择不同脱敏方法进行脱敏 * @param value 原始字符串数据 * @param serializer 自定义的序列化其,主要是获取脱敏参数 * @return 脱敏后字符串 */ public static String desensitize(String value, DesensitizeSerializer serializer){ String desensitizeValue; switch (serializer.getType()){ case CUSTOMER: desensitizeValue = desValue(value, serializer.getPrefixNoMaskLen(), serializer.getSuffixNoMaskLen(), serializer.getSymbol()); break; case NAME: desensitizeValue = hideChineseName(value); break; case EMAIL: desensitizeValue = hideEmail(value); break; case PHONE: desensitizeValue = hidePhone(value); break; case ID_CARD: desensitizeValue = hideIDCard(value); break; default: throw new IllegalArgumentException("unknown privacy type enum " + serializer.getType()); } return desensitizeValue; } /** * 中文名脱敏 */ public static String hideChineseName(String chineseName) { if (ObjectUtils.isEmpty(chineseName)) { return null; } if (chineseName.length() <= 2) { return desValue(chineseName, 1, 0, "*"); } return desValue(chineseName, 1, 1, "*"); } /** * 手机号脱敏 */ public static String hidePhone(String phone) { if (ObjectUtils.isEmpty(phone)) { return null; } return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); // 隐藏中间4位 // return desValue(phone, 3, 4, "*"); // 隐藏中间4位 // return desValue(phone, 7, 0, "*"); // 隐藏末尾4位 } /** * 邮箱脱敏 */ public static String hideEmail(String email) { if (ObjectUtils.isEmpty(email)) { return null; } return email.replaceAll("(\\w?)(\\w+)(\\w)(@\\w+\\.[a-z]+(\\.[a-z]+)?)", "$1****$3$4"); } /** * 身份证号脱敏 */ public static String hideIDCard(String idCard) { if (ObjectUtils.isEmpty(idCard)) { return null; } return idCard.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1*****$2"); } /** * 对字符串进行脱敏操作 * @param origin 原始字符串 * @param prefixNoMaskLen 左侧需要保留几位明文字段 * @param suffixNoMaskLen 右侧需要保留几位明文字段 * @param maskStr 用于遮罩的字符串, 如'*' * @return 脱敏后结果 */ public static String desValue(String origin, int prefixNoMaskLen, int suffixNoMaskLen, String maskStr) { if (ObjectUtils.isEmpty(origin)) { return origin; } StringBuilder sb = new StringBuilder(); for (int i = 0, n = origin.length(); i < n; i++) { if (i < prefixNoMaskLen) { sb.append(origin.charAt(i)); continue; } if (i > (n - suffixNoMaskLen - 1)) { sb.append(origin.charAt(i)); continue; } sb.append(maskStr); } return sb.toString(); } }
至此,自定义序列化方法实现脱敏构建完成
构建实体类,使用自定义注解
@Data @ToString public class UserInfo { private String userId; @DesensitizeField(type = DesensitizeTypeEnum.PHONE) private String mobile; @DesensitizeField(type = DesensitizeTypeEnum.CUSTOMER,prefixNoMaskLen = 0,suffixNoMaskLen = 0) private String password; @DesensitizeField(type = DesensitizeTypeEnum.ID_CARD) private String identityId; @DesensitizeField(type = DesensitizeTypeEnum.EMAIL) private String email; private String createTime; }
测试接口
@GetMapping("test")
public UserInfo getUserInfo(){
UserInfo userInfo = new UserInfo();
userInfo.setUserId("100001");
userInfo.setMobile("12345678910");
userInfo.setPassword("Mm123456");
userInfo.setIdentityId("430444199901017878");
userInfo.setEmail("zhangsanlisi123@qq.com");
userInfo.setCreateTime(null);
return userInfo;
}
测试结果如下图
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。