
1. 项目概述当分库分表遇上数据安全在分布式数据库架构成为标配的今天ShardingSphere 作为一套开源的分布式数据库中间件其分库分表能力已经被很多团队所熟知和应用。然而随着数据安全法规的日趋严格和业务对隐私保护要求的提升仅仅实现数据的水平拆分已经不够了。我们常常面临一个两难境地业务上需要根据用户ID等敏感字段进行分片路由但这些字段本身又涉及用户隐私不能以明文形式存储。直接加密会导致路由失效不加密则面临合规风险。ShardingSphere 提供的数据加密功能正是为了解决这个“既要分片又要加密”的核心矛盾而设计的。这个功能允许你在数据写入数据库时自动进行加密在查询时自动解密同时对应用程序层完全透明。这意味着你的业务代码可以像操作普通明文数据一样进行CRUD而底层的数据在落盘时已经是密文。更关键的是它支持将明文逻辑列与密文物理列分离并兼容分片键让你能在加密的同时依然保持高效、准确的数据路由能力。本文将结合一个可运行的示例源码深入拆解 ShardingSphere 数据加密的实现原理、配置细节以及在实际开发中那些容易踩坑的地方。2. 核心设计思路与架构解析2.1 透明化加解密的实现模型ShardingSphere 数据加密的核心设计思想是“透明化”。它并不要求开发者修改业务逻辑而是在 SQL 解析和路由之后执行之前插入了一个加密/解密的处理环节。其架构模型主要围绕几个核心概念构建逻辑列与物理列这是理解该功能的基础。在你的业务实体和 SQL 中你操作的是“逻辑列”比如user_name,id_card。而在实际的数据库表结构中存在的是“物理列”。加密功能会在两者之间建立映射关系。例如你可以定义逻辑列id_card对应两个物理列id_card_cipher存储密文和id_card_assisted存储辅助查询列可选。应用程序永远只感知id_card。加密器负责具体的加密和解密算法。ShardingSphere 内置了 AES、MD5 等常用加密器的实现。你需要为每个需要加密的逻辑列配置一个加密器并指定其类型和属性如 AES 的密钥。查询辅助列这是一个精妙的设计用于解决加密后数据查询特别是范围查询和模糊查询的难题。当对加密字段进行LIKE ‘%张%’或BETWEEN ‘100’ AND ‘200’这类操作时直接对密文操作是无意义的。查询辅助列通常用于存储经过特定处理如哈希、前缀保留加密的值专门服务于这类查询场景而主密文列用于存储高强度的加密数据以保证安全。2.2 与分片功能协同工作的原理数据加密功能需要与 ShardingSphere 的核心——分片功能——无缝协同。其协同工作的时序至关重要SQL 解析与路由首先ShardingSphere 会解析你的 SQL并根据分片规则确定数据应该落在哪个库、哪个表。这个阶段分片算法所依据的分片键值必须是明文。因此如果你的分片键恰好是需要加密的字段如user_id就必须在配置中明确指出让系统在路由阶段使用该字段的明文或辅助列进行计算。改写与加密路由确定后ShardingSphere 会改写 SQL。将逻辑列名替换为对应的物理列名密文列。同时将 SQL 中的明文参数值通过配置的加密器进行加密生成密文参数。执行与返回将改写好并参数加密后的 SQL 发送到底层数据库执行。结果解密数据库返回结果集后ShardingSphere 会识别出其中的密文列数据调用对应的解密器进行解密然后将解密后的明文数据组装成结果对象返回给应用程序。整个过程对开发者无感。关键在于配置你需要清晰地告诉 ShardingSphere哪些列要加密、用什么加密、明文和密文列如何映射、分片键如何处理。3. 详细配置与实操要点3.1 环境准备与依赖引入首先我们创建一个 Spring Boot 项目来演示。在pom.xml中引入关键依赖。这里我们使用 ShardingSphere-JDBC 的 Spring Boot Starter它比原生 JDBC 配置更便捷。dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.3.2/version !-- 请使用最新稳定版本 -- /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency接下来准备数据库表。假设我们有一张t_user表其中phone字段需要加密存储。我们设计两个物理列phone_cipher用于存储 AES 加密后的密文。phone_assisted用于存储辅助查询列这里我们用手机号的后4位方便进行LIKE ‘%8888’这类尾部模糊查询。建表语句如下CREATE TABLE t_user ( id bigint NOT NULL, name varchar(45) DEFAULT NULL, phone_cipher varchar(100) DEFAULT NULL, -- 密文列 phone_assisted varchar(10) DEFAULT NULL, -- 辅助查询列 PRIMARY KEY (id) ) ENGINEInnoDB;注意业务概念上的phone列在物理表中并不存在。3.2 YAML 配置深度解析核心配置都在application.yml中。下面是一个完整的、带详细注释的配置示例spring: shardingsphere: datasource: names: ds0 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/demo_ds?serverTimezoneUTCuseSSLfalse username: root password: root rules: encrypt: encryptors: # 定义加密器 aes_encryptor: # 加密器名称可自定义 type: AES # 加密算法类型 props: aes-key-value: 123456abc # AES密钥生产环境务必从安全渠道获取且长度需符合算法要求 assisted_encryptor: # 辅助查询列加密器 type: MD5 # 这里使用MD5仅为示例。实际可根据查询需求选择保留格式加密等。 tables: t_user: # 指定需要加密配置的表 columns: phone: # 逻辑列名即业务代码中使用的列名 cipherColumn: phone_cipher # 对应的密文存储物理列名 encryptorName: aes_encryptor # 使用的加密器 assistedQueryColumn: phone_assisted # 可选辅助查询列物理列名 assistedQueryEncryptorName: assisted_encryptor # 可选辅助查询列加密器 queryWithCipherColumn: true # 关键是否使用密文列进行查询。通常为true。 props: sql-show: true # 开发时开启显示实际执行的SQL便于调试配置项精讲encryptors: 这里定义了两个加密器。aes_encryptor用于主加密强度高assisted_encryptor用于生成辅助查询列的值。注意辅助查询加密器不一定需要可逆因为它只用于查询匹配。cipherColumn与assistedQueryColumn: 明确指出了逻辑列到物理列的映射关系。queryWithCipherColumn: 这个属性至关重要。当设置为true时所有对逻辑列phone的查询条件在生成 SQL 时都会自动指向phone_cipher列并使用加密后的值进行比对。这意味着WHERE phone ‘13800138000’会被改写成WHERE phone_cipher ‘加密后的字符串’。如果设置为false则查询会使用辅助列或在不推荐的情况下要求物理表存在明文列。3.3 业务代码编写与效果验证现在我们可以像操作普通字段一样编写业务代码。创建一个 User 实体类注意其字段对应的是逻辑列。Data TableName(t_user) // 假设使用MyBatis-Plus public class User { private Long id; private String name; private String phone; // 逻辑列对应配置中的 phone }编写一个简单的插入和查询测试SpringBootTest public class EncryptTest { Autowired private UserMapper userMapper; // MyBatis-Plus Mapper Test public void testInsertAndQuery() { User user new User(); user.setId(1L); user.setName(张三); user.setPhone(13800138000); // 插入 userMapper.insert(user); // 此时查看数据库phone_cipher 列存储的是 AES 密文 // phone_assisted 列存储的是 “8000” 的 MD5 值根据辅助加密器而定。 // 查询根据明文手机号查询 User queriedUser userMapper.selectOne( new QueryWrapperUser().eq(phone, 13800138000) ); System.out.println(queriedUser.getName()); // 应输出 “张三” // 这个查询过程对开发者透明框架自动处理了加解密。 } }运行测试并开启sql-show: true你会在日志中看到实际执行的 SQL 已经被改写参数也变成了密文。4. 核心场景与高阶配置实战4.1 分片键字段加密处理这是最具挑战性的场景。假设我们不仅要对user_id加密还要用它作为分片键。配置的关键在于确保分片算法能获得正确的值进行计算。方案一使用辅助查询列作为分片键这是推荐的做法。分片算法不关心真正的数据内容只关心一个确定性的、可用于路由的值。我们可以将user_id_assisted例如对用户ID进行一致性哈希后的值作为分片键。rules: sharding: tables: t_order: actualDataNodes: ds0.t_order_$-{0..1} tableStrategy: standard: shardingColumn: user_id_assisted # 分片键指向辅助列 shardingAlgorithmName: table_inline_mod shardingAlgorithms: table_inline_mod: type: INLINE props: algorithm-expression: t_order_$-{user_id_assisted % 2} encrypt: tables: t_order: columns: user_id: cipherColumn: user_id_cipher encryptorName: aes_encryptor assistedQueryColumn: user_id_assisted # 辅助列同时用于分片 assistedQueryEncryptorName: hash_encryptor queryWithCipherColumn: true方案二在分片算法中集成解密逻辑不推荐另一种思路是自定义一个复杂的分片算法在算法内部先对传入的明文参数进行加密或对密文参数进行解密再用结果值计算分片。这种方法将加密逻辑耦合进了分片算法增加了复杂性和维护成本仅作为理论上的备选。4.2 存量数据加密迁移方案项目上线初期可能没有加密积累了海量明文数据。如何平滑迁移到加密方案双写阶段首先修改数据库表结构增加密文列cipherColumn和辅助列assistedQueryColumn。然后修改 ShardingSphere 配置将queryWithCipherColumn设置为false。这样新写入的数据会同时写入明文列和密文列通过加密器但查询仍走明文列。同时启动一个离线任务将历史明文数据批量加密后更新到新的密文列和辅助列。数据校验与切换待离线任务完成并经过充分的数据一致性校验后修改配置将queryWithCipherColumn设置为true并将业务 SQL 中的逻辑列映射指向密文列。此时所有读写都将基于密文列。清理阶段稳定运行一段时间后确认无误再通过数据库任务删除原来的明文列。整个过程业务停机时间极短主要在切换配置的那一刻。注意迁移过程务必做好完整的数据备份和回滚方案。双写阶段要特别注意对同一行的并发更新可能带来的数据不一致问题。5. 常见问题排查与性能调优5.1 典型错误与解决方案在实际使用中以下几个问题非常常见加密解密失败报错NoSuchColumnException现象应用启动或执行SQL时抛出异常提示找不到某个列。排查立即检查你的 YAML 配置中tables.table-name.columns下的逻辑列名如phone是否与你的 Java 实体类字段名、MyBatis XML/注解中的列名完全一致包括大小写。同时检查物理表是否真的存在配置中指定的cipherColumn和assistedQueryColumn。心得建议在实体类上使用TableField(value “phone”)MyBatis-Plus或Column(name “phone”)JPA显式指定列名避免因命名风格转换如驼峰转下划线导致的意外问题。查询结果为空但数据确实存在现象根据明文条件查询返回空结果但直接连数据库用密文查能查到。排查首先确认queryWithCipherColumn是否为true。如果为true则检查你的WHERE条件。ShardingSphere 的加密查询仅支持等值查询。对于LIKE,IN,BETWEEN等非等值查询必须依赖assistedQueryColumn并配置对应的辅助查询加密器。如果你的查询是WHERE phone LIKE ‘%138%’而你没有配置辅助列或配置不当查询必然会失败。解决方案为非等值查询场景设计辅助列。例如对于手机号可以存储一个“手机号后4位”的哈希值作为辅助列查询时业务上可以支持WHERE phone LIKE ‘%8888’框架会将其转换为对辅助列的等值查询。但这牺牲了部分查询灵活性。性能明显下降现象开启加密后写操作和某些读操作变慢。分析加解密是CPU密集型操作。AES加密每次写入和读取都需要计算。辅助查询列的增加也意味着索引可能变多或需要联合索引。调优建议选择性加密并非所有字段都需要加密。严格评估数据敏感等级只对真正必要的字段如身份证、手机号、银行卡号启用加密。算法选型在安全性和性能间权衡。AES-256比AES-128更安全但略慢。对于辅助查询列如果不需要可逆使用快速的哈希算法如SHA-256即可。索引优化对辅助查询列建立合适的索引以加速基于该列的查询。避免对长密文列建立索引。5.2 安全配置警示密钥管理是生命线绝对不要将加密密钥如aes-key-value硬编码在配置文件或代码中提交到代码仓库。必须使用安全的密钥管理系统如 Vault、KMS或在生产环境通过环境变量、启动参数等方式注入。辅助列的安全风险辅助查询列虽然方便了查询但其本身可能泄露信息例如手机号后4位。需要评估其是否属于敏感信息。如果敏感则需要使用更强的、不可逆的哈希加盐来处理并确保该列数据无法被轻易反推。加密不能替代其他安全措施数据加密是“最后一道防线”它不能替代访问控制、SQL注入防护、网络传输加密TLS和日志脱敏等安全措施。必须建立纵深防御体系。ShardingSphere 的数据加密功能将数据安全能力无缝集成到了分布式数据访问层为应对数据安全合规要求提供了优雅的解决方案。它的价值在于平衡了安全、性能和开发效率。在实际项目中成功落地的关键不在于技术本身有多复杂而在于对业务查询模式的精准分析从而设计出合理的“逻辑列-密文列-辅助列”映射关系以及一个稳妥的存量数据迁移方案。