Featured image of post ShardingSphere 实现数据加密(脱敏)第二篇

ShardingSphere 实现数据加密(脱敏)第二篇

上一篇文章中说道数据加密分两种场景 ShardingSphere 实现数据加密(脱敏)第一篇分别是:新上线业

上一篇文章中说道数据加密分两种场景 ShardingSphere 实现数据加密(脱敏)第一篇

分别是:

  • 新上线业务
  • 已上线业务

这篇我们对已上线业务进行模拟实验。

已上线业务改造

系统迁移前

建表语句和配置文件

1CREATE TABLE `t_cipher_old` (
2  `id` bigint(20) NOT NULL,
3  `name` varchar(255) DEFAULT NULL,
4  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
5  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
6  `pwd` varchar(100) DEFAULT NULL,
7  `mobile` varchar(100) DEFAULT NULL,
8  PRIMARY KEY (`id`)
9) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

为了模拟已经上线的业务,我们为表中造一些测试数据,并编写业务接口实现 CURD

Image

然而需要在数据库表 t_user 里新增一个字段叫做 pwd_cipher,即 cipherColumn,用于存放密文数据,同时我们把 plainColumn 设置为 pwd,用于存放明文数据,而把 logicColumn 也设置为 pwd。

1ALTER TABLE test.t_cipher_old ADD pwd_cipher varchar(100) NULL;

由于之前的代码 SQL 就是使用 pwd 进行编写,即面向逻辑列进行 SQL 编写,所以业务代码无需改动。通过 Apache ShardingSphere,针对新增的数据,会把明文写到 pwd 列,并同时把明文进行加密存储到 pwd_cipher 列。此时,由于 queryWithCipherColumn 设置为 false,对业务应用来说,依旧使用 pwd 这一明文列进行查询存储,却在底层数据库表 pwd_cipher 上额外存储了新增数据的密文数据

配置文件如下(本文只需要关注 encrypt 节点部分):

 1spring:
 2  profiles:
 3    include: common-local
 4  shardingsphere:
 5    datasource:
 6      names: write-ds,read-ds-0
 7      write-ds:
 8        jdbcUrl: jdbc:mysql://mysql.local.test.myapp.com:23306/test?allowPublicKeyRetrieval=true&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false&autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
 9        type: com.zaxxer.hikari.HikariDataSource
10        driver-class-name: com.mysql.cj.jdbc.Driver
11        username: root
12        password: Qq2e66hxnNd9MdNc
13        connectionTimeoutMilliseconds: 3000
14        idleTimeoutMilliseconds: 60000
15        maxLifetimeMilliseconds: 1800000
16        maxPoolSize: 50
17        minPoolSize: 1
18        maintenanceIntervalMilliseconds: 30000
19      read-ds-0:
20        jdbcUrl: jdbc:mysql://mysql.local.test.read1.glzhapp.com:23306/test?allowPublicKeyRetrieval=true&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false&autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
21        type: com.zaxxer.hikari.HikariDataSource
22        driver-class-name: com.mysql.cj.jdbc.Driver
23        username: root
24        password: Qq2e66hxnNd9MdNc
25        connectionTimeoutMilliseconds: 3000
26        idleTimeoutMilliseconds: 60000
27        maxLifetimeMilliseconds: 1800000
28        maxPoolSize: 50
29        minPoolSize: 1
30        maintenanceIntervalMilliseconds: 30000
31    rules:
32      readwrite-splitting:
33        data-sources:
34          glapp:
35            write-data-source-name: write-ds
36            read-data-source-names:
37              - read-ds-0
38            load-balancer-name: roundRobin # 负载均衡算法名称
39        load-balancers:
40          roundRobin:
41            type: ROUND_ROBIN # 一共两种一种是 RANDOM(随机),一种是 ROUND_ROBIN(轮询)
42      encrypt:
43        encryptors:
44          pwd-encryptor:
45            props:
46              aes-key-value: 123456abc
47            type: AES
48        tables:
49          t_cipher_old:
50            columns:
51              pwd: # pwd 与 pwd_cipher 的转换映射
52                plain-column: pwd # 原文列名称
53                cipher-column: pwd_cipher # 加密列名称
54                encryptor-name: pwd-encryptor # 加密算法名称(名称不能有下划线)
55        queryWithCipherColumn: false # 是否使用加密列进行查询。在有原文列的情况下,可以使用原文列进行查询

