介绍
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); }
版权声明:除特别声明外,本站所有文章皆是本站原创,转载请以超链接形式注明出处!