大家好呀,我是小羊,如果大家喜欢我的文章的话,就关注我一起学习进步吧~

最近 公司在做安全这块,准备把数据库中的敏感字段进行加密处理,防止数据被滥用。大家讨论了一下,最终确定使用shadingsphere 进行加密解密。这里和大家分享一下。

Apache ShardingSphere(Incubator) 是一套开源的分布式数据库中间件解决方案组成的生态圈。

说到 ShardingSphere 的起源,我们不得不提 Sharding-JDBC 框架,该框架是一款起源于当当网内部的应用框架,并于 2017 年初正式开源。从 Sharding-JDBC 到 Apache 顶级项目,目前社区也是非常活跃。ShardingSphere 的发展经历了不同的演进阶段。纵观整个 ShardingSphere 的发展历史,我们可以得到时间线与阶段性里程碑的演进过程图:

官方的代码贡献趋势图,可以看到是越来越活跃。

github 地址,目前16k的 star https://github.com/apache/shardingsphere

 

shardingsphere 包含很多组件,你可以用它来做分库分表、数据分片、分布式事务和数据库治理功能。

官方地址:https://shardingsphere.apache.org/document/5.0.0/cn/overview/

我们这次使用的是jdbc模块,shardingsphere 为了减少代码的侵入,使用了代理的方式,可以把它看成一个中间代理层,它的作用就是在我们修改数据时,它在数据库层把明文转成密文存储,在我们查询数据时,它读取密文将其解密成明文返回,这样开发者就不需要关心具体的细节了。

「主要优点有:」

  1. 较少的代码入侵

  2. 配置简单

  3. 性能损耗较小

  4. 轻量级

「主要缺点有:」

  1. 需要替换原来的数据源

  2. 不支持sql函数

1.maven 依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.6</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.44</version>
        </dependency>


        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
            <version>5.1.1-SNAPSHOT</version>
        </dependency>

    </dependencies>

我们目前用的 shardingsphere-jdbc-core-spring-boot-starter 是 5.1.1 版本的。

2.库表设计

/*
SQLyog Ultimate v12.09 (64 bit)
MySQL - 5.7.31-log : Database - test
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`test` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;

USE `test`;

/*Table structure for table `test_encrypt` */

DROP TABLE IF EXISTS `test_encrypt`;

CREATE TABLE `test_encrypt` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `name_encrypt` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;

/*Data for the table `test_encrypt` */

insert  into `test_encrypt`(`id`,`name`,`name_encrypt`) values (1,'test',NULL),(2,'test1',NULL),(3,'test1',NULL),(4,'test1','Tqdxz02pk09zEDUpxbbSOA==');

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

这里创建了一个很简单的一张表: 一个 id, 一个明文字段 name,一个密文字段 name_encrypt 用于存储加密后的字段。

3.项目配置

spring.datasource.druid.url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.druid.username=root
spring.datasource.druid.password=123456
spring.datasource.druid.max-active=20
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.max-wait=60000
spring.datasource.druid.validation-query=select 1
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.time-between-eviction-runs-millis=60000

mybatis.type-aliases-package=com.yangzheng.shardingsphere.entity
mybatis.mapper-locations = classpath:mapper/**/*.xml



#关闭原数据源配置,改用shardingsphere.datasource
spring.autoconfigure.exclude = com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure

#数据源配置
spring.shardingsphere.datasource.names = ds
spring.shardingsphere.datasource.ds.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds.driver-class-name = com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.ds.username = root
spring.shardingsphere.datasource.ds.password = 123456
spring.shardingsphere.datasource.ds.max-active = 100
# 采用AES对称加密策略
spring.shardingsphere.rules.encrypt.encryptors.aesencrypt.type = AES
spring.shardingsphere.rules.encrypt.encryptors.aesencrypt.props.aes-key-value = 4ZRAr+
# 是否使用加密列进行查询。在有原文列的情况下,可以使用原文列进行查询
spring.shardingsphere.rules.encrypt.queryWithCipherColumn = true
#plainColumn为明文列,cipherColumn密文列
spring.shardingsphere.rules.encrypt.tables.test_encrypt.columns.name.cipherColumn = name_encrypt
spring.shardingsphere.rules.encrypt.tables.test_encrypt.columns.name.encryptorName = aesencrypt
spring.shardingsphere.rules.encrypt.tables.test_encrypt.columns.name.plainColumn = name
spring.shardingsphere.rules.encrypt.tables.test_encrypt.queryWithCipherColumn = true

需要关注的几点

  1. 要关闭原数据源 druid, 使用 shardingsphere数据源。

  2. 明文列密文列的表名字段名不用写错了

  3. 我们目前使用的是AES算法,大家也可以换成其他的,不过后续如果要刷历史数据时,加密算法和密钥一定要保持一致,否则会解析不了

