Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
superleeyom opened this issue Jul 8, 2021 · 0 comments
Open

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

superleeyom opened this issue Jul 8, 2021 · 0 comments
Assignees

Comments

@superleeyom
Copy link
Owner

superleeyom commented Jul 8, 2021

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

使用背景

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

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

技术调研

常见的脱敏算法

目前常见的脱敏算法包括 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, "[email protected]"));

观察控制台打印的两条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("[email protected]");
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":"[email protected]"} 

若设置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":"[email protected]"} 

我原本以为在开启密文查询的情况,实际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

@superleeyom superleeyom self-assigned this Jul 8, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant