引言
数据脱敏是指对敏感信息(如身份证号、手机号、银行卡号等)进行处理,使其在展示或传输时不会泄露真实数据。常见的数据脱敏场景包括:
日志脱敏:防止敏感信息写入日志。
接口返回脱敏:在 `API` 响应中隐藏敏感信息。
数据库查询脱敏:在查询结果中隐藏敏感信息。
实现方案
通过自定义注解和 AOP
(面向切面编程),在接口返回数据时对敏感字段进行脱敏处理。注解实现防重复提交具有以下优势:
1. 代码解耦:将日志记录逻辑与业务逻辑分离,避免代码重复,提升代码可读性和可维护性。
2. 灵活性与扩展性:通过注解可以灵活地控制哪些方法需要记录日志,便于扩展和修改日志记录逻辑。
3. 非侵入性:无需修改现有业务代码,只需在方法上添加注解即可实现日志记录。
4. 集中管理:日志记录逻辑集中在切面中,便于统一管理和维护。
5. 提高开发效率:通过注解方式快速实现日志功能,减少重复代码编写。
定义注解
在 xiaomayi-common/xiaomayi-sensitive
模块中自定义脱敏注解 Sensitive
文件。
js
package com.xiaomayi.sensitive.annotation;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.xiaomayi.sensitive.enums.SensitiveMode;
import com.xiaomayi.sensitive.enums.SensitiveType;
import com.xiaomayi.sensitive.utils.DesensitizationSerializer;
import java.lang.annotation.*;
/**
* <p>
* 脱敏注解
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
// 该注解有一个ElementType数组类型的字段,主要用于标识自定义注解使用的位置
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR})
// 该元注解有一个RetentionPolicy类型的字段,主要用于标识自定义注解的生命周期
@Retention(RetentionPolicy.RUNTIME)
// 主要用于指示编译器将被注解元素的注释信息包含在生成的API文档中
@Documented
// 用来指示自定义注解是否可以被继承
@Inherited
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizationSerializer.class)
public @interface Sensitive {
/**
* 脱敏数据类型
*/
SensitiveType type() default SensitiveType.DEFAULT;
/**
* 脱敏方式,默认方式不需要定义下面脱敏长度等信息,根据脱敏的隐私数据类型自动脱敏
*/
SensitiveMode mode() default SensitiveMode.DEFAULT;
/**
* 尾部不脱敏的长度,当mode为HEAD或MIDDLE时使用
*/
int tailNoMaskLen() default 1;
/**
* 头部不脱敏的长度,当mode为TAIL或MIDDLE时使用
*/
int headNoMaskLen() default 1;
/**
* 中间不脱敏的长度,当mode为HEAD_TAIL时使用
*/
int middleNoMaskLen() default 1;
/**
* 脱敏打码符号,默认星号"*"
*/
char maskCode() default '*';
}
数据脱敏处理
js
package com.xiaomayi.sensitive.utils;
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.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.xiaomayi.core.utils.StringUtils;
import com.xiaomayi.sensitive.annotation.Sensitive;
import com.xiaomayi.sensitive.enums.SensitiveMode;
import com.xiaomayi.sensitive.enums.SensitiveType;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import java.io.IOException;
import java.util.Objects;
/**
* <p>
* 数据脱敏注解
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@AllArgsConstructor
@NoArgsConstructor
public class DesensitizationSerializer extends JsonSerializer<String> implements ContextualSerializer {
private Sensitive sensitive;
/**
* 序列化
*
* @param s 参数
* @param jsonGenerator JSON生成器
* @param serializerProvider 序列化
* @throws IOException 异常处理
*/
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(desensitize(s));
}
/**
* 集成重写方法
* 备注:此方法只会在第一次序列化字段时调用
*
* @param serializerProvider 序列化
* @param beanProperty Bean属性
* @return 返回结果
* @throws JsonMappingException 异常处理
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
// 属性判空
if (StringUtils.isNull(beanProperty)) {
return serializerProvider.findNullValueSerializer(null);
}
// 判断属性类型是否String类型
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
// 获取注解
Sensitive annotation = beanProperty.getAnnotation(Sensitive.class);
if (StringUtils.isNull(annotation)) {
// 注解为空则获取注解对象
annotation = beanProperty.getContextAnnotation(Sensitive.class);
}
// 返回序列化结果
return new DesensitizationSerializer(annotation);
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
/**
* 数据脱敏处理
*
* @param data 数据源
* @return 返回结果
*/
private String desensitize(String data) {
if (StrUtil.isNotBlank(data)) {
// 脱敏类型
SensitiveType type = sensitive.type();
// 脱敏方式
SensitiveMode mode = sensitive.mode();
switch (mode) {
case DEFAULT:
// 默认方式,根据type自动选择脱敏方式
data = autoDesensitize(data, type);
break;
case HEAD:
// 头部脱敏
data = headDesensitize(data);
break;
case TAIL:
// 尾部脱敏
data = tailDesensitize(data);
break;
case MIDDLE:
data = middleDesensitize(data);
break;
case HEAD_TAIL:
data = headTailDesensitize(data);
break;
case ALL:
data = allDesensitize(data);
break;
case NONE:
// 不做脱敏,此时不做任何处理
break;
default:
}
}
return data;
}
/**
* 全部脱敏,比如密码
*
* @param data 数据源
* @return 返回结果
*/
private String allDesensitize(String data) {
return String.valueOf(sensitive.maskCode()).repeat(data.length());
}
/**
* 头尾脱敏
*
* @param data 数据源
* @return 返回结果
*/
private String headTailDesensitize(String data) {
// 获取中间不脱敏长度
int middleNoMaskLen = sensitive.middleNoMaskLen();
// 脱敏长度MAX值校验
if (middleNoMaskLen >= data.length()) {
// 如果中间不脱敏的长度大于等于字符串的长度,不进行脱敏
return data;
}
int len = data.length() - middleNoMaskLen;
// 头部脱敏
int headStart = 0;
int headEnd = len / 2;
data = StrUtil.replace(data, headStart, headEnd, sensitive.maskCode());
// 尾部脱敏
int tailStart = data.length() - (len - len / 2);
int tailEnd = data.length();
return StrUtil.replace(data, tailStart, tailEnd, sensitive.maskCode());
}
/**
* 中间脱敏
*
* @param data 数据源
* @return 返回结果
*/
private String middleDesensitize(String data) {
// 获取头部不脱敏长度
int headNoMaskLen = sensitive.headNoMaskLen();
// 获取尾部不脱敏长度
int tailNoMaskLen = sensitive.tailNoMaskLen();
// 如果头部不脱敏的长度+尾部不脱敏长度 大于等于字符串的长度,不进行脱敏
if (headNoMaskLen + tailNoMaskLen >= data.length()) {
return data;
}
// 脱敏起始位置
int start = headNoMaskLen;
// 脱敏结束位置
int end = data.length() - tailNoMaskLen;
// 脱敏处理并返回结果
return StrUtil.replace(data, start, end, sensitive.maskCode());
}
/**
* 尾部脱敏
*
* @param data 数据源
* @return 返回结果
*/
private String tailDesensitize(String data) {
// 获取头部不脱敏长度
int headNoMaskLen = sensitive.headNoMaskLen();
// 如果头部不脱敏的长度大于等于字符串的长度,不进行脱敏
if (headNoMaskLen >= data.length()) {
return data;
}
// 脱敏起始位置
int start = headNoMaskLen;
// 脱敏结束位置
int end = data.length();
// 脱敏处理并返回结果
return StrUtil.replace(data, start, end, sensitive.maskCode());
}
/**
* 头部脱敏
*
* @param data 数据源
* @return 返回结果
*/
private String headDesensitize(String data) {
// 获取尾部不脱敏长度
int tailNoMaskLen = sensitive.tailNoMaskLen();
// 如果尾部不脱敏的长度大于等于字符串的长度,不进行脱敏
if (tailNoMaskLen >= data.length()) {
return data;
}
// 脱敏起始位置
int start = 0;
// 脱敏结束位置
int end = data.length() - tailNoMaskLen;
// 脱敏处理并返回结果
return StrUtil.replace(data, start, end, sensitive.maskCode());
}
/**
* 根据脱敏类型自动脱敏,包括但不限于:手机号、邮件、身份证号等
*
* @param data 数据源
* @param type 脱敏类型
* @return 返回结果
*/
private String autoDesensitize(String data, SensitiveType type) {
switch (type) {
case CHINESE_NAME:
data = DesensitizedUtil.chineseName(data);
break;
case FIXED_PHONE:
data = DesensitizedUtil.fixedPhone(data);
break;
case MOBILE_PHONE:
data = DesensitizedUtil.mobilePhone(data);
break;
case ADDRESS:
data = DesensitizedUtil.address(data, 8);
break;
case PASSWORD:
data = DesensitizedUtil.password(data);
break;
case BANK_CARD:
data = DesensitizedUtil.bankCard(data);
break;
case EMAIL:
data = DesensitizedUtil.email(data);
break;
case ID_CARD:
data = DesensitizedUtil.idCardNum(data, 1, 2);
break;
case DEFAULT:
// 其他类型的不支持以默认方式脱敏,直接返回
break;
default:
}
return data;
}
}
添加脱敏依赖
在 pom.xml
配置文件中引入以下依赖:
js
<!-- 数据脱敏模块 -->
<dependency>
<groupId>com.xiaomayi</groupId>
<artifactId>xiaomayi-sensitive</artifactId>
</dependency>
测试脱敏功能
在需要设置防重复提交的方法上添加以下注解:
js
/**
* 模拟密码
*/
@Sensitive(type = SensitiveType.PASSWORD)
private String password;
/**
* 模拟邮箱
*/
@Sensitive(type = SensitiveType.EMAIL)
private String email;
/**
* 模拟手机号
*/
@Sensitive(type = SensitiveType.MOBILE_PHONE)
private String phone;
/**
* 模拟座机
*/
@Sensitive(type = SensitiveType.FIXED_PHONE)
private String fixPhone;
/**
* 模拟银行卡
*/
@Sensitive(type = SensitiveType.BANK_CARD)
private String bankCard;
/**
* 模拟身份证号
*/
@Sensitive(type = SensitiveType.ID_CARD)
private String idCard;
/**
* 模拟中文名
*/
@Sensitive(type = SensitiveType.CHINESE_NAME)
private String name;
/**
* 模拟住址
*/
@Sensitive(type = SensitiveType.ADDRESS)
private String address;
/**
* 模拟自定义脱敏-头部脱敏
*/
@Sensitive(type = SensitiveType.DEFAULT, mode = SensitiveMode.HEAD, tailNoMaskLen = 4)
private String headStr;
/**
* 模拟自定义脱敏-尾部脱敏
*/
@Sensitive(type = SensitiveType.DEFAULT, mode = SensitiveMode.TAIL, headNoMaskLen = 4)
private String tailStr;
/**
* 模拟自定义脱敏-中间脱敏
*/
@Sensitive(type = SensitiveType.DEFAULT, mode = SensitiveMode.MIDDLE, headNoMaskLen = 2, tailNoMaskLen = 2)
private String middleStr;
/**
* 模拟自定义脱敏-两头脱敏,设置中间不脱敏长度
*/
@Sensitive(type = SensitiveType.DEFAULT, mode = SensitiveMode.HEAD_TAIL, middleNoMaskLen = 4)
private String headTailStr;
/**
* 模拟自定义脱敏-全部脱敏
*/
@Sensitive(type = SensitiveType.DEFAULT, mode = SensitiveMode.ALL)
private String allStr;
/**
* 模拟自定义脱敏-不脱敏
*/
@Sensitive(type = SensitiveType.DEFAULT, mode = SensitiveMode.NONE)
private String noneStr;
使用案例:
js
package com.xiaomayi.admin.controller.demo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaomayi.admin.entity.DataModel;
import com.xiaomayi.core.utils.R;
import com.xiaomayi.ratelimiter.annotation.RateLimiter;
import com.xiaomayi.system.entity.DictItem;
import com.xiaomayi.system.entity.User;
import com.xiaomayi.system.service.UserService;
import com.xiaomayi.system.utils.DictResolver;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* 测试案例 前端控制器
* 特别备注:此文件非项目本身有效文件,仅仅是工程师编写测试案例使用,留个备份未删除
* 实际项目使用时请删除此文件,以免造成其他影响
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@Slf4j
@RestController
@RequestMapping("/demo")
@AllArgsConstructor
public class DemoController {
/**
* 数据脱敏验证案例
*
* @return 返回结果
*/
@GetMapping("/sensitive")
public R sensitive() {
List<DataModel> list = new ArrayList<>();
DataModel t1 = new DataModel();
t1.setPassword("123456");
t1.setEmail("xxx@163.com");
t1.setPhone("18000000001");
t1.setFixPhone("0928-1234567");
t1.setBankCard("123456789012345");
t1.setIdCard("52000000000000000X");
t1.setName("张三");
t1.setAddress("中国北京中国北京中国北京中国北京中国北京中国北京");
t1.setHeadStr("测试头部脱敏");
t1.setTailStr("测试尾部脱敏");
t1.setMiddleStr("测试中间脱敏");
t1.setHeadTailStr("测试头尾脱敏");
t1.setAllStr("测试全部脱敏");
t1.setNoneStr("测试不脱敏");
// 加入列表
list.add(t1);
DataModel t2 = new DataModel();
t2.setPassword("123456");
t2.setEmail("xxx@163.com");
t2.setPhone("18000000001");
t2.setFixPhone("0928-1234567");
t2.setBankCard("123456789012345");
t2.setIdCard("52000000000000000X");
t2.setName("张三");
t2.setAddress("中国北京中国北京中国北京中国北京中国北京");
t2.setHeadStr("测试头部脱敏");
t2.setTailStr("测试尾部脱敏");
t2.setMiddleStr("测试中间脱敏");
t2.setHeadTailStr("测试头尾脱敏");
t2.setAllStr("测试全部脱敏");
t2.setNoneStr("测试不脱敏");
// 加入列表
list.add(t2);
return R.ok(list);
}
}
使用 ApiFox
调试工具测试结果如下图:
请求响应结果输出:
js
{
"code": 0,
"msg": "操作成功",
"data": [
{
"password": "******",
"email": "x**@163.com",
"phone": "180****0001",
"fixPhone": "0928******67",
"bankCard": "1234 **** **** 345",
"idCard": "5***************0X",
"name": "张*",
"address": "中国北京中国北京中国北京中国北京********",
"headStr": "**头部脱敏",
"tailStr": "测试尾部**",
"middleStr": "测试**脱敏",
"headTailStr": "*试头尾脱*",
"allStr": "******",
"noneStr": "测试不脱敏"
},
{
"password": "******",
"email": "x**@163.com",
"phone": "180****0001",
"fixPhone": "0928******67",
"bankCard": "1234 **** **** 345",
"idCard": "5***************0X",
"name": "张*",
"address": "中国北京中国北京中国北京********",
"headStr": "**头部脱敏",
"tailStr": "测试尾部**",
"middleStr": "测试**脱敏",
"headTailStr": "*试头尾脱*",
"allStr": "******",
"noneStr": "测试不脱敏"
}
],
"ok": true
}
总结
根据具体场景选择合适的方案,可以有效保护敏感数据,提升系统的安全性。