Posted in

数据脱敏代码这样写,连 CTO 看了都点赞收藏_AI阅读总结 — 包阅AI

包阅导读总结

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);    }}