针对大规模服务日志敏感信息的长效治理实践

news2024/9/22 9:31:43

文章目录

    • 1 背景
    • 2 目标与措施
    • 3 实施
      • 3.1 脱敏工具类
      • 3.2 JSON脱敏
      • 3.3 APT自动脱敏
        • 3.3.1 本地缓存问题
        • 3.3.2 JDK序列化问题
      • 3.4 弃用方案
    • 4 规划
    • 5 总结

1 背景

近年来,国家采取了多项重要举措来加强个人数据保护,包括实施《中华人民共和国网络安全法》和《个人信息保护法》等法律法规。这些举措旨在确保用户隐私的安全,同时确保企业合规运营。在处理敏感数据时,企业有责任采取适当的措施来保护用户信息。

在数据保护方面,日志记录成为一个需要特别关注的敏感信息领域。因此,本文将重点介绍转转在日志脱敏方面的应用与实践。

2 目标与措施

目标:
对日志内的手机号、身份证号、银行卡号等敏感信息脱敏,建立一个可持续的日志敏感信息管控机制。

措施:

  1. 检测和定位存在敏感日志的服务与CASE;
  2. 开发低接入成本的日志脱敏工具;
  3. 推动相关业务进行迭代修改;
  4. 长期监控和持续治理,确保日志安全。

我们的第一步是利用大数据离线扫描服务日志,并使用正则表达式匹配敏感信息。

然而,第二和第三步是挑战的关键,即
如何在不干扰业务正常迭代排期的情况下,推动大量服务的日志做脱敏。
我们希望使用技术手段尽量降低业务日志脱敏的人力成本。

3 实施

参考《转转日志规范》查看标准日志输出要求,在此基础之上,提供一些工具辅助业务对日志脱敏。

【推荐】JavaBean类需实现toString()方法,日志直接打印对象,慎用JSON工具将对象转换成String。

3.1 脱敏工具类

我们开发了脱敏工具类,期望
业务同学在实现JavaBean toString()方法的同时,使用脱敏工具对敏感字段使用脱敏。

  • desensitize(String input):通用脱敏函数,支持对任意字符脱敏,将提取字符串中4位以上数字(如手机号、银行卡号、身份证号、数字验证码等)做脱敏;
  • desensitizeByInputLength(String input):据字符串长度匹配不同的脱敏规则,如:11位则使用手机号脱敏规则,18位则使用身份证号脱敏规则;
  • desensitizePhoneNumber(String phoneNumber):脱敏手机号,前3位和后4位,中间的数字用*代替;
  • desensitizeIDCard(String idCard):脱敏身份证号, 保留前6位和后4位,脱敏7~15位生日信息, 用*代替;
  • desensitizeBankCardNumber(String bankCardNumber):脱敏银行卡号, 前6位和后4位,中间的数字用*代替。
public final class DesensitizeUtil {    
    
    /**
     * 根据字符串长度匹配不同的脱敏函数, 强制脱敏
     */
    public static String desensitizeByInputLength(String input) {
        int length = input.length();
        // 手机号
        if (length == 11) {
            return desensitizePhoneNumber(input);
        }
        // ,,,
    }

    /**
     * 脱敏手机号, 前3位和后4位,中间的数字用*代替
     */
    public static String desensitizePhoneNumber(String phoneNumber) {
        // 11位手机号
        if (phoneNumber.length() == 11) {
            return phoneNumber.substring(0, phoneNumber.length() - 8) + "****" + phoneNumber.substring(phoneNumber.length() - 4);
        }
        return phoneNumber;
    }
    
    // 省略其他脱敏函数...

}    

3.2 JSON脱敏

在某些日志记录的场景中,会打印包含敏感字段的JSON格式的数据,需要对其中的敏感信息进行脱敏处理。

在常见的JSON工具中,比如Jackson,可以
使用自定义的序列化器/反序列化器来实现脱敏。
下面以Jackson为例进行说明:

首先,我们可以定义一个注解来标注哪些字段需要脱敏处理:

/**
 * 脱敏注解
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Desensitize {
 
}

然后,我们可以创建一个自定义的Jackson模块,通过继承BeanSerializerModifier类来修改字段的序列化行为。在这个类中,我们可以根据字段上的Desensitize注解来判断是否需要进行脱敏处理:

/**
 * Jackson脱敏序列化修改器
 */
public class JacksonDesensitizeSerializerModifier extends BeanSerializerModifier {

