27 KiB
因收到Google相关通知,网站将会择期关闭。相关通知内容
30 数据脱敏:如何基于改写引擎实现低侵入性数据脱敏方案?
今天,我们讨论 ShardingSphere 中的数据脱敏模块。通过在 “10 | 数据脱敏:如何确保敏感数据的安全访问?” 课时中的介绍,我们知道 ShardingSphere 提供了一套自动的数据加解密机制来实现透明化的数据脱敏。
数据脱敏模块整体架构
与普通的编程模式一样,对于数据脱敏而言,我们同样先获取一个 DataSource 作为整个流程的入口,当然这里获取的不是一个普通的 DataSource,而是一个专门针对数据脱敏的 EncryptDataSource。对于数据脱敏模块,我们的思路还是从上到下,从 EncryptDataSource 开始进入到 ShardingSphere 数据脱敏的世界中。
同时,我们这次讲解数据脱敏模块不是零基础,因为在前面介绍 ShardingDataSource、ShardingConnection、ShardingStatement 等内容时,已经对整个 SQL 执行流程的抽象过程做了全面介绍,所涉及的很多内容对于数据脱敏模块而言也都是适用的。
让我们结合下图来做一些回顾:
上图中,可以看到与数据脱敏模块相关的类实际上都继承了一个抽象类,而这些抽象类在前面的内容都已经做了介绍。因此,我们对数据脱敏模块将重点关注于几个核心类的讲解,对于已经介绍过的内容我们会做一些回顾,但不会面面俱到。
基于上图,我们从 EncryptDataSource 开始入手,EncryptDataSource 的创建依赖于工厂类 EncryptDataSourceFactory,其实现如下所示:
public final class EncryptDataSourceFactory {
public static DataSource createDataSource(final DataSource dataSource, final EncryptRuleConfiguration encryptRuleConfiguration, final Properties props) throws SQLException {
return new EncryptDataSource(dataSource, new EncryptRule(encryptRuleConfiguration), props);
}
}
这里直接创建了一个 EncryptDataSource,依赖于 EncryptRule 规则对象,我们先来梳理一下 EncryptRule 中具体包含了哪些内容。
EncryptRule
EncryptRule 是数据脱敏模块的一个核心对象,值得我们专门进行展开。在 EncryptRule 中,定义了如下所示的三个核心变量:
//加解密器 private final Map<String, ShardingEncryptor> encryptors = new LinkedHashMap<>(); //脱敏数据表 private final Map<String, EncryptTable> tables = new LinkedHashMap<>(); //脱敏规则配置 private EncryptRuleConfiguration ruleConfiguration;
我们可以把这三个变量分成两部分,其中 ShardingEncryptor 用于完成加解密,而 EncryptTable 和 EncryptRuleConfiguration 则更多的与数据脱敏的配置体系相关。
接下来我将对这两部分分别展开讲解。
1.ShardingEncryptor
在 EncryptRule 中,ShardingEncryptor 是一个接口,代表具体的加密器类,该接口定义如下:
public interface ShardingEncryptor extends TypeBasedSPI { //初始化 void init(); //加密 String encrypt(Object plaintext); //解密 Object decrypt(String ciphertext); }
ShardingEncryptor 接口中存在一对用于加密和解密的方法,同时该接口也继承了 TypeBasedSPI 接口,意味着会通过 SPI 的方式进行动态类加载。
ShardingEncryptorServiceLoader 完成了这个工作,同时在 sharding-core-common 工程中,我们也找到了 SPI 的配置文件,如下所示:
ShardingEncryptor 的 SPI 配置文件
可以看到这里有两个实现类,分别是 MD5ShardingEncryptor 和 AESShardingEncryptor。对于 MD5 算法而言,我们知道它是单向散列的,无法根据密文反推出明文,MD5ShardingEncryptor 的实现类如下所示:
public final class MD5ShardingEncryptor implements ShardingEncryptor {
private Properties properties = new Properties();
@Override
public String getType() {
return "MD5";
}
@Override
public void init() {
}
@Override
public String encrypt(final Object plaintext) {
return DigestUtils.md5Hex(String.valueOf(plaintext));
}
@Override
public Object decrypt(final String ciphertext) {
return ciphertext;
}
}
而 AES 是一个对称加密算法,所以可以根据密文反推出明文,对应的 AESShardingEncryptor 如下所示:
public final class AESShardingEncryptor implements ShardingEncryptor {
private static final String AES_KEY = "aes.key.value";
private Properties properties = new Properties();
@Override
public String getType() {
return "AES";
}
@Override
public void init() {
}
@Override
@SneakyThrows
public String encrypt(final Object plaintext) {
byte[] result = getCipher(Cipher.ENCRYPT_MODE).doFinal(StringUtils.getBytesUtf8(String.valueOf(plaintext)));
//使用 Base64 进行加密
return Base64.encodeBase64String(result);
}
@Override
@SneakyThrows
public Object decrypt(final String ciphertext) {
if (null == ciphertext) {
return null;
}
//使用 Base64 进行解密
byte[] result = getCipher(Cipher.DECRYPT_MODE).doFinal(Base64.decodeBase64(String.valueOf(ciphertext)));
return new String(result, StandardCharsets.UTF_8);
}
private Cipher getCipher(final int decryptMode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
Preconditions.checkArgument(properties.containsKey(AES_KEY), "No available secret key for `%s`.", AESShardingEncryptor.class.getName());
Cipher result = Cipher.getInstance(getType());
result.init(decryptMode, new SecretKeySpec(createSecretKey(), getType()));
return result;
}
private byte[] createSecretKey() {
Preconditions.checkArgument(null != properties.get(AES_KEY), String.format("%s can not be null.", AES_KEY));
//创建秘钥
return Arrays.copyOf(DigestUtils.sha1(properties.get(AES_KEY).toString()), 16);
}
}
这里就是对一些常用加密库的直接使用,不做展开讨论。
2.EncryptRuleConfiguration
我们接下来关注于 EncryptRule 中的第二组变量,即 EncryptTable,以及与之相关的配置类 EncryptRuleConfiguration 之间的关系。
我们先来看 EncryptRuleConfiguration,内部包含了两部分内容:
private final Map<String, EncryptorRuleConfiguration> encryptors; private final Map<String, EncryptTableRuleConfiguration> tables;
而在 EncryptTableRuleConfiguration 内部,同样保存着一个 EncryptColumnRuleConfiguration 列表,如下所示:
private final Map<String, EncryptColumnRuleConfiguration> columns = new LinkedHashMap<>();
我们再来看 EncryptColumnRuleConfiguration 的数据结构,如下所示:
public final class EncryptColumnRuleConfiguration { //存储明文的字段 private final String plainColumn; //存储密文的字段 private final String cipherColumn; //辅助查询字段 private final String assistedQueryColumn; //加密器名字 private final String encryptor; }
终于,我们在这里看到了指定存放明文的 plainColumn、存放密文的 cipherColumn,以及加密器 encryptor 等信息。
我们可以回顾案例中的相关配置项来加深理解:
spring.shardingsphere.encrypt.tables.encrypt_user.columns.user_name.plainColumn=user_name_plain spring.shardingsphere.encrypt.tables.encrypt_user.columns.user_name.cipherColumn=user_name spring.shardingsphere.encrypt.tables.encrypt_user.columns.user_name.encryptor=name_encryptor
我们回到最上层的 EncryptRule,发现它的构造函数如下所示:
public EncryptRule(final EncryptRuleConfiguration encryptRuleConfig) { this.ruleConfiguration = encryptRuleConfig; Preconditions.checkArgument(isValidRuleConfiguration(), "Invalid encrypt column configurations in EncryptTableRuleConfigurations."); initEncryptors(encryptRuleConfig.getEncryptors()); initTables(encryptRuleConfig.getTables()); }
上述 initEncryptors 方法就是初始化加解密器 Encryptor,而 initTables 方法会根据 EncryptRuleConfiguration 中的 EncryptTableRuleConfiguration 来初始化 EncryptTable。这里的 EncryptTable 更多是一种中间领域模型,用于简化对各种配置信息的处理,其内部保存着一个 EncryptColumn 列表,如下所示:
private final Map<String, EncryptColumn> columns;
而这个 EncryptColumn 中的变量则跟前面介绍的 EncryptColumnRuleConfiguration 一样,包含了存放明文的 plainColumn、存放密文的 cipherColumn,以及加密器 encryptor 等信息。
在了解了 EncryptRule 中所持有的数据模型之后,我们就可以来看一下 EncryptDataSource,在 EncryptDataSource 的构造函数中使用到了 EncryptRule,如下所示:
private final EncryptRuntimeContext runtimeContext;
public EncryptDataSource(final DataSource dataSource, final EncryptRule encryptRule, final Properties props) throws SQLException { super(dataSource); runtimeContext = new EncryptRuntimeContext(dataSource, encryptRule, props, getDatabaseType()); }
可以看到所传入的 EncryptRule 和 Properties 是用来构建一个 EncryptRuntimeContext,该类继承自 AbstractRuntimeContext 类,而 EncryptRuntimeContext 内部主要保存了用于描述表元数据的 TableMetas 数据结构。
基于改写引擎的数据脱敏实现方案
我们知道 EncryptDataSource 继承了适配器类 AbstractDataSourceAdapter,而它的作用就是生成 EncryptConnection。而对于 EncryptConnection,我们同样也明确它的职责是创建各种 EncryptStatement 和 EncryptPreparedStatement,如下所示:
@Override public Statement createStatement() throws SQLException { return new EncryptStatement(this); }
@Override public PreparedStatement prepareStatement(final String sql) throws SQLException { return new EncryptPreparedStatement(this, sql); }
然后,我们再快速来到 EncryptStatement,来看它的 executeQuery 方法,如下所示:
@Override public ResultSet executeQuery(final String sql) throws SQLException { if (Strings.isNullOrEmpty(sql)) { throw new SQLException(SQLExceptionConstant.SQL_STRING_NULL_OR_EMPTY); } //获取改写后的 SQL 并执行 ResultSet resultSet = statement.executeQuery(getRewriteSQL(sql)); this.resultSet = new EncryptResultSet(connection.getRuntimeContext(), sqlStatementContext, this, resultSet); return this.resultSet; }
显然这里需要重点关注的是 getRewriteSQL 方法,该方法用于获取改写后的 SQL,如下所示:
private String getRewriteSQL(final String sql) { //通过 ParseEngine 对 SQL 进行解析 SQLStatement sqlStatement = connection.getRuntimeContext().getParseEngine().parse(sql, false); //获取关系元数据 RelationMetas RelationMetas relationMetas = getRelationMetas(connection.getRuntimeContext().getTableMetas()); //构建 SQLStatementContext sqlStatementContext = SQLStatementContextFactory.newInstance(relationMetas, sql, Collections.emptyList(), sqlStatement); //构建 SQLRewriteContext SQLRewriteContext sqlRewriteContext = new SQLRewriteContext(relationMetas, sqlStatementContext, sql, Collections.emptyList()); //判断是否根据数据脱敏列进行查询 boolean isQueryWithCipherColumn = connection.getRuntimeContext().getProps().getValue(ShardingPropertiesConstant.QUERY_WITH_CIPHER_COLUMN); //构建 EncryptSQLRewriteContextDecorator 对 SQLRewriteContext 进行装饰 new EncryptSQLRewriteContextDecorator(connection.getRuntimeContext().getRule(), isQueryWithCipherColumn).decorate(sqlRewriteContext); //生成 SQLTokens sqlRewriteContext.generateSQLTokens(); //使用 DefaultSQLRewriteEngine 进行改写 String result = new DefaultSQLRewriteEngine().rewrite(sqlRewriteContext).getSql(); //打印结果 showSQL(result); //返回结果 return result; }
这个方法的部分代码有一种让人似曾相识的感觉,我们回想一下 “20 | 改写引擎:如何理解装饰器模式下的 SQL 改写实现机制?” 中介绍的 BaseShardingEngine的rewriteAndConvert 方法,也看到过 isQueryWithCipherColumn 判断,以及 EncryptSQLRewriteContextDecorator,当时我们没有具体展开,今天就来一起看一下。
1.EncryptSQLRewriteContextDecorator
EncryptSQLRewriteContextDecorator 实现如下所示:
public final class EncryptSQLRewriteContextDecorator implements SQLRewriteContextDecorator {
private final EncryptRule encryptRule;
private final boolean isQueryWithCipherColumn;
@Override
public void decorate(final SQLRewriteContext sqlRewriteContext) {
//参数改写
for (ParameterRewriter each : new EncryptParameterRewriterBuilder(encryptRule, isQueryWithCipherColumn).getParameterRewriters(sqlRewriteContext.getRelationMetas())) {
if (!sqlRewriteContext.getParameters().isEmpty() && each.isNeedRewrite(sqlRewriteContext.getSqlStatementContext())) {
each.rewrite(sqlRewriteContext.getParameterBuilder(), sqlRewriteContext.getSqlStatementContext(), sqlRewriteContext.getParameters());
}
}
//SQLTokenGenerator 初始化
sqlRewriteContext.addSQLTokenGenerators(new EncryptTokenGenerateBuilder(encryptRule, isQueryWithCipherColumn).getSQLTokenGenerators());
}
}
我们还是来对比 ShardingSQLRewriteContextDecorator 类,会发现它与 EncryptSQLRewriteContextDecorator 类的结构完全一致。区别在于这里创建的 ParameterRewriterBuilder 和 SQLTokenGeneratorBuilder 分别是 EncryptParameterRewriterBuilder 和 EncryptTokenGenerateBuilder,而不是ShardingParameterRewriterBuilder 和 ShardingTokenGenerateBuilder。但这两组类的内部结构同样是完全一致的。
在 EncryptParameterRewriterBuilder 内部,同样使用如下方法获取一组 ParameterRewriter:
private Collection getParameterRewriters() { Collection result = new LinkedList<>(); result.add(new EncryptAssignmentParameterRewriter()); result.add(new EncryptPredicateParameterRewriter()); result.add(new EncryptInsertValueParameterRewriter()); return result; }
接下来,我们先以 EncryptAssignmentParameterRewriter 为例来看用于数据脱敏的具体 ParameterRewriter 的实现机制。
2.EncryptAssignmentParameterRewriter
EncryptAssignmentParameterRewriter 类完成在数据脱敏场景下对参数赋值过程的改写。我们首先注意到 EncryptAssignmentParameterRewriter 中存在一个 isNeedRewriteForEncrypt 方法用于判断是否需要改写。
@Override protected boolean isNeedRewriteForEncrypt(final SQLStatementContext sqlStatementContext) { return sqlStatementContext.getSqlStatement() instanceof UpdateStatement || sqlStatementContext instanceof InsertSQLStatementContext && sqlStatementContext.getSqlStatement().findSQLSegment(SetAssignmentsSegment.class).isPresent(); }
这里的判断条件有两个,一个是 UpdateStatement,一个是 InsertSQLStatementContext(且其中的 SQLStatement 中包含 SetAssignmentsSegment)。我们知道在 SQL 语法中,INSERT 和 UPDATE 语句中都具有如下所示的 SET 赋值部分:
SET userId = 1, task_name = 'taskName'
EncryptAssignmentParameterRewriter 类针对的就是这种场景。我们来看它的 Rewrite 核心方法,如下所示:
@Override public void rewrite(final ParameterBuilder parameterBuilder, final SQLStatementContext sqlStatementContext, final List