❝大家好呀,我是小羊,如果大家喜欢我的文章的话,就关注我一起学习进步吧~
❞
最近 公司在做安全这块,准备把数据库中的敏感字段进行加密处理,防止数据被滥用。大家讨论了一下,最终确定使用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 为了减少代码的侵入,使用了代理的方式,可以把它看成一个中间代理层,它的作用就是在我们修改数据时,它在数据库层把明文转成密文存储,在我们查询数据时,它读取密文将其解密成明文返回,这样开发者就不需要关心具体的细节了。
「主要优点有:」
-
较少的代码入侵
-
配置简单
-
性能损耗较小
-
轻量级
「主要缺点有:」
-
需要替换原来的数据源
-
不支持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
需要关注的几点
-
要关闭原数据源 druid, 使用 shardingsphere数据源。
-
明文列密文列的表名字段名不用写错了
-
我们目前使用的是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";
}
}
一些问题
-
这种做法是不支持 函数的,如果有函数的sql 会失效。
-
如果自己配置了数据源,需要关掉。
源码
https://github.com/yangzheng0/springboot-shardingsphere-encrypt
好啦,今天的分享就到这里啦。
喜欢这篇文章就给点个赞吧。