背景
现代网络环境中,敏感数据的处理是至关重要的。敏感数据包括个人身份信息、银行账号、手机号码等,泄露这些数据可能导致用户隐私泄露、财产损失等严重后果。因此,对敏感数据进行脱敏处理是一种必要的安全措施。
比如页面上常见的敏感数据都是加*遮挡处理过的,如下图所示。

接下来本文将以Spring Boot和MyBatis框架实现返回数据的脱敏处理。
脱敏工具
脱敏工具有很多种,本文主要介绍和使用hutool工具包提供的脱敏工具类DesensitizedUtil
,它提供了常见的手机号、身份证号、银行卡、邮箱等脱敏的方法,将敏感数据部分加*处理。
使用方法如下:
maven项目需要导入hutool包依赖,坐标如下:
| <dependency> |
| <groupId>cn.hutool</groupId> |
| <artifactId>hutool-core</artifactId> |
| <version>5.8.4</version> |
| </dependency> |
| import cn.hutool.core.util.DesensitizedUtil; |
| |
| public class SensitiveHutoolTest { |
| public static void main(String[] args) { |
| System.out.println(DesensitizedUtil.mobilePhone("13812345678")); |
| System.out.println(DesensitizedUtil.idCardNum("110101200007283706", 3, 4)); |
| System.out.println(DesensitizedUtil.bankCard("6225809637392380845")); |
| System.out.println(DesensitizedUtil.email("zhangsanfeng@test.com")); |
| } |
| } |
输出如下:
| 138****5678 |
| 110***********3706 |
| 6225 **** **** *** 0845 |
| z***********@test.com |
实现思路
- 设计一个常用的脱敏类型枚举类。
- 编写脱敏注解,包含脱敏类型,采用注解的方式对需要脱敏的字段进行标注。
- 再编写脱敏注解相应的实现逻辑使用MyBatis的拦截器功能对查出的结果集采用反射的处理方式对结果进行脱敏处理,然后将脱敏处理后的结果集再返回。
代码实现
- 定义脱敏类型枚举
利用hutool工具包,对常见的手机号、身份证号、银行卡号、邮箱进行脱敏处理。还给了一个默认的DEFAULT枚举类型按原数据返回,同时自定义了一个CUSTOM枚举,可根据实现CustomMaskService这个接口去自定义脱敏逻辑。
注意:这里的CUSTOM只是举一个简单的例子,实际情况可能需要根据实际情况扩展输入输出参数等。
| import cn.hutool.core.util.DesensitizedUtil; |
| import lombok.Getter; |
| |
| public enum SensitiveTypeEnum { |
| MOBILE("mobile", "手机号") { |
| @Override |
| public String maskSensitiveData(String data) { |
| |
| return DesensitizedUtil.mobilePhone(data); |
| } |
| }, |
| IDENTIFY("identify", "身份证号") { |
| @Override |
| public String maskSensitiveData(String data) { |
| |
| return DesensitizedUtil.idCardNum(data, 3, 4); |
| } |
| }, |
| BANKCARD("bankcard", "银行卡号") { |
| @Override |
| public String maskSensitiveData(String data) { |
| |
| return DesensitizedUtil.bankCard(data); |
| } |
| }, |
| |
| EMAIL("email", "邮箱") { |
| @Override |
| public String maskSensitiveData(String data) { |
| |
| return DesensitizedUtil.email(data); |
| } |
| }, |
| DEFAULT("default", "默认") { |
| @Override |
| public String maskSensitiveData(String data) { |
| |
| return data; |
| } |
| }, |
| CUSTOM("custom", "自定义") { |
| @Override |
| public String maskSensitiveData(String data, CustomMaskService customMaskService) { |
| |
| return customMaskService.maskData(data); |
| } |
| }; |
| |
| |
| @Getter |
| private String type; |
| |
| @Getter |
| private String desc; |
| |
| SensitiveTypeEnum(String type, String desc) { |
| this.type = type; |
| this.desc = desc; |
| } |
| |
| |
| |
| |
| |
| |
| |
| public String maskSensitiveData(String data) { |
| return data; |
| } |
| |
| public String maskSensitiveData(String data, CustomMaskService customMaskService) { |
| return null; |
| } |
| } |
- 定义一个脱敏注解
| import java.lang.annotation.*; |
| |
| @Documented |
| @Retention(RetentionPolicy.RUNTIME) |
| @Target(ElementType.FIELD) |
| public @interface SensitiveData { |
| SensitiveTypeEnum type() default SensitiveTypeEnum.DEFAULT; |
| } |
- 定义并配置一个mybatis拦截器。
| import lombok.extern.slf4j.Slf4j; |
| import org.apache.ibatis.executor.resultset.ResultSetHandler; |
| import org.apache.ibatis.plugin.Interceptor; |
| import org.apache.ibatis.plugin.Intercepts; |
| import org.apache.ibatis.plugin.Invocation; |
| import org.apache.ibatis.plugin.Signature; |
| import org.springframework.beans.factory.annotation.Autowired; |
| |
| import java.lang.reflect.Field; |
| import java.sql.Statement; |
| import java.util.List; |
| import java.util.Map; |
| |
| @Intercepts({ |
| @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) |
| }) |
| @Slf4j |
| public class SensitiveDataInterceptor implements Interceptor { |
| |
| @Autowired |
| private CustomMaskService customMaskService; |
| |
| @Override |
| public Object intercept(Invocation invocation) throws Throwable { |
| Object result = invocation.proceed(); |
| log.debug("进入数据脱敏拦截器..."); |
| if (result instanceof List) { |
| List<?> resultList = (List<?>) result; |
| for (Object obj : resultList) { |
| doSensitiveFields(obj); |
| } |
| } else if (result instanceof Map) { |
| Map<?, ?> resultMap = (Map<?, ?>) result; |
| for (Object obj : resultMap.values()) { |
| doSensitiveFields(obj); |
| } |
| } else { |
| doSensitiveFields(result); |
| } |
| return result; |
| } |
| |
| private void doSensitiveFields(Object obj) throws IllegalAccessException { |
| Field[] fields = obj.getClass().getDeclaredFields(); |
| for (Field field : fields) { |
| if (field.isAnnotationPresent(SensitiveData.class)) { |
| field.setAccessible(true); |
| Object value = field.get(obj); |
| if (value == null) { |
| continue; |
| } |
| SensitiveData sensitiveData = field.getAnnotation(SensitiveData.class); |
| SensitiveTypeEnum type = sensitiveData.type(); |
| String result; |
| if (type == SensitiveTypeEnum.CUSTOM) { |
| result = type.maskSensitiveData(value.toString(), customMaskService); |
| } else { |
| result = type.maskSensitiveData(value.toString()); |
| } |
| field.set(obj, result); |
| } |
| } |
| } |
| } |
拦截器注解里写明了要拦截的对象和方法,类:ResultSetHandler,方法:handleResultSets,它是在sql执行完成后拿到结果集并对结果集进行处理再返回。
- 配置mybatis及拦截器
| import com.star95.project.study.mybatisplus.interceptor.SensitiveDataInterceptor; |
| import org.mybatis.spring.annotation.MapperScan; |
| import org.springframework.context.annotation.Bean; |
| import org.springframework.context.annotation.Configuration; |
| |
| @Configuration |
| @MapperScan("com.star95.project.study.mybatisplus.mapper") |
| public class MyBatisPlusConfig { |
| |
| @Bean |
| public SensitiveDataInterceptor sensitiveDataInterceptor() { |
| return new SensitiveDataInterceptor(); |
| } |
| } |
| |
- 测试
| import com.star95.project.study.mybatisplus.interceptor.SensitiveData; |
| import lombok.Data; |
| |
| import static com.star95.project.study.mybatisplus.interceptor.SensitiveTypeEnum.*; |
| |
| @Data |
| public class UserDto { |
| |
| |
| |
| private Long id; |
| |
| |
| |
| @SensitiveData(type = CUSTOM) |
| private String name; |
| |
| |
| |
| private Integer age; |
| |
| |
| |
| |
| @SensitiveData(type = EMAIL) |
| private String email; |
| |
| |
| |
| |
| @SensitiveData(type = MOBILE) |
| private String mobile; |
| |
| |
| |
| |
| @SensitiveData(type = IDENTIFY) |
| private String identify; |
| |
| |
| |
| |
| @SensitiveData(type = BANKCARD) |
| private String bankcard; |
| } |
| public interface CustomMaskService { |
| String maskData(String data); |
| } |
| import cn.hutool.core.text.CharSequenceUtil; |
| import org.springframework.stereotype.Service; |
| |
| @Service |
| public class CustomMaskServiceImpl implements CustomMaskService { |
| @Override |
| public String maskData(String data) { |
| |
| return CharSequenceUtil.hide(data, 1, data.length()); |
| } |
| } |
| import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| import com.star95.project.study.mybatisplus.dto.User; |
| import com.star95.project.study.mybatisplus.dto.UserDto; |
| import org.apache.ibatis.annotations.Mapper; |
| import org.apache.ibatis.annotations.Select; |
| |
| import java.util.List; |
| |
| @Mapper |
| public interface UserMapper extends BaseMapper<User> { |
| @Select({"select * from user where id=#{id}"}) |
| UserDto getSpecialUser(String id); |
| |
| @Select({"select * from user"}) |
| List<UserDto> queryAll(); |
| } |
| import com.star95.project.study.mybatisplus.dto.UserDto; |
| import com.star95.project.study.mybatisplus.mapper.UserMapper; |
| import org.springframework.web.bind.annotation.GetMapping; |
| import org.springframework.web.bind.annotation.PathVariable; |
| import org.springframework.web.bind.annotation.RequestMapping; |
| import org.springframework.web.bind.annotation.RestController; |
| |
| import javax.annotation.Resource; |
| import java.util.List; |
| |
| @RestController |
| @RequestMapping("/sensitive") |
| public class SensitiveMybatisInterceptorTestController { |
| @Resource |
| private UserMapper userMapper; |
| |
| @GetMapping("/user/{id}") |
| public Result<UserDto> queryUserInfo(@PathVariable String id) { |
| return Result.success(userMapper.getSpecialUser(id)); |
| } |
| |
| @GetMapping("/userlist") |
| public Result<List<UserDto>> queryUserList() { |
| return Result.success(userMapper.queryAll()); |
| } |
| } |
写了两个测试接口,一个是查询单个对象,一个是返回list集合列表,访问一下:


可以看到单个结果和集合列表都做了脱敏处理,这样功能就实现完成了。
其他
使用mybatis拦截器这种方式,是在数据持久层的逻辑处理,需要注意的是,查询结果返回的dto如果是共享的情况下,可能会把不需要脱敏的数据也给处理了,影响业务逻辑,所以使用过程中要注意区分。
另外也可在接口返回数据时进行脱敏处理,也就是所说的序列化方式,比如自定义Jackson、fastjson等的序列化逻辑同样可以完成数据脱敏。
总结
通过使用MyBatis拦截器,我们可以实现对敏感数据的优雅脱敏处理,保护用户隐私和数据安全。这种方式可以灵活应用于各种场景,提供了一种简单而强大的解决方案。在实际开发中,我们可以根据具体需求,定制化开发拦截器的逻辑,以满足不同的数据脱敏需求。
数据的隐私和安全是非常重要的,敏感数据除存储方面要加密外,再展示方便也要适当的做脱敏处理,本文只介绍了mybatis拦截器实现的一种数据脱敏方式,还有很多其他技术可以实现,可以自行搜索,根据实际情况选择合适的解决方案。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步