    @Override
    public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
                                                     List<BeanPropertyWriter> beanProperties) {
        for (BeanPropertyWriter beanProperty : beanProperties) {
            // 只针对使用了@Desensitize的字段做脱敏
            Desensitize desensitize = beanProperty.getAnnotation(Desensitize.class);
            if(desensitize != null) {
                // 指定自定义的序列化器
                beanProperty.assignSerializer(new Desensitization());
            }
        }
        return beanProperties;
    }


    /**
     * Jackson序列化器
     */
    public class Desensitization extends StdSerializer<Object> {

        @Override
        public final void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            // 根据长度对字段做脱敏
            String desensitize = DesensitizeUtil.desensitizeByInputLength(String.valueOf(value));
            gen.writeString(desensitize);
        }

    }
}

最后,我们需要注册这个自定义的模块到Jackson

/**
 * JSON工具
 */
public class JsonUtil {

    private static final ObjectMapper DESENSITIZE_OBJECT_MAPPER = newObjectMapper();

    private static ObjectMapper newObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        //增加脱敏序列化器
        SimpleModule simpleModule = new SimpleModule("SimpleModuleDesensitize");
        simpleModule.setSerializerModifier(new JacksonDesensitizeSerializerModifier());
        mapper.registerModule(simpleModule);
        return mapper;
    }
    
   /**
    * 对象转JSON的自动脱敏工具
    */
    public static <T> String object2DesensitizeString(T object) throws JsonProcessingException {
        return DESENSITIZE_OBJECT_MAPPER.writeValueAsString(object);
    }
    
    //...
}    

对于业务同学而言,只需在需要脱敏的对象上添加脱敏注解,然后使用我们提供的JsonUtil进行脱敏操作,实现简单高效。

/**
 * 需要脱敏的对象
 */
public class User {
      
    /**
     * 标记此字段需要脱敏
     */
    @Desensitize
    private String mobile;

    private String username;
    
    //getter setter...
}

User user = new User();
user.setAge(18);
user.username = "zhangsan";
user.password = "123456";

JsonUtil.object2DesensitizeString(user);
//输出结果: {"mobile":"135****5555","username":"张三"}    

注意:以上代码只是一个示例,并不完整。在实际使用中,还需要根据具体的需求来灵活实现脱敏处理。

3.3 APT自动脱敏

在实际实施过程中,以上两个方案遇到了很多阻碍。主要问题在于业务同学
手动维护Bean的toString()方法过于繁琐、重复工作多、容易遗漏对象并导致增加或删除字段时需要不断修改toString()函数。此外,业务服务所依赖的Bean来源复杂,有可能是其他业务提供的第二方Jar包或第三方Jar包。

因此,在实际应用中,业务同学
更倾向于将Bean序列化为JSON
并输出到日志中,如下所示:

log.info("data={}", JsonUtil.object2DesensitizeString(bean));

然而,这种方法不符合《转转日志规范》要求,而且忽略了
JSON序列化性能
的问题。此外,这种方案也需要耗费大量的人力资源:
需要评估每一行日志,以确定是否需要添加JSON脱敏功能。

因此,业务同学提出了以下需求:是否可以实现类似Lombok一样的功能,
只需在Bean的字段上添加脱敏注解,就能在编译期自动实现脱敏后的toString()函数?
这样的话,在打印日志时直接打印对象即可自动脱敏。

经过调研发现,Lombok在编译时利用APT(Annotation Processing Tools)生成代码,实现了自动化的代码生成过程,从而简化了开发工作。

APT(Annotation Processing Tool)是Java的编译期注解处理器。它允许开发人员在编译期间处理注解,并根据注解和相关对象的信息生成Java代码模板或配置文件等。

APT的使用可以提高程序性能,因为它在代码编译时完成注解处理,而不是在运行时使用反射方式处理注解。

著名的开源框架,如Lombok、MapStruct和AutoService等,也使用了类似的技术来优化代码的生成和处理过程。

我们利用APT技术实现了这样的功能:
如果一个类没有重写Object.toString()方法,在编译时会自动为该类生成一个脱敏后的toString()方法。
这个自动生成的toString()方法能够识别脱敏注解,并在生成的toString()方法内对敏感信息进行脱敏处理。

在Java编译后的Class文件中,toString()方法可能来自三个来源:源代码、转转APT处理、Lombok等。优先级为:
源代码 > 转转APT处理 > Lombok等其他APT。
简言之,我们的APT处理不会覆盖源代码中定义的toString()方法,但会覆盖由Lombok生成的toString()方法。

比如,我们有以下源码:

class User {

    private String username;
    /**
     * 密码,增加了脱敏注解
     */
    @Desensitize
    private String password;
}

在接入转转APT后,反编译的Class文件如下:

class User {
    private String username;
    @Desensitize
    private String password;

    public String toString() {
        StringJoiner sj = new StringJoiner(", ", "User[", "]");
        
        if (this.username != null) {
            sj.add("username=" + this.username);
        }
 
        if (this.password != null) {
            sj.add("password=" + DesensitizeUtil.desensitizeByInputLength(value));
        }
 
        return sj.toString();
    }
}

测试如下:

User user = new User();
user.username = "zhangsan";
user.password = "123456";

System.out.println(user);  
//输出结果: User[username=张三, password=1****6]

这个功能的上线大大降低了业务同学实现日志脱敏的工作量,只需为字段添加脱敏注解即可。同时,
也解决了线上对象未重写Object.toString()时打印日志的尴尬问题。

不过,
在落地APT过程中,我们也遇到了一些问题,
希望能给读者提供一些有收益的参考。

3.3.1 本地缓存问题

在某个服务的Spring Bean上,有一个包含大量本地缓存的List字段,这个服务会打印Spring Bean对象到日志中。在引入转转APT之前,一切正常;但引入后,出现了频繁的OOM问题。通过内存分析后发现,问题出在转转APT为Spring Bean自动生成的toString()函数内产生了大量的字符串上。

@Service
public class AppService {
    /**
     * 本地缓存
     */
    private List<Object> cache = new ArrayList<>();
    
}

@Autowired
private AppService service;

log.info("service={}", service);

我们观察到大部分带有本地缓存(或者高内存占用字段)的对象都是Spring的Bean,因此,我们对转转APT进行了修改:即
不再为Spring Bean生成toString()函数。

3.3.2 JDK序列化问题

某个服务的JavaBean使用了原生JDK的序列化/反序列化工具,但是这个JavaBean却没有添加serialVersionUID

class Person implements Serializable {

    // 没有定义serialVersionUID
    // private static final long serialVersionUID = -55721300387280236L;

}

Java序列化机制使用long型的serialVersionUID字段来标志类的版本号;序列化对象时,JVM会将serialVersionUID的值写入序列化数据中;反序列化时,JVM会将序列化数据中的serialVersionUID与对应类中的serialVersionUID进行比较,若不同,则抛出InvalidCastException;若版本号相同,则能够进行反序列化。

当一个类没有显式定义serialVersionUID时,JVM会自动根据类的信息计算生成一个默认的serialVersionUID。这样,在类发生变化时,自动生成的serialVersionUID可能会改变,导致无法正确反序列化之前的数据。

引入转转APT后,由于自动生成了toString函数,类信息发生变化,导致serialVersionUID也发生了改变,进而导致反序列化失败。

解决方式是将之前默认生成的serialVersionUID找到,并将其添加到类的源码中。

3.4 弃用方案

还有一种快速落地的方法是,通过在应用程序内部统一拦截日志输出,正则匹配敏感信息,并利用脱敏工具进行脱敏处理。

我们没有使用这种方式的原因是因为:脱敏应尽量避免正则匹配,容易误伤且性能低下。

4 规划

上文提过,服务内依赖的Java Bean来源十分复杂,我们目前只解决了对象本身的脱敏问题。而对于服务依赖的Jar包版本控制,仍需要业务团队梳理依赖关系,并手动修改脱敏后的Jar包版本,这一过程仍需要耗费较多的时间和人力。

考虑到这个问题,是否可以为每个服务提供一个
依赖关系管控系统
?该系统可以对Jar包的版本实现自动更新、自动化测试、灰度发布、自动发布和回滚等一系列功能。对于转转目前的情况来说,我相信这不是一个技术问题,而是一个需要更多时间来完善的TODO List。

5 总结

一个小小的功能日志脱敏,却经历了多个阶段与挑战,从敏感日志的发现到开发脱敏工具类,再到Json脱敏,再到APT脱敏,最终推动业务应用。核心的挑战在于
如何做好推动相关的工作?

