superleeyom / blog

:bookmark: 个人博客仓库,用于记录一些幼稚的想法和脑残的瞬间,欢迎 star、watch,该仓库为个人博客,请不要提 issue ,该仓库后端参考了 @yihong0618 的 gitblog 项目,前端参考了@LoeiFy 的 Mirror 项目,感谢!

Home Page:https://blog.leeyom.top

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

业务数据脱敏解决方案探究

superleeyom opened this issue · comments

业务数据脱敏解决方案探究

使用背景

  • 在实际的业务场景中,业务开发团队需要针对公司安全部门需求,针对涉及客户安全数据或者一些商业性敏感数据,如身份证号、手机号、银行卡号、客户号等个人信息,都需要进行数据脱敏。

  • 搭建和生产环境一模一样的预发布环境,需要把生产环境的存量原文数据 加密后存储到预发环境。

技术调研

常见的脱敏算法

目前常见的脱敏算法包括 AES 加密、K 匿名、加星、屏蔽、洗牌、全保留、格式保留、令牌化等,算法及其主要用途介绍如下:

算法 主要用途
K 匿名 通过如 K-匿名的算法对原始数据加入扰动和泛化,使得脱敏后的数据无法唯一对应回原数据,用于保留部分统计信息
加星 最基础的脱敏方法,保留字段一部分信息的同时通过加星遮蔽局部信息
屏蔽 用特殊字符如星号 * 作为掩码来将字段信息屏蔽掉
洗牌 将目标字段在样本中洗牌,使得脱敏后的信息无法唯一对应回原始样本,常用于脱敏后作为测试样例或保留部分统计信息
全保留 字段全保留,不脱敏,常用于字段无需脱敏的场景,如对主键不脱敏
格式保留 保留字段的原格式的前提下将其中一部分信息进行随机化,达到脱敏的同时仍然可以提供一部分该类型的信息,常用于脱敏后作为测试样例
令牌化 通过不可逆的算法将目标字段脱敏,但保留一份令牌使得可以在必要时还原
AES 加密 一种对称加密算法,必要时可通过 AESKey 对脱敏结果进行还原

Java生态下的脱敏方案

目前 Java 生态环境下,数据脱敏中间件这块,做的最好的应该数 ShardingSphere,引用 ShardingSphere 官方文档关于数据脱敏模块的说明:

数据脱敏模块属于ShardingSphere分布式治理这一核心功能下的子功能模块。它通过对用户输入的SQL进行解析,并依据用户提供的脱敏配置对SQL进行改写,从而实现对原文数据进行加密,并将原文数据(可选)及密文数据同时存储到底层数据库。在用户查询数据时,它又从数据库中取出密文数据,并对其解密,最终将解密后的原始数据返回给用户。

更多关于 ShardingSphere 的数据脱敏原理,可以查阅官方文档

那如果说,我只是想简单的做一些数据清洗和隐私脱敏,有什么的好的工具类吗?那可以使用 hutool 的 DesensitizedUtil 工具类就行,例如:

String phone = "13488883888";
String encryptPhone = DesensitizedUtil.mobilePhone(phone);
// 打印:134****3888
System.out.println(encryptPhone);

技术演练

在 ShardingSphere 的关于数据脱敏的场景分析里,针对已上线的历史数据,需要业务方自己进行清洗。那怎么清洗?简单说下自己的想法:

  1. 首先数据库对那些需要脱敏的列,新增额外的加密列,比如需要对 email进行脱敏,则新建额外的加密列 encrypt_email。

  2. 系统接入 sharding-jdbc,并配置好脱敏规则。

  3. 写个脚本,多线程同时遍历需要脱敏的历史数据,将明文列(email)取出,更新到加密列(encrypt_email),由于sharding-jdbc 会根据脱敏规则,对SQL进行解析、改写,最后加密列存储的其实是加密后的数据。

这里将使用 ShardingSphere 的 sharding-jdbc 组件简单的演示如何进行数据脱敏。

数据库结构

user 表结构,其中 email 为明文列,encrypt_email 为密文列。

