⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/qq_36737803/article/details/122366043 「Taoge526」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

这两天在整改等保测出的问题,里面有一个“用户信息泄露”的风险项(就是后台系统里用户的一些隐私数据直接明文显示了),其实指的就是要做数据脱敏。

数据脱敏:把系统里的一些敏感数据进行加密处理后再返回,达到保护隐私作用,实现效果图如下:

其实要实现上面的效果,可能最先想到的方法是直接改每个controller接口,在返回数据前做一次加密处理,当然这个方法肯定是非常捞的。这里推荐用注解来实现,即高效又优雅,省时省力,支持扩展。

其实解决方案大体上分两种:

  • 在拿到数据时就已经脱敏了(如在 mysql 查询时用 insert 函数进行隐藏)
  • 拿到数据后在序列化的时候再进行脱敏(如用 fastjson、jackson)

这里我所选用的方案是第二种,即在接口返回数据前,在序列化的时候对敏感字段值进行处理,并且选用 jackson 的序列化来实现(推荐)

1. 创建隐私数据类型枚举:PrivacyTypeEnum

import lombok.Getter;

/**
* 隐私数据类型枚举
*/
@Getter
public enum PrivacyTypeEnum {

/** 自定义(此项需设置脱敏的范围)*/
CUSTOMER,

/** 姓名 */
NAME,

/** 身份证号 */
ID_CARD,

/** 手机号 */
PHONE,

/** 邮箱 */
EMAIL,
}

2. 创建自定义隐私注解:PrivacyEncrypt

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 自定义数据脱敏注解
*/
@Target(ElementType.FIELD) // 作用在字段上
@Retention(RetentionPolicy.RUNTIME) // class文件中保留,运行时也保留,能通过反射读取到
@JacksonAnnotationsInside // 表示自定义自己的注解PrivacyEncrypt
@JsonSerialize(using = PrivacySerializer.class) // 该注解使用序列化的方式
public @interface PrivacyEncrypt {

/**
* 脱敏数据类型(没给默认值,所以使用时必须指定type)
*/
PrivacyTypeEnum type();

/**
* 前置不需要打码的长度
*/
int prefixNoMaskLen() default 1;

/**
* 后置不需要打码的长度
*/
int suffixNoMaskLen() default 1;

/**
* 用什么打码
*/
String symbol() default "*";
}

3. 创建自定义序列化器:PrivacySerializer

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.zk.common.core.domain.enumerate.PrivacyTypeEnum;
import com.zk.common.core.utils.PrivacyUtil;
import java.io.IOException;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;