我认为,推动相关工作的核心在于有效应对内在和外在的因素。然而,外部因素对推动的阻力常常更大,要成功推动工作,
转变外部阻力为内部动力至关重要
。而对于推动者而言,
换位思考、勇于挑战未知、深入追根究底
的打磨产品会使产品更容易被接受和推广。


关于作者

苑冲,转转架构部存储服务负责人,负责MQ、监控系统、KV存储、时序数据库、Redis、KMS秘钥管理等基础组件。喜欢深入思考问题,对探索新领域和解决问题充满热情。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1381337.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JAVA毕业设计632—基于Java+ssm的宠物店商城系统(源代码+数据库)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于Javassm的宠物店商城系统(源代码数据库)632 一、系统介绍 本项目分为用户、营养师、管理员三种角色 1、用户&#xff1a; 登录、注册、宠物信息、宠物粮食、宠物用品、宠物疫…

imx6ull基于yocto工程的l汇编点亮ed

通过汇编点亮led 在裸机状态下通过汇编点亮led&#xff0c;即没有操作系统&#xff0c;(uboot kernel rootfs 都不需要实现&#xff09;。 led点亮原理 1.GPIO复用 根据原理图&#xff0c;找到led对应的引脚&#xff08;pin)&#xff0c;复用为GPIO&#xff08;只有GPIO才能…

平衡搜索二叉树(AVL树)

目录 前言 一、AVL树的概念 二、AVL树的定义 三、AVL树的插入 四、AVL树的旋转 4.1、右单旋 4.2、左单旋 4.3、左右双旋 4.4、右左双旋 五、AVL树的验证 5.1、 验证其为二叉搜索树 5.2、 验证其为平衡树 六、AVL树的性能 前言 二叉搜索树虽可以缩短查找的效率&…

对快速排序思想的进一步理解,分而治之,欧几里得算法(常用求最大公约数的方法)

自己找到的最优的快排的代码 快速排序 思想 分而治之使用欧几里得算法&#xff08;辗转相除法&#xff09;来求解一个应用题 假设有一块地&#xff0c;现在用这个同样大小的正方形来铺满&#xff0c;求所可用的最大的正方形地砖的面积 这两个方法放在一起是因为这个欧几里得要…

JS中的File(二):TypedArray和ArrayBuffer详解

目录 一、TypedArray 1、定义 2、注意事项 二、ArrayBuffer 1、定义和构造 2、属性 3、方法 4、使用意义 三、Blob、TypedArray和ArrayBuffer的互相转换 1、websocket接收arrayBuffer 2、blob转arrayBuffer 3、arrayBuffer to Blob 4、ArrayBuffer to Uint8数组&am…

两周掌握Vue3(五):自定义指令、路由、ajax

文章目录 一、自定义指令1.创建和使用自定义指令2.钩子函数3.使用参数 二、路由1.创建一个router实例2.在components目录中创建组件3.将路由实例挂载到应用4.使用路由 三、Ajax 代码仓库&#xff1a;跳转 当前分支&#xff1a;05 一、自定义指令 自定义指令是Vue.js框架提供的…

掌握WPF控件:熟练常用属性(一)

WPF布局常用控件&#xff08;一&#xff09; Border Border控件是一个装饰控件&#xff0c;用于围绕其他元素绘制边框和背景。它提供了一种简单的方式来为其他控件添加边框和背景样式&#xff0c;而无需自定义控件的绘制逻辑。 常用属性描述Background用于设置背景颜色或图像…

增删改查管理系统 总结1

//提醒&#xff1a; 管理员也要有增删改查 新增员工代码完善2可能需要用到 目录 细节1 pom文件出现奇怪页面&#xff1f; 细节2 如何联系DataGrip与idea&#xff1f; 细节3 Yapi?接口文档&#xff1f;如何有以下画面&#xff1f; ​细节4 如何将时间转化为好看的时间&…

【.NET Core】Lazy<T> 实现延迟加载详解

【.NET Core】Lazy 实现延迟加载详解 文章目录 【.NET Core】Lazy<T> 实现延迟加载详解一、概述二、Lazy<T>是什么三、Lazy基本用法3.1 构造时使用默认的初始化方式3.2 构造时使用指定的委托初始化 四、Lazy.Value使用五、Lazy扩展用法5.1 实现延迟属性5.2 Lazy实现…

语义分割miou指标计算详解