CREATE TABLE `user` (
  `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
  `age` int unsigned NOT NULL COMMENT '年龄',
  `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '明文邮箱 ',
  `encrypt_email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '加密邮箱',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_email` (`email`) USING BTREE COMMENT '邮箱唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

配置解析

这里只展示 sharding-jdbc 的配置部分:

spring:
  # sharding-jdbc配置
  shardingsphere:
    # 数据源配置
    datasource:
      name: ds
      ds:
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://127.0.0.1:3306/shardingsphere?useUnicode=true&useSSL=true&characterEncoding=utf8
        username: root
        password: '123456'
        max-total: 100
    encrypt:
      # 加密器配置
      encryptors:
        # 加密器的名字,这里设置为:email_encryptor
        email_encryptor:
          # 加密方式,内置MD5/AES 
          type: aes
          props:
            # 配置AES加密器的KEY属性
            aes.key.value: 123456abc
      # 脱敏表配置
      tables:
        # 脱敏表名
        user:
          columns:
            # 脱敏逻辑列,真实面向用户编写SQL
            email:
              # 存储明文列
              plainColumn: email
              # 存储加密列
              cipherColumn: encrypt_email
              # 使用的加密器
              encryptor: email_encryptor
    props:
      # 是否使用密文列查询
      query.with.cipher.column: true
      # 是否打印SQL,默认false
      sql.show: true

由于使用 ShardingSphere 的数据源 datasource 后,无需再配置 Spring 自带的 datasource,另外遇到个小问题就是,如果按照 ShardingSphere 官方关于数据脱敏的 springboot配置(官网示例 properties 格式,实际也可以转成yml 格式):

url: jdbc:mysql://127.0.0.1:3306/shardingsphere?useUnicode=true&useSSL=true&characterEncoding=utf8

会报错 jdbcUrl is required with driverClassName,换成 jdbc-url 则正常启动:

jdbc-url: jdbc:mysql://127.0.0.1:3306/shardingsphere?useUnicode=true&useSSL=true&characterEncoding=utf8

实验对比

插入一条数据:

userService.save(new User("韩武江", 28, "hanwujiang@163.com"));

观察控制台打印的两条log:

# 逻辑SQL
Logic SQL: INSERT INTO user  ( name,age,email )  VALUES  ( ?,?,? )
# 实际SQL
Actual SQL: ds ::: INSERT INTO user  ( name,age,encrypt_email, email )  VALUES  (?, ?, ?, ?) ::: [韩武江, 28, dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M=, hanwujiang@163.com] 

再观察数据库:

加密列 encrypt_email 被 sharding-jdbc 加密存储。

查询数据,并设置 query.with.cipher.column: true,开启密文列查询:

User user = userService.getByEmail("jianglanhe@gmail.com");
System.out.println(JSONUtil.toJsonStr(user));

观察控制台打印的log:

# 逻辑SQL
Logic SQL: SELECT  id,name,age,email,encrypt_email  FROM user WHERE  email=?
# 实际SQL
Actual SQL: ds ::: SELECT  id,name,age,encrypt_email AS email,encrypt_email  FROM user WHERE  encrypt_email=? ::: [dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M=] 
# 打印结果
{"encryptEmail":"dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M=","name":"韩武江","id":13,"age":28,"email":"hanwujiang@163.com"} 

若设置query.with.cipher.column: false,关闭密文列查询:

观察控制台打印的log:

# 逻辑SQL
Logic SQL: SELECT  id,name,age,email,encrypt_email  FROM user WHERE  email=?
# 实际SQL
Actual SQL: ds ::: SELECT  id,name,age,email AS email,encrypt_email  FROM user WHERE  email=? ::: [hanwujiang@163.com] 
# 打印结果
{"encryptEmail":"dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M=","name":"韩武江","id":13,"age":28,"email":"hanwujiang@163.com"} 

我原本以为在开启密文查询的情况,实际SQL都 encrypt_email AS email了,最终返回实体里面的email应该是密文:

SELECT
  id,
  `name`,
  age,
  encrypt_email AS email,
  encrypt_email 
FROM
  `user` 
WHERE
  encrypt_email = 'dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M=';

但是对比可以发现,开启密文列查询配置后,实际sharding-jdbc会在中间把密文解密后返回,最终返回实体里面的email应该是解密后的明文,密文存在的还是在密文列 encrypt_email 中。

假如后期业务上线一段时间后,需要完全删除明文列,只保留密文列,在不改变代码的基础上是否可行?那直接在数据库层,把email列删除:

然后修改 sharding-jdbc 配置,去掉明文列,不改写任何其他代码层面的东西:

# 脱敏表配置
tables:
  user:
    columns:
      email:
        # plainColumn: email
        cipherColumn: encrypt_email
        encryptor: email_encryptor

然后查询数据:

// 为了演示,这里改用id查询,如果用email查询的话,肯定为空
User user = userService.getById(13);
System.out.println(JSONUtil.toJsonStr(user));

观察控制台,发现其实依然能正常执行,且不报错:

# 逻辑SQL
Logic SQL: SELECT id,name,age,email,encrypt_email FROM user WHERE id=? 
# 实际SQL
Actual SQL: ds ::: SELECT id,name,age,encrypt_email AS email,encrypt_email FROM user WHERE id=?  ::: [13]
# 打印结果
{"encryptEmail":"dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M=","name":"韩武江","id":13,"age":28,"email":"dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M="}

至于为什么会正常运行,因为:

因为有logicColumn存在,用户的编写SQL都面向这个虚拟列,Encrypt-JDBC就可以把这个逻辑列和底层数据表中的密文列进行映射转换

假如你删除 email列,但是配置中还配置了 plainColumn: email,那代码执行的时候,则会报错:Cause: java.sql.SQLSyntaxErrorException: Unknown column 'email' in 'field list'

总结

我个人的感觉是 sharding-jdbc 确实做到了屏蔽底层对数据的脱敏处理 ,但是要接入 sharding-jdbc 的前提是,团队有制定严格的SQL规范 ,这样可能接入数据库中间件的时候,才会出现比较少的问题,对于一些老系统,动辄几百行的SQL,各种复杂函数,还是放弃接入的好,到时候只会是一步一个坑。

另外如果想要满足文章开头的第二个需求,也就是把生产库的数据同步到预发布,同时要屏蔽部分敏感数据,大部分的云厂商,都有提供脱敏工具,比如我们自己在用的腾讯云的 DBbrain,就可以支持数据脱敏,但是实际使用还不是怎么完善,有待改进。

示例代码:shardingsphere-encrypt-jdbc-demo