@NoArgsConstructor
@AllArgsConstructor
public class PrivacySerializer extends JsonSerializer<String> implements ContextualSerializer {

// 脱敏类型
private PrivacyTypeEnum privacyTypeEnum;
// 前几位不脱敏
private Integer prefixNoMaskLen;
// 最后几位不脱敏
private Integer suffixNoMaskLen;
// 用什么打码
private String symbol;

@Override
public void serialize(final String origin, final JsonGenerator jsonGenerator,
final SerializerProvider serializerProvider) throws IOException {
if (StringUtils.isNotBlank(origin) && null != privacyTypeEnum) {
switch (privacyTypeEnum) {
case CUSTOMER:
jsonGenerator.writeString(PrivacyUtil.desValue(origin, prefixNoMaskLen, suffixNoMaskLen, symbol));
break;
case NAME:
jsonGenerator.writeString(PrivacyUtil.hideChineseName(origin));
break;
case ID_CARD:
jsonGenerator.writeString(PrivacyUtil.hideIDCard(origin));
break;
case PHONE:
jsonGenerator.writeString(PrivacyUtil.hidePhone(origin));
break;
case EMAIL:
jsonGenerator.writeString(PrivacyUtil.hideEmail(origin));
break;
default:
throw new IllegalArgumentException("unknown privacy type enum " + privacyTypeEnum);
}
}
}

@Override
public JsonSerializer<?> createContextual(final SerializerProvider serializerProvider,
final BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) {
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
PrivacyEncrypt privacyEncrypt = beanProperty.getAnnotation(PrivacyEncrypt.class);
if (privacyEncrypt == null) {
privacyEncrypt = beanProperty.getContextAnnotation(PrivacyEncrypt.class);
}
if (privacyEncrypt != null) {
return new PrivacySerializer(privacyEncrypt.type(), privacyEncrypt.prefixNoMaskLen(),
privacyEncrypt.suffixNoMaskLen(), privacyEncrypt.symbol());
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(null);
}
}

这里是具体的实现过程,因为要脱敏的数据都是 String 类型的,所以继承 JsonSerializer 时的类型填 String

重写的 serialize 方法是实现脱敏的核心,根据类型 type 的不同去设置序列化后的值

重写的 createContextual 方法就是去读取我们自定义的 PrivacyEncrypt 注解,打造一个上下文的环境。

4. 隐私数据隐藏工具类:PrivacyUtil

public class PrivacyUtil {

/**
* 隐藏手机号中间四位
*/
public static String hidePhone(String phone) {
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}

/**
* 隐藏邮箱
*/
public static String hideEmail(String email) {
return email.replaceAll("(\\w?)(\\w+)(\\w)(@\\w+\\.[a-z]+(\\.[a-z]+)?)", "$1****$3$4");
}

/**
* 隐藏身份证
*/
public static String hideIDCard(String idCard) {
return idCard.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1*****$2");
}

/**
* 【中文姓名】只显示第一个汉字,其他隐藏为星号,比如:任**
*/
public static String hideChineseName(String chineseName) {
if (chineseName == null) {
return null;
}
return desValue(chineseName, 1, 0, "*");
}

// /**
// * 【身份证号】显示前4位, 后2位
// */
// public static String hideIdCard(String idCard) {
// return desValue(idCard, 4, 2, "*");
// }

// /**
// * 【手机号码】前三位,后四位,其他隐藏。
// */
// public static String hidePhone(String phone) {
// return desValue(phone, 3, 4, "*");
// }

/**
* 对字符串进行脱敏操作
* @param origin 原始字符串
* @param prefixNoMaskLen 左侧需要保留几位明文字段
* @param suffixNoMaskLen 右侧需要保留几位明文字段
* @param maskStr 用于遮罩的字符串, 如'*'
* @return 脱敏后结果
*/
public static String desValue(String origin, int prefixNoMaskLen, int suffixNoMaskLen, String maskStr) {
if (origin == null) {
return null;
}
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();
}

public static void main(String[] args) {
System.out.println(hideChineseName("张三三"));
}
}

这个工具类其实可以自己定,根据自己的业务去扩展,提一点:

在自定义注解 PrivacyEncrypt 里,只有 type 的值为 PrivacyTypeEnum.CUSTOMER(自定义)时,才需要指定脱敏范围,即 prefixNoMaskLensuffixNoMaskLen 的值,像邮箱、手机号这种隐藏格式都采用固定的

5. 注解使用

直接在需要脱敏的字段上加上注解,指定 type 值即可,如下:

@Data
public class People {

private Integer id;

private String name;

private Integer sex;

private Integer age;

@PrivacyEncrypt(type = PrivacyTypeEnum.PHONE) // 隐藏手机号
private String phone;

@PrivacyEncrypt(type = PrivacyTypeEnum.EMAIL) // 隐藏邮箱
private String email;

private String sign;
}

到这里,脱敏工作就已经结束了,全局使用这一个注解即可,一劳永逸,测试效果图如下:

以上代码已经过测试,并已实际使用,所以可放心使用

文章目录
  1. 1. 1. 创建隐私数据类型枚举:PrivacyTypeEnum
  2. 2. 2. 创建自定义隐私注解:PrivacyEncrypt
  3. 3. 3. 创建自定义序列化器:PrivacySerializer
  4. 4. 4. 隐私数据隐藏工具类:PrivacyUtil
  5. 5. 5. 注解使用