文章目录 1. 语义分割的评价指标2. 混淆矩阵计算2.1 np.bincount的使用2.2 混淆矩阵计算 3. 语义分割指标计算3.1 IOU计算方式1(推荐)方式2 3.2 Precision 计算3.3 总体的Accuracy计算3.4 Recall 计算3.5 MIOU计算 参考 MIoU全称为Mean Intersection over Union&#xff0c;平均…

山西电力市场日前价格预测【2024-01-13】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2024-01-13&#xff09;山西电力市场全天平均日前电价为231.81元/MWh。其中&#xff0c;最高日前电价为345.71元/MWh&#xff0c;预计出现在00:15。最低日前电价为0.00元/MWh&#xff0c;预计出…

大模型实战笔记04——XTuner 大模型单卡低成本微调实战

大模型实战笔记04——XTuner 大模型单卡低成本微调实战 1、Finetune简介 2、XTuner 3、8GB显存玩转LLM 4、动手实战环节 注&#xff1a; 笔记内容均为截图 课程视频地址&#xff1a;https://www.bilibili.com/video/BV1yK4y1B75J/?spm_id_from333.788&vd_source2882acf…

行为型设计模式——备忘录模式

备忘录模式 备忘录模式提供了一种状态恢复的实现机制&#xff0c;使得用户可以方便地回到一个特定的历史步骤&#xff0c;当新的状态无效或者存在问题时&#xff0c;可以使用暂时存储起来的备忘录将状态复原&#xff0c;很多软件都提供了撤销&#xff08;Undo&#xff09;操作…

计算机网络NCEPU复习资料

目录 一&#xff0e;概述&#xff1a; 计算机网络组成&#xff1a; 计算机网络分类&#xff1a; 计算机网络体系结构&#xff1a; C/S架构与P2P架构区别&#xff1a; OSI开放式系统互连参考模型&#xff1a; OSI开放式系统互连参考模型 相关协议&#xff1a; 五层协议网…

【Vue2】一个数组按时间分割为【今年】和【往年】俩个数组

一. 需求 后端返回一个数组&#xff0c;前端按时间维度将该数组的分割为【今年】和【往年】俩个数组后端返回的数组格式如下 timeList:[{id:1,billTime:"2024-01-10",createTime:"2024-01-10 00:00:00",status:0},{id:2,billTime:"2022-05-25"…

(收藏)数据治理:一文讲透数据安全

数据治理&#xff1a;一文讲透数据安全 数据安全是数据治理的核心内容之一&#xff0c;随着数据治理的深入&#xff0c;我不断的碰到数据安全中的金发姑娘问题&#xff08;指安全和效率的平衡&#xff09;。 DAMA说&#xff0c;降低风险和促进业务增长是数据安全活动的主要…

详解Java信号量-Semaphore

第1章&#xff1a;引言 大家好&#xff0c;我是小黑。今天&#xff0c;咱们一起来深入探讨一下Semaphore。在Java中&#xff0c;正确地管理并发是一件既挑战又有趣的事情。当谈到并发控制&#xff0c;大家可能首先想到的是synchronized关键字或者是ReentrantLock。但其实&…

Open3D 获取点云坐标最值(17)

Open3D 获取点云坐标最值(17) 一、算法介绍二、算法实现1.代码2.结果人生天地间,忽如远行客 一、算法介绍 快速获取点云块,沿着 x y z 各方向的坐标最值,这些在点云处理中的应用范围是如此广泛,这也是点云最常被用到的关键信息,后续的很多算法都会设置到这一处理方法。…

迈入AI智能时代!ChatGPT国内版免费AI助手工具 peropure·AI正式上线 一个想法写一首歌?这事AI还真能干!

号外&#xff01;前几天推荐的Peropure.Ai迎来升级&#xff0c;现已支持联网模式&#xff0c;回答更新更准&#xff0c;欢迎注册体验&#xff1a; https://sourl.cn/5T74Hu 相信很多人都有过这样的想法&#xff0c;有没有一首歌能表达自己此时此刻的心情&#xff1a; 当你在深…

【LabVIEW FPGA入门】模拟输入和模拟输出

1.简单模拟输入和输出测试 1.打开项目&#xff0c;在FPGA终端下面新建一个VI 2.本示例以模拟输入卡和模拟输出卡同时举例。 3.新建一个VI编写程序&#xff0c;同时将卡1的输出连接到卡2的输入使用物理连线。 4.编译并运行程序&#xff0c;观察是否能从通道中采集和输出信号。 5…