SpringBoot实现返回值数据脱敏的步骤详解
作者:月色无痕
这篇文章主要给大家介绍一下SpringBoot实现返回值数据脱敏的步骤,文章通过代码示例介绍的非常详细,具有一定的参考价值,需要的朋友可以参考下
介绍
SpringBoot实现返回数据脱敏
有时,敏感数据返回时,需要进行隐藏处理,但是如果一个字段一个字段的进行硬编码处理的话,不仅增加了工作量,而且后期需求变动的时候,更加是地狱般的工作量变更。
下面,通过身份证,姓名,密码,手机号等等示例去演示脱敏的流程,当然你也可以在此基础上添加自己的实现方式
原理
- 项目使用的是SpringBoot,所以需要在序列化的时候,进行脱敏处理,springboot内置的序列化工具为jackson
- 通过实现com.fasterxml.jackson.databind.JsonSerializer进行自定义序列化
- 通过重写com.fasterxml.jackson.databind.ser.ContextualSerializer.createContextual获取自定义注解的信息
实现
自定义注解类
@Target(ElementType.FIELD) //作用于字段上 @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside // 表示自定义自己的注解Sensitive @JsonSerialize(using = SensitiveInfoSerialize.class) // 该注解使用序列化的方式 public @interface Sensitive { SensitizedType value(); }
创建脱敏字段类型枚举
public enum SensitizedType { /** * 用户id */ USER_ID, /** * 中文名 */ CHINESE_NAME, /** * 身份证号 */ ID_CARD, /** * 座机号 */ FIXED_PHONE, /** * 手机号 */ MOBILE_PHONE, /** * 地址 */ ADDRESS, /** * 电子邮件 */ EMAIL, /** * 密码 */ PASSWORD, /** * 中国大陆车牌,包含普通车辆、新能源车辆 */ CAR_LICENSE, /** * 银行卡 */ BANK_CARD, /** * IPv4地址 */ IPV4, /** * IPv6地址 */ IPV6, /** * 定义了一个first_mask的规则,只显示第一个字符。 */ FIRST_MASK }
脱敏工具类
import cn.hutool.core.util.CharUtil; import cn.hutool.core.util.StrUtil; /** * @Auther: wu * @Date: 2023/7/11 * @Description: com.wu.demo.common.my_sensitive */ public class SensitizedUtil { public static String desensitized(CharSequence str, SensitizedType desensitizedType) { if (StrUtil.isBlank(str)) { return StrUtil.EMPTY; } String newStr = String.valueOf(str); switch (desensitizedType) { case USER_ID: newStr = String.valueOf(userId()); break; case CHINESE_NAME: newStr = chineseName(String.valueOf(str)); break; case ID_CARD: newStr = idCardNum(String.valueOf(str), 3, 4); break; case FIXED_PHONE: newStr = fixedPhone(String.valueOf(str)); break; case MOBILE_PHONE: newStr = mobilePhone(String.valueOf(str)); break; case ADDRESS: newStr = address(String.valueOf(str), 8); break; case EMAIL: newStr = email(String.valueOf(str)); break; case PASSWORD: newStr = password(String.valueOf(str)); break; case CAR_LICENSE: newStr = carLicense(String.valueOf(str)); break; case BANK_CARD: newStr = bankCard(String.valueOf(str)); break; case IPV4: newStr = ipv4(String.valueOf(str)); break; case IPV6: newStr = ipv6(String.valueOf(str)); break; case FIRST_MASK: newStr = firstMask(String.valueOf(str)); break; default: } return newStr; } /** * 【用户id】不对外提供userId * * @return 脱敏后的主键 */ public static Long userId() { return 0L; } /** * 定义了一个first_mask的规则,只显示第一个字符。<br> * 脱敏前:123456789;脱敏后:1********。 * * @param str 字符串 * @return 脱敏后的字符串 */ public static String firstMask(String str) { if (StrUtil.isBlank(str)) { return StrUtil.EMPTY; } return StrUtil.hide(str, 1, str.length()); } /** * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李** * * @param fullName 姓名 * @return 脱敏后的姓名 */ public static String chineseName(String fullName) { return firstMask(fullName); } /** * 【身份证号】前1位 和后2位 * * @param idCardNum 身份证 * @param front 保留:前面的front位数;从1开始 * @param end 保留:后面的end位数;从1开始 * @return 脱敏后的身份证 */ public static String idCardNum(String idCardNum, int front, int end) { //身份证不能为空 if (StrUtil.isBlank(idCardNum)) { return StrUtil.EMPTY; } //需要截取的长度不能大于身份证号长度 if ((front + end) > idCardNum.length()) { return StrUtil.EMPTY; } //需要截取的不能小于0 if (front < 0 || end < 0) { return StrUtil.EMPTY; } return StrUtil.hide(idCardNum, front, idCardNum.length() - end); } /** * 【固定电话 前四位,后两位 * * @param num 固定电话 * @return 脱敏后的固定电话; */ public static String fixedPhone(String num) { if (StrUtil.isBlank(num)) { return StrUtil.EMPTY; } return StrUtil.hide(num, 4, num.length() - 2); } /** * 【手机号码】前三位,后4位,其他隐藏,比如135****2210 * * @param num 移动电话; * @return 脱敏后的移动电话; */ public static String mobilePhone(String num) { if (StrUtil.isBlank(num)) { return StrUtil.EMPTY; } return StrUtil.hide(num, 3, num.length() - 4); } /** * 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区**** * * @param address 家庭住址 * @param sensitiveSize 敏感信息长度 * @return 脱敏后的家庭地址 */ public static String address(String address, int sensitiveSize) { if (StrUtil.isBlank(address)) { return StrUtil.EMPTY; } int length = address.length(); return StrUtil.hide(address, length - sensitiveSize, length); } /** * 【电子邮箱】邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com * * @param email 邮箱 * @return 脱敏后的邮箱 */ public static String email(String email) { if (StrUtil.isBlank(email)) { return StrUtil.EMPTY; } int index = StrUtil.indexOf(email, '@'); if (index <= 1) { return email; } return StrUtil.hide(email, 1, index); } /** * 【密码】密码的全部字符都用*代替,比如:****** * * @param password 密码 * @return 脱敏后的密码 */ public static String password(String password) { if (StrUtil.isBlank(password)) { return StrUtil.EMPTY; } return StrUtil.repeat('*', password.length()); } /** * 【中国车牌】车牌中间用*代替 * eg1:null -》 "" * eg1:"" -》 "" * eg3:苏D40000 -》 苏D4***0 * eg4:陕A12345D -》 陕A1****D * eg5:京A123 -》 京A123 如果是错误的车牌,不处理 * * @param carLicense 完整的车牌号 * @return 脱敏后的车牌 */ public static String carLicense(String carLicense) { if (StrUtil.isBlank(carLicense)) { return StrUtil.EMPTY; } // 普通车牌 if (carLicense.length() == 7) { carLicense = StrUtil.hide(carLicense, 3, 6); } else if (carLicense.length() == 8) { // 新能源车牌 carLicense = StrUtil.hide(carLicense, 3, 7); } return carLicense; } /** * 银行卡号脱敏 * eg: 1101 **** **** **** 3256 * * @param bankCardNo 银行卡号 * @return 脱敏之后的银行卡号 * @since 5.6.3 */ public static String bankCard(String bankCardNo) { if (StrUtil.isBlank(bankCardNo)) { return bankCardNo; } bankCardNo = StrUtil.trim(bankCardNo); if (bankCardNo.length() < 9) { return bankCardNo; } final int length = bankCardNo.length(); final int midLength = length - 8; final StringBuilder buf = new StringBuilder(); buf.append(bankCardNo, 0, 4); for (int i = 0; i < midLength; ++i) { if (i % 4 == 0) { buf.append(CharUtil.SPACE); } buf.append('*'); } buf.append(CharUtil.SPACE).append(bankCardNo, length - 4, length); return buf.toString(); } /** * IPv4脱敏,如:脱敏前:192.0.2.1;脱敏后:192.*.*.*。 * * @param ipv4 IPv4地址 * @return 脱敏后的地址 */ public static String ipv4(String ipv4) { return StrUtil.subBefore(ipv4, '.', false) + ".*.*.*"; } /** * IPv4脱敏,如:脱敏前:2001:0db8:86a3:08d3:1319:8a2e:0370:7344;脱敏后:2001:*:*:*:*:*:*:* * * @param ipv6 IPv4地址 * @return 脱敏后的地址 */ public static String ipv6(String ipv6) { return StrUtil.subBefore(ipv6, ':', false) + ":*:*:*:*:*:*:*"; } }
上述枚举类和脱敏工具类,我使用了hutool中的代码,如果hutool满足你的需求,可以直接把上述自定义注解类和自定义序列化类使用到的SensitizedType类直接替换为hutool中的cn.hutool.core.util.DesensitizedUtil.DesensitizedType的枚举类,
添加自定义序列化实现类
public class SensitiveInfoSerialize extends JsonSerializer<String> implements ContextualSerializer { private SensitizedType sensitizedType; /** * 步骤一 * 方法来源于ContextualSerializer,获取属性上的注解属性,同时返回一个合适的序列化器 */ @Override public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException { // 获取自定义注解 Sensitive annotation = beanProperty.getAnnotation(Sensitive.class); // 注解不为空,且标注的字段为String if(Objects.nonNull(annotation) && Objects.equals(String.class, beanProperty.getType().getRawClass())){ this.sensitizedType = annotation.value(); //自定义情况,返回本序列化器,将顺利进入到该类中的serialize方法中 return this; } // 注解为空,字段不为String,寻找合适的序列化器进行处理 return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); } /** * 步骤二 * 方法来源于JsonSerializer<String>:指定返回类型为String类型,serialize()将修改后的数据返回 */ @Override public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { if(Objects.isNull(sensitizedType)){ // 定义策略为空,返回原字符串 jsonGenerator.writeString(str); }else { // 定义策略不为空,返回策略处理过的字符串 jsonGenerator.writeString(SensitizedUtil.desensitized(str,sensitizedType)); } } }
测试验证
在需要的脱敏的实体类字段上加上相应的注解
@Data public class SensitiveBody { private String name; @Sensitive(SensitizedType.MOBILE_PHONE) private String mobile; @Sensitive(SensitizedType.ID_CARD) private String idCard; }
@ApiOperation(value = "脱敏测试处理") @GetMapping("sensitiveTest") public AjaxResult sensitiveTest(){ SensitiveBody body = new SensitiveBody(); body.setMobile("13041064026"); body.setIdCard("411126189912355689"); body.setName("Tom"); return AjaxResult.success(body); }
到此这篇关于SpringBoot实现返回值数据脱敏的步骤详解的文章就介绍到这了,更多相关SpringBoot返回值数据脱敏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!