此时调用业务接口,新插入的数据就会在明文列 pwd 和加密列 pwd_cipher 同时存储数据。Image

上面整个的处理流程如下图所示:

Image

至此,改造以后时间点进入的数据都是加密的了。

系统迁移中

将旧的数据自行加密处理

具体到我们这个例子来讲,需要手动将 pwd 字段未加密的值全部手动加密后将密文存储到 pwd_cipher.

形象地说,就是将空的位置手动补齐。Image

首先我们参考 ShardingSphere 的 AES 加解密码算法改造了一个工具类:

 1
 2/**
 3 * AES 加解密
 4 *
 5 * @author xiaohezi
 6 * @since 2021-09-23 15:49
 7 */
 8public class AesUtils {
 9
10    private static byte[] createSecretKey(String aesKey) {
11        return Arrays.copyOf(DigestUtils.sha1(aesKey), 16);
12    }
13
14    /**
15     * AES 加密方法
16     *
17     * @param plaintext 加密文本
18     * @param aesKey    加密 key
19     * @return
20     * @throws NoSuchAlgorithmException
21     * @throws InvalidKeyException
22     * @throws BadPaddingException
23     * @throws NoSuchPaddingException
24     * @throws IllegalBlockSizeException
25     */
26    public static Object encrypt(String plaintext, String aesKey) throws NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException {
27        try {
28            if (null == plaintext) {
29                return null;
30            } else {
31                byte[] result = getCipher(1, aesKey).doFinal(StringUtils.getBytesUtf8(plaintext));
32                return Base64.encodeBase64String(result);
33            }
34        } catch (GeneralSecurityException var3) {
35            throw var3;
36        }
37    }
38
39    /**
40     * AES 解密方法
41     *
42     * @param ciphertext 密码
43     * @param aesKey     加密 Key
44     * @return
45     * @throws NoSuchAlgorithmException
46     * @throws InvalidKeyException
47     * @throws BadPaddingException
48     * @throws NoSuchPaddingException
49     * @throws IllegalBlockSizeException
50     */
51
52    public static Object decrypt(String ciphertext, String aesKey) throws NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException {
53        try {
54            if (null == ciphertext) {
55                return null;
56            } else {
57                byte[] result = getCipher(2, aesKey).doFinal(Base64.decodeBase64(ciphertext));
58                return new String(result, StandardCharsets.UTF_8);
59            }
60        } catch (GeneralSecurityException var3) {
61            throw var3;
62        }
63    }
64
65    private static Cipher getCipher(int decryptMode, String aesKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
66        Cipher result = Cipher.getInstance(getType());
67        result.init(decryptMode, new SecretKeySpec(createSecretKey(aesKey), getType()));
68        return result;
69    }
70
71    public static String getType() {
72        return "AES";
73    }
74}

然后为了简单演示,我的思路是用 java 程序将数据查出来以后直接更新,查询简单,更新的话用 mybatisplus 的 mapper 简单写了个自定义 sql 的方法

1/**
2 * 根据 id 将密码的密文更新
3 *
4 * @param id
5 * @param pwdCipher
6 */
7@Update("update t_cipher_old set pwd_cipher =#{pwdCipher}  where id = #{id}")
8void updateCipher(@Param("id") Long id, @Param("pwdCipher") String pwdCipher);

