SpringBoot使用自定义注解实现数据脱敏过程详细解析
码农-文若书生 人气:0前言
对于某些接口返回的信息,涉及到敏感数据的必须进行脱敏操作,例如银行卡号、SFZ号、手机号等,脱敏方式有多种方式。可以修改SQL语句,也可以写硬代码,也可以修改JSON序列化,这里介绍通过修改Jackson序列化方式实现数据脱敏。
一、引入hutool工具类
maven:
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.5</version> </dependency>
gradle:
// https://mvnrepository.com/artifact/cn.hutool/hutool-all
implementation group: 'cn.hutool', name: 'hutool-all', version: '5.8.5'
二、定义常用需要脱敏的数据类型的枚举
其中 OTHER类型为自定义类型,需在后面自定义脱敏的长度等。
package com.iscas.authentication.model.enums; import lombok.Getter; /** * * @version 1.0 * @since jdk1.8 */ @Getter public enum PrivacyTypeEnum { /** * 中文名 * */ CHINESE_NAME, /** * 固话 * */ FIXED_PHONE, /** * 手机号 * */ MOBILE_PHONE, /** * 住址 * */ ADDRESS, /** * 密码 * */ PASSWORD, /** * 银行卡号 * */ BANK_CARD, /** * 邮箱 * */ EMAIL, /** * SFZ * */ ID_CARD, /** * 其他类型 * */ OTHER; }
三、定义脱敏方式枚举
其中,DEFAULT类型时,需要数据类型为上一步枚举中除OTHER外的已确定的类型,NONE表示不做脱敏,其他类型为注释的意思。
package com.iscas.authentication.model.enums; /** * * @version 1.0 * @since jdk1.8 */ public enum DesensitizationTypeEnum { /** * 默认方式 * */ DEFAULT, /** * 头部脱敏 * */ HEAD, /** * 尾部脱敏 * */ TAIL, /** * 中间脱敏 * */ MIDDLE, /** * 头尾脱敏 * */ HEAD_TAIL, /** * 全部脱敏 * */ ALL, /** * 不脱敏,相当于没打这个注解 * */ NONE; }
四、自定义脱敏的注解
其中,mode默认为DEFAULT,此时只需要设置dataType的类型为除OTHER外的确定类型即可,当mode不是DEFAULT或NONE时,根据不同的类型,headNoMaskLen等长度属性需要设置,见上面的注释的字面意思。
package com.iscas.authentication.annotation; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.iscas.authentication.model.enums.DesensitizationTypeEnum; import com.iscas.authentication.model.enums.PrivacyTypeEnum; import com.iscas.authentication.service.DesensitizationSerializer; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 脱敏注解 * * @version 1.0 * @since jdk1.8 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @JsonSerialize(using = DesensitizationSerializer.class) public @interface Desensitization { /** * 脱敏的隐私数据类型 */ PrivacyTypeEnum dataType(); /** * 脱敏方式,默认方式不需要定义下面脱敏长度等信息,根据脱敏的隐私数据类型自动脱敏 */ DesensitizationTypeEnum mode() default DesensitizationTypeEnum.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 '*'; }
五、自定义Jackson的序列化方式
package com.iscas.authentication.service; 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.iscas.authentication.annotation.Desensitization; import com.iscas.authentication.model.enums.DesensitizationTypeEnum; import com.iscas.authentication.model.enums.PrivacyTypeEnum; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import java.io.IOException; import java.util.Objects; /** * 脱敏序列化类 * * @author zhuquanwen * @version 1.0 * @date 2023/1/5 9:24 * @since jdk1.8 */ @AllArgsConstructor @NoArgsConstructor public class DesensitizationSerializer extends JsonSerializer<String> implements ContextualSerializer { private Desensitization desensitization; @Override public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeString(desensitize(s)); } @Override public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException { if (beanProperty != null) { if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) { Desensitization desensitization = beanProperty.getAnnotation(Desensitization.class); if (desensitization == null) { desensitization = beanProperty.getContextAnnotation(Desensitization.class); } if (desensitization != null) { return new DesensitizationSerializer(desensitization); } } return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); } return serializerProvider.findNullValueSerializer(null); } /** * 脱敏处理 * */ private String desensitize(String s) { if (StrUtil.isNotBlank(s)) { PrivacyTypeEnum dataType = desensitization.dataType(); DesensitizationTypeEnum mode = desensitization.mode(); switch (mode) { case DEFAULT: // 默认方式,根据dataType自动选择脱敏方式 s = autoDesensitize(s, dataType); break; case HEAD: // 头部脱敏 s = headDesensitize(s); break; case TAIL: // 尾部脱敏 s = tailDesensitize(s); break; case MIDDLE: s = middleDesensitize(s); break; case HEAD_TAIL: s = headTailDesensitize(s); break; case ALL: s = allDesensitize(s); break; case NONE: // 不做脱敏 break; default: } } return s; } /** * 全部脱敏 * */ private String allDesensitize(String s) { return String.valueOf(desensitization.maskCode()).repeat(s.length()); } /** * 头尾脱敏 * */ private String headTailDesensitize(String s) { int middleNoMaskLen = desensitization.middleNoMaskLen(); if (middleNoMaskLen >= s.length()) { // 如果中间不脱敏的长度大于等于字符串的长度,不进行脱敏 return s; } int len = s.length() - middleNoMaskLen; // 头部脱敏 int headStart = 0; int headEnd = len / 2; s = StrUtil.replace(s, headStart, headEnd, desensitization.maskCode()); // 尾部脱敏 int tailStart = s.length() - (len - len / 2); int tailEnd = s.length(); return StrUtil.replace(s, tailStart, tailEnd, desensitization.maskCode()); } /** * 中间脱敏 * */ private String middleDesensitize(String s) { int headNoMaskLen = desensitization.headNoMaskLen(); int tailNoMaskLen = desensitization.tailNoMaskLen(); if (headNoMaskLen + tailNoMaskLen >= s.length()) { // 如果头部不脱敏的长度+尾部不脱敏长度 大于等于字符串的长度,不进行脱敏 return s; } int start = headNoMaskLen; int end = s.length() - tailNoMaskLen; return StrUtil.replace(s, start, end, desensitization.maskCode()); } /** * 尾部脱敏 * */ private String tailDesensitize(String s) { int headNoMaskLen = desensitization.headNoMaskLen(); if (headNoMaskLen >= s.length()) { // 如果头部不脱敏的长度大于等于字符串的长度,不进行脱敏 return s; } int start = headNoMaskLen; int end = s.length(); return StrUtil.replace(s, start, end, desensitization.maskCode()); } /** * 头部脱敏 * */ private String headDesensitize(String s) { int tailNoMaskLen = desensitization.tailNoMaskLen(); if (tailNoMaskLen >= s.length()) { // 如果尾部不脱敏的长度大于等于字符串的长度,不进行脱敏 return s; } int start = 0; int end = s.length() - tailNoMaskLen; return StrUtil.replace(s, start, end, desensitization.maskCode()); } public static void main(String[] args) { System.out.println(StrUtil.replace("231085198901091813", 2, -10, '#')); } /** * 根据数据类型自动脱敏 * */ private String autoDesensitize(String s, PrivacyTypeEnum dataType) { switch (dataType) { case CHINESE_NAME: s = DesensitizedUtil.chineseName(s); break; case FIXED_PHONE: s = DesensitizedUtil.fixedPhone(s); break; case MOBILE_PHONE: s = DesensitizedUtil.mobilePhone(s); break; case ADDRESS: s = DesensitizedUtil.address(s, 8); break; case PASSWORD: s = DesensitizedUtil.password(s); break; case BANK_CARD: s = DesensitizedUtil.bankCard(s); break; case EMAIL: s = DesensitizedUtil.email(s); break; case ID_CARD: s = DesensitizedUtil.idCardNum(s, 1, 2); break; case OTHER: // 其他类型的不支持以默认方式脱敏,直接返回 break; default: } return s; } }
六、使用
下面是一个测试的例子:
package com.iscas.base.biz.test.controller; import com.iscas.base.biz.desensitization.Desensitization; import com.iscas.base.biz.desensitization.DesensitizationTypeEnum; import com.iscas.base.biz.desensitization.PrivacyTypeEnum; import lombok.Data; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; /** * * @author zhuquanwen * @version 1.0 * @date 2023/1/6 8:40 * @since jdk1.8 */ @RestController @RequestMapping("/test/desensitization") public class TestDesensitizationController { @GetMapping public List<TestModel> test() { TestModel t1 = new TestModel(); t1.setPassword("123456"); t1.setEmail("zzz@163.com"); t1.setPhone("137654879451"); t1.setFixPhone("0453-4785462"); t1.setBankCard("622648754896457"); t1.setIdCard("245874563214578965"); t1.setName("张王钊"); t1.setAddress("北京市昌平区xxx街道xxx小区1-1-101"); t1.setHeadStr("测试头部脱敏"); t1.setTailStr("测试尾部脱敏"); t1.setMiddleStr("测试中间脱敏"); t1.setHeadTailStr("测试头尾脱敏"); t1.setAllStr("测试全部脱敏"); t1.setNoneStr("测试不脱敏"); TestModel t2 = new TestModel(); t2.setPassword("iscas123"); t2.setEmail("xwg@sina.com"); t2.setPhone("18547896547"); t2.setFixPhone("010-62268795"); t2.setBankCard("622648754896487"); t2.setIdCard("100412547865478947"); t2.setName("李二麻子"); t2.setAddress("新疆省克拉玛依市xxx街道xxx小区1-1-101"); t2.setHeadStr("测试头部脱敏"); t2.setTailStr("测试尾部脱敏"); t2.setMiddleStr("测试中间脱敏"); t2.setHeadTailStr("测试头尾脱敏"); t2.setAllStr("测试全部脱敏"); t2.setNoneStr("测试不脱敏"); return new ArrayList<>(){{ add(t1); add(t2); }}; } @Data private static class TestModel { /** * 模拟密码 * */ @Desensitization(dataType = PrivacyTypeEnum.PASSWORD) private String password; /** * 模拟邮箱 * */ @Desensitization(dataType = PrivacyTypeEnum.EMAIL) private String email; /** * 模拟手机号 * */ @Desensitization(dataType = PrivacyTypeEnum.MOBILE_PHONE) private String phone; /** * 模拟座机 * */ @Desensitization(dataType = PrivacyTypeEnum.FIXED_PHONE) private String fixPhone; /** * 模拟银行卡 * */ @Desensitization(dataType = PrivacyTypeEnum.BANK_CARD) private String bankCard; /** * 模拟SFZ号 * */ @Desensitization(dataType = PrivacyTypeEnum.ID_CARD) private String idCard; /** * 模拟中文名 * */ @Desensitization(dataType = PrivacyTypeEnum.CHINESE_NAME) private String name; /** * 模拟住址 * */ @Desensitization(dataType = PrivacyTypeEnum.ADDRESS) private String address; /** * 模拟自定义脱敏-头部脱敏 * */ @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.HEAD, tailNoMaskLen = 4) private String headStr; /** * 模拟自定义脱敏-尾部脱敏 * */ @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.TAIL, headNoMaskLen = 4) private String tailStr; /** * 模拟自定义脱敏-中间脱敏 * */ @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.MIDDLE, headNoMaskLen = 2, tailNoMaskLen = 2) private String middleStr; /** * 模拟自定义脱敏-两头脱敏 * */ @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.HEAD_TAIL, middleNoMaskLen = 4) private String headTailStr; /** * 模拟自定义脱敏-全部脱敏 * */ @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.ALL) private String allStr; /** * 模拟自定义脱敏-不脱敏 * */ @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.NONE) private String noneStr; } }
下面是一个实际使用的例子如下,在tel、password、email上添加了@Desensitization注解,自定义的@TbField等注解请忽略
package com.iscas.authentication.model.sys; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.iscas.authentication.annotation.Desensitization; import com.iscas.authentication.model.enums.PrivacyTypeEnum; import com.iscas.templet.annotation.table.TbField; import com.iscas.templet.annotation.table.TbFieldRule; import com.iscas.templet.annotation.table.TbSetting; import com.iscas.templet.view.table.TableFieldType; import com.iscas.templet.view.table.TableSearchType; import com.iscas.templet.view.table.TableViewType; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.experimental.Accessors; import java.util.List; /** * @author zhuquanwen * @version 1.0 * @date 2022/3/11 21:23 * @since jdk11 */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Schema(title = "用户") @TableName(value = "oauth_sys_user") @Accessors(chain = true) @TbSetting(title = "用户", checkbox = true, viewType = TableViewType.multi) public class User extends BaseEntity { @TableId(type = IdType.AUTO) @Schema(title = "id") @TbField(field = "id", header = "id", type = TableFieldType.text, hidden = true) private Integer id; @Schema(title = "用户名") @TbField(field = "name", header = "名称", search = true, searchType = TableSearchType.like, type = TableFieldType.text, rule=@TbFieldRule(required = true, minLength = 2, maxLength = 20, distinct = true, desc = "用户名不能为空,且长度介于2-20个字符之间")) private String name; @Schema(title = "密码") @TbField(field = "password", header = "密码", hidden = true, editable = false, type = TableFieldType.text) @Desensitization(dataType = PrivacyTypeEnum.PASSWORD) private String password; @Schema(title = "type") @TbField(field = "type", header = "用户类型", search = true, searchType = TableSearchType.exact, type = TableFieldType.select, option = "[{\"label\":\"正常用户\",\"value\":\"1\"},{\"label\":\"战位IP用户\",\"value\":\"2\"}]") private String type; @Schema(title = "status") @TbField(field = "status", header = "状态", search = true, searchType = TableSearchType.exact, type = TableFieldType.select, option = "[{\"label\":\"正常\",\"value\":\"1\"},{\"label\":\"禁用\",\"value\":\"0\"}]") private String status; @Schema(title = "真实姓名") @TbField(field = "realName", header = "真实姓名", type = TableFieldType.text, rule=@TbFieldRule(required = true, minLength = 2, maxLength = 20, desc = "真实姓名不能为空,且长度介于2-20个字符之间")) private String realName; @Schema(title = "电话号码") @TbField(field = "tel", header = "电话号码", type = TableFieldType.text, rule=@TbFieldRule(reg = "^(13[0-9]|14[01456879]|15[0-3,5-9]|16[2567]|17[0-8]|18[0-9]|19[0-3,5-9])\\d{8}$", desc = "电话号码需符规则")) @Desensitization(dataType = PrivacyTypeEnum.MOBILE_PHONE) private String tel; @Schema(title = "邮箱") @TbField(field = "email", header = "邮箱", type = TableFieldType.text, rule=@TbFieldRule(reg = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*.\\w+([-.]\\w+)*$", desc = "邮箱需符规则")) @Desensitization(dataType = PrivacyTypeEnum.EMAIL) private String email; @Schema(title = "部门") @TbField(field = "orgIds", header = "部门", type = TableFieldType.multiSelect, selectUrl = "/api/v1/orgs/combobox/tree?status=1") @TableField(exist = false) private List<Integer> orgIds; @Schema(title = "角色") @TbField(field = "roleIds", header = "角色", type = TableFieldType.multiSelect, selectUrl = "/api/v1/roles/combobox?status=1") @TableField(exist = false) private List<Integer> roleIds; @Schema(title = "岗位") @TbField(field = "postIds", header = "岗位", type = TableFieldType.multiSelect, selectUrl = "/api/v1/posts/combobox?status=1") @TableField(exist = false) private List<Integer> postIds; }
七、脱敏效果
下面是测试的结果:
下面是一个查询接口返回带User实体的结果:
加载全部内容