learn-tech/专栏/ShardingSphere核心原理精讲-完/30数据脱敏:如何基于改写引擎实现低侵入性数据脱敏方案?.md
2024-10-16 06:37:41 +08:00

27 KiB
Raw Blame History

                        因收到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