1. 为何要对数据进行脱敏
对数据进行脱敏(Data Masking)主要是出于以下几个原因:
-
保护隐私
- 数据脱敏可以有效地隐藏敏感信息,如个人身份信息、银行账户信息等,从而防止这些信息在开发、测试或数据分析过程中被泄露。这对于遵守数据保护法规至关重要,比如GDPR和中国的《个人信息保护法》。
-
安全合规
- 许多行业都有严格的数据保护标准和合规要求。通过对数据进行脱敏处理,企业能够确保其数据处理活动符合这些规定,避免因数据泄露导致的法律风险和罚款。
-
降低风险
- 在软件开发过程中,使用真实数据可能会增加数据泄露的风险。通过使用脱敏数据,即使发生数据泄露,也只会影响非敏感信息,从而降低了潜在的损害。
废话不多说,直接上代码
2. 自定义字符串序列化器
原理:通过序列化器拦截JSON字符串的序列化,在序列化前将字符进行替换
1. 自定义注解 -> 指定序列化器
@Documented
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside // 此注解是其他所有 jackson 注解的元注解,打上了此注解的注解表明是 jackson 注解的一部分
@JsonSerialize(using = StringDesensitizeSerializer.class) // 指定序列化器
public @interface DesensitizeBy {
/**
* 脱敏处理器
*/
@SuppressWarnings("rawtypes")
Class<? extends DesensitizationHandler> handler();
}
2. 实现自定义序列化器
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) throws JsonMappingException {
// 查看bean的属性上有没有DesensitizeBy注解
DesensitizeBy annotation = beanProperty.getAnnotation(DesensitizeBy.class);
if (Objects.isNull(annotation)) {
return this;
}
// 创建一个 StringDesensitizeSerializer 对象,使用 DesensitizeBy 对应的处理器
StringDesensitizeSerializer serializer = new StringDesensitizeSerializer();
serializer.setDesensitizationHandler(Singleton.get(annotation.handler()));
return serializer;
}
@Override
public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (StrUtil.isBlank(value)) {
jsonGenerator.writeNull();
return;
}
//需要脱敏的属性名
String currentName = jsonGenerator.getOutputContext().getCurrentName();
//类的所有属性名极其对应的值
Object currentValue = jsonGenerator.getCurrentValue();
Class<?> currentValueClass = currentValue.getClass();
//通过反射拿到指定属性名的值
Field field = ReflectUtil.getField(currentValueClass, currentName);
for (Annotation annotation : field.getAnnotations()) {
//如果该类属性上面有DesensitizeBy,则进行序列化
if (AnnotationUtil.hasAnnotation(annotation.annotationType(), DesensitizeBy.class)) {
value = this.desensitizationHandler.desensitize(value, annotation);
jsonGenerator.writeString(value);
return;
}
}
}
}
3. 动态实现不同的脱敏器
1. 顶层脱敏接口
public interface DesensitizationHandler<T extends Annotation> {
/**
* 脱敏
*
* @param origin 原始字符串
* @param annotation 注解信息
* @return 脱敏后的字符串
*/
String desensitize(String origin, T annotation);
/**
* 是否禁用脱敏的 Spring EL 表达式
* <p>
* 如果返回 true 则跳过脱敏
*
* @param annotation 注解信息
* @return 是否禁用脱敏的 Spring EL 表达式
*/
default String getDisable(T annotation) {
// 约定:默认就是 enable() 属性。如果不符合,子类重写
try {
return (String) ReflectUtil.invoke(annotation, "disable");
} catch (Exception ex) {
return "";
}
}
}
2. 脱敏器默认实现类
public abstract class AbstractSliderDesensitizationHandler<T extends Annotation> implements DesensitizationHandler<T> {
@Override
public String desensitize(String origin, T annotation) {
Object disable = SpringExpressionUtils.parseExpression(getDisable(annotation));
if (Boolean.FALSE.equals(disable)) {
return origin;
}
// 2. 执行脱敏
int prefixKeep = getPrefixKeep(annotation);
int suffixKeep = getSuffixKeep(annotation);
String replacer = getReplacer(annotation);
int length = origin.length();
// 情况一:原始字符串长度小于等于保留长度,则原始字符串全部替换
if (prefixKeep >= length || suffixKeep >= length) {
return buildReplacerByLength(replacer, length);
}
// 情况二:原始字符串长度小于等于前后缀保留字符串长度,则原始字符串全部替换
if ((prefixKeep + suffixKeep) >= length) {
return buildReplacerByLength(replacer, length);
}
// 情况三:原始字符串长度大于前后缀保留字符串长度,则替换中间字符串
int interval = length - prefixKeep - suffixKeep;
return origin.substring(0, prefixKeep) +
buildReplacerByLength(replacer, interval) +
origin.substring(prefixKeep + interval);
}
/**
* 根据长度循环构建替换符
*
* @param replacer 替换符
* @param length 长度
* @return 构建后的替换符
*/
private String buildReplacerByLength(String replacer, int length) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
builder.append(replacer);
}
return builder.toString();
}
/**
* 前缀保留长度
*
* @param annotation 注解信息
* @return 前缀保留长度
*/
abstract Integer getPrefixKeep(T annotation);
/**
* 后缀保留长度
*
* @param annotation 注解信息
* @return 后缀保留长度
*/
abstract Integer getSuffixKeep(T annotation);
/**
* 替换符
*
* @param annotation 注解信息
* @return 替换符
*/
abstract String getReplacer(T annotation);
}
3. 身份证脱敏实现类
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();
}
}
4. 自定义脱敏注解,可配置脱敏格式
@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 "";
}
4. 测试
@Data
@Builder
public class User {
private Integer id;//主键ID
private String username;//用户名
@IdCardDesensitize
private String idCardNo; //身份证号
}
{
"id": 2,
"username": "young",
"nickname": "",
"idCardNo": "510922**********72",
}
5. 需要用到的工具类
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
</dependency>
public class SpringExpressionUtils {
/**
* Spring EL 表达式解析器
*/
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
/**
* 参数名发现器
*/
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private SpringExpressionUtils() {
}
/**
* 从切面中,单个解析 EL 表达式的结果
*
* @param joinPoint 切面点
* @param expressionString EL 表达式数组
* @return 执行界面
*/
public static Object parseExpression(JoinPoint joinPoint, String expressionString) {
Map<String, Object> result = parseExpressions(joinPoint, Collections.singletonList(expressionString));
return result.get(expressionString);
}
/**
* 从切面中,批量解析 EL 表达式的结果
*
* @param joinPoint 切面点
* @param expressionStrings EL 表达式数组
* @return 结果,key 为表达式,value 为对应值
*/
public static Map<String, Object> parseExpressions(JoinPoint joinPoint, List<String> expressionStrings) {
// 如果为空,则不进行解析
if (CollUtil.isEmpty(expressionStrings)) {
return MapUtil.newHashMap();
}
// 第一步,构建解析的上下文 EvaluationContext
// 通过 joinPoint 获取被注解方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组
String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
// Spring 的表达式上下文对象
EvaluationContext context = new StandardEvaluationContext();
// 给上下文赋值
if (ArrayUtil.isNotEmpty(paramNames)) {
Object[] args = joinPoint.getArgs();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
}
// 第二步,逐个参数解析
Map<String, Object> result = MapUtil.newHashMap(expressionStrings.size(), true);
expressionStrings.forEach(key -> {
Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context);
result.put(key, value);
});
return result;
}
/**
* 从 Bean 工厂,解析 EL 表达式的结果
*
* @param expressionString EL 表达式
* @return 执行界面
*/
public static Object parseExpression(String expressionString) {
if (StrUtil.isBlank(expressionString)) {
return null;
}
Expression expression = EXPRESSION_PARSER.parseExpression(expressionString);
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getApplicationContext()));
return expression.getValue(context);
}
}