包阅导读总结
1. 关键词:数据脱敏、Jackson、互联网项目、敏感信息、隐私安全
2. 总结:
本文围绕互联网项目的数据脱敏需求展开,介绍了数据脱敏的概念和常见应用场景,指出传统脱敏代码的缺点,重点阐述了基于Jackson的优雅脱敏方法,包括使用方法、实现步骤及相关代码示例。
3. 主要内容:
– 引言
– 介绍数据脱敏的概念及常见技术手段
– 说明数据脱敏的应用行业和作用
– 哪些数据需要脱敏
– 个人身份信息
– 金融信息
– 医疗信息
– 登录凭证和认证信息
– 设备和网络信息
– 行为和偏好数据
– 通信内容
– 地理位置数据
– 工作和教育信息
– 客户服务数据
– 传统脱敏代码
– 自己写代码
– 使用第三方工具如 Hutool
– 指出这些方法的缺点
– 基于 Jackson 优雅脱敏
– 使用方法:添加注解指定脱敏策略,可自定义脱敏方法
– 实现步骤
– 定义注解
– 定义接口
– 默认实现
– 定义序列化器
– 定义默认脱敏方法
思维导图:
文章地址:https://juejin.cn/post/7401042923490476032
文章来源:juejin.cn
作者:赵侠客
发布时间:2024/8/11 3:09
语言:中文
总字数:5163字
预计阅读时间:21分钟
评分:84分
标签:数据脱敏,数据安全,Jackson,代码实现,隐私保护
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
首发公众号:【赵侠客】
引言
数据脱敏是一种通过对敏感信息进行处理,以保护数据隐私和安全的技术手段。它通过对数据进行伪装、加密、匿名化或模糊处理,使得未经授权的人员无法识别或还原原始数据,从而防止数据泄露和滥用。常见的脱敏技术包括替换、掩码、随机化和数据混淆等。数据脱敏广泛应用于金融、医疗、互联网等行业,在数据测试、开发和共享过程中,确保了数据的安全性和隐私性,同时又不影响数据的整体分析和使用价值
二、哪些数据需要脱敏
个人身份信息(PII):
- 姓名:包括用户的真实姓名、昵称等。如:赵侠客 脱敏为 赵*客
- 身份证号:如社会保障号码、身份证号码等。如:342822199202230227 脱敏为 ***************0227
- 出生日期:完整的生日信息。如:将”1990-05-15″脱敏为”1990-0*-1*”
- 地址:家庭住址、邮寄地址等。如:“北京市海淀区中关村大街27号”脱敏为“北京市海淀区街号
- 联系方式:电话号码、电子邮件地址等。如:18283741313 脱敏为:182****1313
金融信息:
- 信用卡信息:信用卡号、有效期、安全码等。
- 银行账户信息:银行账户号码、路由号码等。
- 交易记录:包括交易时间、交易金额、交易对象等。
医疗信息:
- 医疗记录:包括病史、诊断信息、治疗记录等。
- 保险信息:健康保险号码、保险公司信息等。
登录凭证和认证信息:
- 用户名和密码:账户登录名、密码、加密后的密码等。
- 多因素认证信息:包括安全问题答案、二次认证代码等。
设备和网络信息:
- IP地址:用户的公网IP地址和内网IP地址。
- MAC地址:设备的物理地址。
- 设备标识符:如设备ID、序列号等。
行为和偏好数据:
- 浏览历史:用户访问的网站、点击记录等。
- 搜索记录:用户的搜索查询记录。
- 购买历史:用户在电商平台上的购买记录、购物车内容等。
通信内容:
- 电子邮件:邮件内容、附件等。
- 聊天记录:即时通讯工具中的聊天记录、消息内容等。
- 社交媒体信息:用户在社交媒体上的发言、评论、私信等。
地理位置数据:
- GPS数据:设备的精确地理位置。
- 位置历史:用户过去的位置信息记录。
工作和教育信息:
- 工作记录:工作单位、职位、薪资等。
- 教育记录:学校、学历、学位等。
客户服务数据:
三、传统脱敏代码
3.1 自己写代码
我想大部公司都有自己的一套工具类,自己写代码脱敏可能就是自己写一个工具类,然后放到公共包中,其它项目需要使用引用这个公共包就可以调用,如以下:
public class StrUtil {public static String maskPhoneNumber(String phoneNumber) { if (phoneNumber == null || phoneNumber.length() != 11) { throw new IllegalArgumentException("手机号码格式不正确"); } return phoneNumber.substring(0, 3) + "****" + phoneNumber.substring(7);}public static void main(String[] args) { String phoneNumber = "13812345678"; String maskedPhoneNumber = maskPhoneNumber(phoneNumber); System.out.println("原始手机号码: " + phoneNumber); System.out.println("脱敏手机号码: " + maskedPhoneNumber);}}
3.2 使用第三方工具如:Hutool
很多第三方工具类都提供了字段脱敏的工具类,如Hutool,我们可以很方便的调用DesensitizedUtil.desensitized来完成数据脱敏,如以下代码
D.d("admin", DT.USER_ID);D.d("赵侠客", DT.CHINESE_NAME);D.d("323455100993934554", DT.ID_CARD);D.d("0571-8331223", DT.FIXED_PHONE);D.d("18034232232",DT.MOBILE_PHON);D.d("浙江省杭州市西湖区西湖大道1号", DT.ADDRESS);D.d("zhaoxiake@163.com", DT.EMAIL);D.d("123456789", DT.PASSWORD);D.d("浙A888888", DT.CAR_LICENSE);D.d("62122612028837228932", DT.BANK_CARD);D.d("10.100.12.12", DesensitDTizedType.IPV4);D.d("2001:0db8:85a3:0000:0000:8a2e:0370:7334", DT.IPV6);D.d("1231231", DT.FIRST_MASK);
输出:
0 赵** 3***************54 3234************54 057******2234 浙江省杭州市西******** z********@163.com ********* 浙A8****8 6212 **** **** **** 8932 10.*.*.* 2001:*:*:*:*:*:*:* 1******
3.3 这些方法的缺点
我觉得这些方法有以下缺点:
四、基于Jackson优雅脱敏
4.1 使用方法
首先我们看一下如何使用,看看是不是很优雅,添加@FieldAnonymize注解,指定脱敏策略,如果想自定义增加自己的脱敏方法如:testStrategy,就不需要写其它代码了
@Datapublic class SentiveUser {private Long id;@FieldAnonymize("testStrategy")private String username;@FieldAnonymize(AnonymizeType.mobile)private String mobile;@FieldAnonymize(AnonymizeType.email)private String email;@FieldAnonymize(AnonymizeType.chineseName)private String chineseName;@FieldAnonymize(AnonymizeType.idCard)private String idCard;@FieldAnonymize(AnonymizeType.phone)private String phone;@FieldAnonymize(AnonymizeType.address)private String address;@FieldAnonymize(AnonymizeType.bankCard)private String bankCard;@FieldAnonymize(AnonymizeType.password)private String password;@FieldAnonymize(AnonymizeType.carNumber)private String carNumber;}
使用方法:
@Testpublic void testAnonymize() { //自定义脱敏策略AnonymizeImpl strategy= new AnonymizeImpl().addStrategy("testStrategy", t -> t + "***test***")AnonymizeSerializer.setAnonymizeStrategy(strategy)SentiveUser sentiveUser=new SentiveUser()sentiveUser.setMobile("0571-85312234")sentiveUser.setUsername("admin")sentiveUser.setEmail("zhaoxiake@163.com")sentiveUser.setChineseName("赵侠客")sentiveUser.setAddress("浙江省杭州市西湖区西湖大道1号")sentiveUser.setPhone("180723432123")sentiveUser.setIdCard("323455100993934554")sentiveUser.setBankCard("62122612028837228932")sentiveUser.setCarNumber("浙AA1126")sentiveUser.setPassword("Aa123456")String json=JsonUtils.toJson(sentiveUser)System.out.println(json) }
这样我们所有JSON序列化输出结果都脱敏了,包括使用SpringBoot开发接口返回的JSON数据 ,前提是使用了Jackson做JSON数据序列化:
{ "username": "admin***test***", "mobile": "057********34", "email": "zha******@163.com", "chineseName": "赵**", "idCard": "**************4554", "phone": "********2123", "address": "浙江省杭州市西********", "bankCard": "621226**********8932", "password": "********", "carNumber": "浙A****6"}
4.2 实现步骤
定义注解:
@Retention(RetentionPolicy.RUNTIME)@JacksonAnnotationsInside@JsonSerialize(using = AnonymizeSerializer.class)public @interface FieldAnonymize { String value();}
定义接口:
public interface AnonymizeType { String chineseName = "chineseName" String idCard = "idCard" String phone = "phone" String mobile = "mobile" String address = "address" String email = "email" String bankCard = "bankCard" String password = "password" String carNumber = "carNumber"}public interface IAnonymize { default String handle(String type, String value) { return ((Function<String, String>)getStrategyFunctionMap().get(type)).apply(value) } Map<String, Function<String, String>> getStrategyFunctionMap() } public interface IJacksonSerializer extends ContextualSerializer { default JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property) throws JsonMappingException { if (null != property) { return getJsonSerializer(provider, property) } return provider.findNullValueSerializer(null) } default <A extends Annotation> A getAnnotation(BeanProperty property, Class<A> clazz) { Annotation annotation = property.getAnnotation(clazz) if (null == annotation) { annotation = property.getContextAnnotation(clazz) } return (A) annotation } JsonSerializer<?> getJsonSerializer(SerializerProvider paramSerializerProvider, BeanProperty paramBeanProperty) throws JsonMappingException}
默认实现:
public class AnonymizeImpl implements IAnonymize { private static Map<String, Function<String, String>> STRATEGY_FUNCTION_MAP; public AnonymizeImpl() { STRATEGY_FUNCTION_MAP = new HashMap<>(); STRATEGY_FUNCTION_MAP.put(AnonymizeType.chineseName, DefaultAnonymize::chineseName); STRATEGY_FUNCTION_MAP.put(AnonymizeType.idCard, DefaultAnonymize::idCard); STRATEGY_FUNCTION_MAP.put(AnonymizeType.phone, DefaultAnonymize::phone); STRATEGY_FUNCTION_MAP.put(AnonymizeType.mobile, DefaultAnonymize::mobile); STRATEGY_FUNCTION_MAP.put(AnonymizeType.address, DefaultAnonymize::address); STRATEGY_FUNCTION_MAP.put(AnonymizeType.email, DefaultAnonymize::email); STRATEGY_FUNCTION_MAP.put(AnonymizeType.bankCard, DefaultAnonymize::bankCard); STRATEGY_FUNCTION_MAP.put(AnonymizeType.password, DefaultAnonymize::password); STRATEGY_FUNCTION_MAP.put(AnonymizeType.carNumber, DefaultAnonymize::carNumber); } public Map<String, Function<String, String>> getStrategyFunctionMap() { return STRATEGY_FUNCTION_MAP; } public AnonymizeImpl addStrategy(String paramString, Function<String, String> paramFunction) { STRATEGY_FUNCTION_MAP.put(paramString, paramFunction); return this; }}
定义序列化器:
public class AnonymizeSerializer extends JsonSerializer<String> implements IJacksonSerializer { private static IAnonymize ANONYMIZE_STRATEGY; private String type; public AnonymizeSerializer() { } public AnonymizeSerializer(String type) { this.type = type; } public static void setAnonymizeStrategy(IAnonymize anonymizeStrategy) { ANONYMIZE_STRATEGY = anonymizeStrategy; } public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (null == ANONYMIZE_STRATEGY) { throw new RuntimeException("You used the annotation `@FieldAnonymize` but did not inject `AnonymizeStrategy`"); } Object fieldValue = ANONYMIZE_STRATEGY.handle(this.type, value); gen.writeObject(fieldValue); } public JsonSerializer<?> getJsonSerializer(SerializerProvider provider, BeanProperty property) throws JsonMappingException { if (Objects.equals(property.getType().getRawClass(), String.class)) { FieldAnonymize anonymizeInfo = getAnnotation(property, FieldAnonymize.class); if (null != anonymizeInfo) { return new AnonymizeSerializer(anonymizeInfo.value()); } } return provider.findValueSerializer(property.getType(), property); }}
定义默认脱敏方法:
public class DefaultAnonymize { public static String chineseName(String originalChineseName) { return processString(originalChineseName, x -> StrUtil.concatStr(StrUtil.subStrFromHead(originalChineseName, 1), StrUtil.nullSafeLength(originalChineseName), "*")); } public static String idCard(String originalIdCard) { return processString(originalIdCard, x -> StrUtil.maskStart(StrUtil.subStrFromLast(originalIdCard, 4), StrUtil.nullSafeLength(originalIdCard), "*")); } public static String phone(String originalPhoneNumber) { return processString(originalPhoneNumber, x -> StrUtil.maskStart(StrUtil.subStrFromLast(originalPhoneNumber, 4), StrUtil.nullSafeLength(originalPhoneNumber), "*")); } public static String mobile(String originalMobileNumber) { return processString(originalMobileNumber, x -> StrUtil.subStrFromHead(originalMobileNumber, 3).concat(StrUtil.removeFromHeader(StrUtil.maskStart(StrUtil.subStrFromLast(originalMobileNumber, 2), StrUtil.nullSafeLength(originalMobileNumber), "*"), "***"))); } public static String address(String originalAddress) { return processString(originalAddress, paramString1 -> { int i = StrUtil.nullSafeLength(originalAddress); return (i <= 8) ? originalAddress : StrUtil.concatStr(StrUtil.subStrFromHead(originalAddress, i - 8), i, "*"); }); } public static String email(String originalEmail) { return processString(originalEmail, x -> { int i = StrUtil.findIndex(originalEmail, "@"); byte b = 1; if (i > 5) b = 3; return (1 == i) ? originalEmail : StrUtil.concatStr(StrUtil.subStrFromHead(originalEmail, b), i, "*").concat(StrUtil.subStrFromIndex(originalEmail, i, StrUtil.nullSafeLength(originalEmail))); }); } public static String bankCard(String originalBankCardNumber) { return processString(originalBankCardNumber, x -> StrUtil.subStrFromHead(originalBankCardNumber, 6).concat(StrUtil.removeFromHeader(StrUtil.maskStart(StrUtil.subStrFromLast(originalBankCardNumber, 4), StrUtil.nullSafeLength(originalBankCardNumber), "*"), "******"))); } public static String password(String originalPassword) { return processString(originalPassword, x -> StrUtil.concatStr(StrUtil.subStrFromHead(originalPassword, 0), StrUtil.nullSafeLength(originalPassword), "*")); } public static String carNumber(String originalCarNumber) { return processString(originalCarNumber, x -> StrUtil.subStrFromHead(originalCarNumber, 2).concat(StrUtil.removeFromHeader(StrUtil.maskStart(StrUtil.subStrFromLast(originalCarNumber, 1), StrUtil.nullSafeLength(originalCarNumber), "*"), "**"))); } private static String processString(String originalString, Function<String, String> anonymizeFunction) { return StrUtil.isBlank(originalString)? null : anonymizeFunction.apply(originalString); }}
总结
本文针对互联网项目常见的数据脱敏需求,提供了一种基于Jackson优雅、通用、灵活的的数据脱敏方法,主要有以下优点:
- 使用优雅,只需要添加一个注解
- 和SpringBoot框架无缝对接,实体增加注解后所有接口JSON数据自动脱敏
- 代码通用,所有人都可以使用该方法
- 策略灵活,可以自定义策略
符StrUtil源码:
public class StrUtil { public static String maskStart(String originalString, int targetLength, Object fillElement) { if (originalString == null) { return null; } int originalStringLength = originalString.length(); int paddingLength = targetLength - originalStringLength; if (paddingLength <= 0) { return originalString; } if (fillElement instanceof String) { String fillString = (String) fillElement; if (isBlank(fillString)) { fillString = " "; } int fillStringLength = fillString.length(); if (fillStringLength == 1 && paddingLength <= 8192) { return maskStart(originalString, targetLength, fillString.charAt(0)); } if (paddingLength == fillStringLength) { return fillString.concat(originalString); } if (paddingLength < fillStringLength) { return fillString.substring(0, paddingLength).concat(originalString); } char[] paddingChars = new char[paddingLength]; char[] fillStringChars = fillString.toCharArray(); for (byte b = 0; b < paddingLength; b++) { paddingChars[b] = fillStringChars[b % fillStringLength]; } return new String(paddingChars).concat(originalString); } else if (fillElement instanceof Character) { char fillChar = (char) fillElement; return paddingLength <= 0 ? originalString : (paddingLength > 8192 ? maskStart(originalString, targetLength, String.valueOf(fillChar)) : repeatChar(fillChar, paddingLength).concat(originalString)); } return originalString; } public static String concatStr(String firstString, int totalLength, Object secondElement) { if (firstString == null) { return null; } int firstStringLength = firstString.length(); int paddingLength = totalLength - firstStringLength; if (paddingLength <= 0) { return firstString; } if (secondElement instanceof String) { String secondString = (String) secondElement; if (isBlank(secondString)) { secondString = " "; } int secondStringLength = secondString.length(); if (secondStringLength == 1 && paddingLength <= 8192) { return concatStr(firstString, totalLength, secondString.charAt(0)); } if (paddingLength == secondStringLength) { return firstString.concat(secondString); } if (paddingLength < secondStringLength) { return firstString.concat(secondString.substring(0, paddingLength)); } char[] paddingChars = new char[paddingLength]; char[] secondStringChars = secondString.toCharArray(); for (byte b = 0; b < paddingLength; b++) { paddingChars[b] = secondStringChars[b % secondStringLength]; } return firstString.concat(new String(paddingChars)); } else if (secondElement instanceof Character) { char secondChar = (char) secondElement; return paddingLength <= 0 ? firstString : (paddingLength > 8192 ? concatStr(firstString, totalLength, String.valueOf(secondChar)) : firstString.concat(repeatChar(secondChar, paddingLength))); } return firstString; } private static String repeatChar(char charToRepeat, int repeatCount) { if (repeatCount <= 0) { return ""; } char[] repeatedChars = new char[repeatCount]; for (int i = repeatCount - 1; i >= 0; i--) { repeatedChars[i] = charToRepeat; } return new String(repeatedChars); } public static int findIndex(String sourceString, String subString) { if (isBlank(sourceString)) { return -1; } return sourceString.indexOf(subString); } public static boolean isBlank(CharSequence charSequence) { return charSequence == null || charSequence.length() == 0; } public static int nullSafeLength(CharSequence charSequence) { return charSequence == null ? 0 : charSequence.length(); } public static String removeFromHeader(String firstStr, String secondStr) { return isBlank(firstStr) || isBlank(secondStr) ? firstStr : (firstStr.startsWith(secondStr) ? firstStr.substring(secondStr.length()) : firstStr); } public static String subStrFromHead(String sourceString, int subLength) { if (sourceString == null) { return null; } return subLength < 0 ? "" : sourceString.length() <= subLength ? sourceString : sourceString.substring(0, subLength); } public static String subStrFromLast(String sourceString, int subLength) { if (sourceString == null) { return null; } return subLength < 0 ? "" : sourceString.length() <= subLength ? sourceString : sourceString.substring(sourceString.length() - subLength); } public static String subStrFromIndex(String sourceString, int startIndex, int subLength) { if (sourceString == null) { return null; } if (subLength < 0 || startIndex > sourceString.length()) { return ""; } if (startIndex < 0) { startIndex = 0; } return sourceString.length() <= startIndex + subLength ? sourceString.substring(startIndex) : sourceString.substring(startIndex, startIndex + subLength); }}