4.测试

「查询接口测试」

如果发现 数据源换成了 shardingsphere 就可以了。shardingsphere 会读取密文列 name_encrypt的加密数据,并根据配置的密钥和算法,转换成明文返回。

「插入接口测试」

shardingsphere 会根据明文列的数据,并根据配置的算法和密钥,加密成密文并插入数据表密文列。可以看到数据插入成功之后,加密字段和明文字段都是有数据的。这个是一种双写策略。

因为我们生产环境有历史数据并没有加密,并且我们刚上线时也不确定有没有问题,所以采用了这种双写策略。

具体配置如下,cipherColumn表示加密字段,plainColumn 是明文字段。

spring.shardingsphere.rules.encrypt.tables.test_encrypt.columns.name.cipherColumn = name_encrypt
spring.shardingsphere.rules.encrypt.tables.test_encrypt.columns.name.plainColumn = name

等到运行一段时间没有问题之后,我们就把明文字段删除了,然后把密文字段名 name_encrypt 改成了明文字段名 name,并且改了shardingsphere 的配置,关闭双写。

spring.shardingsphere.rules.encrypt.tables.test_encrypt.columns.name.cipherColumn = name
#spring.shardingsphere.rules.encrypt.tables.test_encrypt.columns.name.plainColumn = name

5.加密工具类

并且,我们写了一个工具类来处理历史数据,其实原理也比较简单,就是调用了它自身的aes加密方法,把mysql 中所有缺失的密文数据补上。具体工具类如下:

package com.yangzheng.shardingsphere.utils;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.StringUtils;
import org.apache.commons.codec.digest.DigestUtils;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

/**
 * AES 加解密
 *
 * @author xiaohezi
 * @since 2021-09-23 15:49
 */
public class AesUtils {

    private static byte[] createSecretKey(String aesKey) {
        return Arrays.copyOf(DigestUtils.sha1(aesKey), 16);
    }
    private static byte[] createMysqlSecretKey(String aesKey) {
        return Arrays.copyOf(aesKey.getBytes(StandardCharsets.UTF_8), 16);
    }

    /**
     * AES 加密方法
     *
     * @param plaintext 加密文本
     * @param aesKey    加密 key
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws BadPaddingException
     * @throws NoSuchPaddingException
     * @throws IllegalBlockSizeException
     */
    public static Object encrypt(String plaintext, String aesKey) throws NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException {
        try {
            if (null == plaintext) {
                return null;
            } else {
                byte[] result = getCipher(1, aesKey).doFinal(StringUtils.getBytesUtf8(plaintext));
                return Base64.encodeBase64String(result);
            }
        } catch (GeneralSecurityException var3) {
            throw var3;
        }
    }

    /**
     * AES 加密方法
     *
     * @param plaintext 加密文本
     * @param aesKey    加密 key
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws BadPaddingException
     * @throws NoSuchPaddingException
     * @throws IllegalBlockSizeException
     */
    public static Object mySqlEncrypt(String plaintext, String aesKey) throws NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException {
        try {
            if (null == plaintext) {
                return null;
            } else {
                byte[] result = getMysqlCipher(1, aesKey).doFinal(StringUtils.getBytesUtf8(plaintext));
                return Base64.encodeBase64String(result);
            }
        } catch (GeneralSecurityException var3) {
            throw var3;
        }
    }

    /**
     * AES 解密方法
     *
     * @param ciphertext 密码
     * @param aesKey     加密 Key
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws BadPaddingException
     * @throws NoSuchPaddingException
     * @throws IllegalBlockSizeException
     */

    public static Object decrypt(String ciphertext, String aesKey) throws NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException {
        try {
            if (null == ciphertext) {
                return null;
            } else {
                byte[] result = getCipher(2, aesKey).doFinal(Base64.decodeBase64(ciphertext));
                return new String(result, StandardCharsets.UTF_8);
            }
        } catch (GeneralSecurityException var3) {
            throw var3;
        }
    }

    private static Cipher getCipher(int decryptMode, String aesKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
        Cipher result = Cipher.getInstance(getType());
        result.init(decryptMode, new SecretKeySpec(createSecretKey(aesKey), getType()));
        return result;
    }
    private static Cipher getMysqlCipher(int decryptMode, String aesKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
        Cipher result = Cipher.getInstance(getType());
        result.init(decryptMode, new SecretKeySpec(createMysqlSecretKey(aesKey), getType()));
        return result;
    }

    public static String getType() {
        return "AES";
    }
}

一些问题

  1. 这种做法是不支持 函数的,如果有函数的sql 会失效。

  2. 如果自己配置了数据源,需要关掉。

源码

https://github.com/yangzheng0/springboot-shardingsphere-encrypt

好啦,今天的分享就到这里啦。

喜欢这篇文章就给点个赞吧。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