下面是更新方法,注意我这里的 aesKey 和上面的配置文件是保持一致的。

 1@Override
 2public void updateOldPwd() {
 3    QueryWrapper<CipherOldDO> wrapper = new QueryWrapper<>();
 4    wrapper.isNull("pwd_cipher");
 5
 6    List<CipherOldDO> list = list(wrapper);
 7
 8    String aesKey = "123456abc";
 9
10    try {
11        for (CipherOldDO cipherOldDO : list) {
12
13            Object encrypt = AesUtils.encrypt(cipherOldDO.getPwd(), aesKey);
14            //更新密码的密文
15            getBaseMapper().updateCipher(cipherOldDO.getId(), encrypt.toString());
16
17        }
18    } catch (NoSuchAlgorithmException e) {
19        e.printStackTrace();
20    } catch (InvalidKeyException e) {
21        e.printStackTrace();
22    } catch (BadPaddingException e) {
23        e.printStackTrace();
24    } catch (NoSuchPaddingException e) {
25        e.printStackTrace();
26    } catch (IllegalBlockSizeException e) {
27        e.printStackTrace();
28    }
29
30}

程序执行完,加密列 pwd_cipher 就有数据了。

Image

由于配置项中的 queryWithCipherColumn = false,所以密文一直没有被使用过。如果我们为了让系统能切到密文数据进行查询,需要将加密配置中的 queryWithCipherColumn 设置为 true。

虽然现在业务系统通过将密文列的数据取出,解密后返回;但是,在存储的时候仍旧会存一份原文数据到明文列,这是为什么呢?答案是:为了能够进行系统回滚。因为只要密文和明文永远同时存在,我们就可以通过开关项配置自由将业务查询切换到 cipherColumn 或 plainColumn。也就是说,如果将系统切到密文列进行查询时,发现系统报错,需要回滚。那么只需将 queryWithCipherColumn = false,Apache ShardingSphere 将会还原,即又重新开始使用 plainColumn 进行查询。处理流程如下图所示:

Image

系统迁移后

业务系统一般不可能让数据库的明文列和密文列永久同步保留,我们需要在系统稳定后将明文列数据删除。

但是删除列对于业务代码来说是不需要发动的,因为有 logicColumn 存在,用户的编写 SQL 都面向这个虚拟列,Apache ShardingSphere 就可以把这个逻辑列和底层数据表中的密文列进行映射转换。于是迁移后的加密配置即为:

 1  encrypt:
 2    encryptors:
 3        pwd-encryptor:
 4        props:
 5            aes-key-value: 123456abc
 6        type: AES
 7    tables:
 8        t_cipher_old:
 9        columns:
10            pwd: # pwd 与 pwd_cipher 的转换映射
11            cipher-column: pwd_cipher # 加密列名称
12            encryptor-name: pwd-encryptor # 加密算法名称(名称不能有下划线)
13    queryWithCipherColumn: true # 是否使用加密列进行查询。在有原文列的情况下,可以使用原文列进行查询

在数据库中直接将 pwd 列删除

Image

可以看到已经没有 pwd 列的,只剩下加过密的 pwd_cipher , 从数据库这里我们已经看不出密码是什么了。然后我们调用查询接口,看到数据:

 1{
 2    "code": 100000,
 3    "msg": "",
 4    "data": [
 5        {
 6            "id": 1,
 7            "name": "Tara",
 8            "pwd": "dogT",
 9            "mobile": "+425(864)267-129",
10            "createTime": "1994-12-02 18:39:01",
11            "updateTime": "2021-09-23 16:45:08"
12        },
13        {
14            "id": 2,
15            "name": "Earl",
16            "pwd": "ju",
17            "mobile": "+17(252)465-481",
18            "createTime": "2016-10-05 15:15:43",
19            "updateTime": "2021-09-23 16:45:08"
20        },
21        {
22            "id": 3,
23            "name": "Roberta",
24            "pwd": "fo",
25            "mobile": "+44(296)354-787",
26            "createTime": "2008-10-09 17:21:36",
27            "updateTime": "2021-09-23 16:45:08"
28        },
29        {
30            "id": 4,
31            "name": "Travis",
32            "pwd": "brow",
33            "mobile": "+77(975)452-214",
34            "createTime": "2005-02-17 07:14:24",
35            "updateTime": "2021-09-23 16:45:08"
36        }
37    ]
38}

可以看到,数据是解密以后的样子。

其处理流程如下:

Image

参考

位旅人路过 次翻阅 初次见面