需求背景
有一个业务线有敏感字段未加密,特对此字段加密。通过方案选型最终确定使用sharding加密
结合自身需求盘点所需改动点
- 当前服务是手动注入的,了解到sharding加密需要使用sharding数据源 所以决定改写成自动注入数据源等
- 数据源改造完成后只需要配置sharding相关配置即可实现
改造过程:
之前是这样注入的:
方案一
1.删除相关手动注入的代码
2.新增依赖
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
<--项目原来的依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
</dependency>
3.启动类上新加:
配置的是和mapper.xml影射的interface **mapper的packages
如何寻找呢?=》xml文件中:namespace的package
@MapperScan(basePackages = {
"com.****.dao",
"com.****.dao1"
})
4.通过配置一些参数和sharding交流
spring.shardingsphere.datasource.name=ds
spring.shardingsphere.datasource.ds.type=org.apache.commons.dbcp2.BasicDataSource
spring.shardingsphere.datasource.ds.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds.url=jdbc:mysql://127.0.0.1:3306/encrypt?serverTimezone=UTC&useSSL=false
spring.shardingsphere.datasource.ds.username=root
spring.shardingsphere.datasource.ds.password=
spring.shardingsphere.datasource.ds.max-total=100
spring.shardingsphere.encrypt.encryptors.encryptor_aes.type=aes
spring.shardingsphere.encrypt.encryptors.encryptor_aes.props.aes.key.value=123456
spring.shardingsphere.encrypt.tables.t_order.columns.user_id.plainColumn=user_decrypt
spring.shardingsphere.encrypt.tables.t_order.columns.user_id.cipherColumn=user_encrypt
spring.shardingsphere.encrypt.tables.t_order.columns.user_id.assistedQueryColumn=user_assisted
spring.shardingsphere.encrypt.tables.t_order.columns.user_id.encryptor=encryptor_aes
spring.shardingsphere.props.sql.show=true
spring.shardingsphere.props.query.with.cipher.column=true
4.1 yml or apollo等一切你配置的地方
- spring.shardingsphere.datasource.<data-source-name>.type= #数据库连接池类名称
- spring.shardingsphere.datasource.<data-source-name>.driver-class-name= #数据库驱动类名
- spring.shardingsphere.datasource.<data-source-name>.url= #数据库url连接
- spring.shardingsphere.datasource.<data-source-name>.username= #数据库用户名
- spring.shardingsphere.datasource.<data-source-name>.password= #数据库密码
- spring.shardingsphere.datasource.<data-source-name>.xxx= #数据库连接池的其它属性
- spring.shardingsphere.encrypt.encryptors.encryptor_aes.type=#加密类型
- spring.shardingsphere.encrypt.encryptors.encryptor_aes.props.aes.key.value=#加密秘钥
- spring.shardingsphere.encrypt.tables.<table-name>.columns.<col_name>.plainColumn=#明文列
- spring.shardingsphere.encrypt.tables.<table-name>.columns.<col_name>.cipherColumn=#加密列
- spring.shardingsphere.encrypt.tables.<table-name>.columns.<col_name>.encryptor=#加密算法
- spring.shardingsphere.props.sql.show=#是否显示sql true/false
- spring.shardingsphere.props.sql.simple=#是否显示简单的重写后sql true/false
- spring.shardingsphere.props.query.with.cipher.column=#是否使用脱敏列查询 true/false
4.2 sharding中针对以下数据类型没有处理
-
java.time.LocalDateTime
-
java.time.LocalDate
-
java.time.LocalTime
配置yml =>你的自定以的时间类型处理的类的package
mybatis-plus:
type-handlers-package: com.**.db.config
需要自定义对应类型的处理器
@Component
@MappedTypes(LocalDateTime.class)
@MappedJdbcTypes(value = JdbcType.DATE, includeNullJdbcType = true)
public class LocalDateTimeTypeHandler extends BaseTypeHandler<LocalDateTime> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType)
throws SQLException {
ps.setTimestamp(i, Timestamp.valueOf(parameter));
}
@Override
public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
Timestamp timestamp = rs.getTimestamp(columnName);
return getLocalDateTime(timestamp);
}
@Override
public LocalDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Timestamp timestamp = rs.getTimestamp(columnIndex);
return getLocalDateTime(timestamp);
}
@Override
public LocalDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Timestamp timestamp = cs.getTimestamp(columnIndex);
return getLocalDateTime(timestamp);
}
private static LocalDateTime getLocalDateTime(Timestamp timestamp) {
if (timestamp != null) {
return LocalDateTime.ofInstant(timestamp.toInstant(), ZoneId.systemDefault());
}
return null;
}
}
@Component
@MappedTypes(LocalDate.class)
@MappedJdbcTypes(value = JdbcType.DATE, includeNullJdbcType = true)
public class LocalDateTypeHandler extends BaseTypeHandler<LocalDate> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalDate parameter, JdbcType jdbcType)
throws SQLException {
ps.setDate(i, Date.valueOf(parameter));
}
@Override
public LocalDate getNullableResult(ResultSet rs, String columnName) throws SQLException {
Date date = rs.getDate(columnName);
return getLocalDate(date);
}
@Override
public LocalDate getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Date date = rs.getDate(columnIndex);
return getLocalDate(date);
}
@Override
public LocalDate getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Date date = cs.getDate(columnIndex);
return getLocalDate(date);
}
private static LocalDate getLocalDate(Date date) {
if (date != null) {
return date.toLocalDate();
}
return null;
}
}
@Component
@MappedTypes(LocalTime.class)
@MappedJdbcTypes(value = JdbcType.DATE, includeNullJdbcType = true)
public class LocalTimeTypeHandler extends BaseTypeHandler<LocalTime> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalTime parameter, JdbcType jdbcType)
throws SQLException {
ps.setTime(i, Time.valueOf(parameter));
}
@Override
public LocalTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
Time time = rs.getTime(columnName);
return getLocalTime(time);
}
@Override
public LocalTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Time time = rs.getTime(columnIndex);
return getLocalTime(time);
}
@Override
public LocalTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Time time = cs.getTime(columnIndex);
return getLocalTime(time);
}
private static LocalTime getLocalTime(Time time) {
if (time != null) {
return time.toLocalTime();
}
return null;
}
}
通过配置一些参数和mybatisplus交流
mybatis-plus:
mapper-locations: classpath*:mapper/*Mapper.xml
type-aliases-packages: com.**.entity,com.**.entity1
configuration:
map-underscore-to-camel-case: true
- mapper-locations: classpath*:mapper/*Mapper.xml
- 此处配置的是xml文件路径
- 如何寻找呢?我的xml文件在这里
- type-aliases-packages: com.**.entity,com.**.entity1
- **mapper.xml文件中和数据库中表列相对于的实体类的packages
- 如何寻找呢?可以参考下=》type的package
- configuration:
map-underscore-to-camel-case: true- 驼峰映射开关
mybatis默认是属性名和数据库字段名一一对应的,
即 数据库表列:user_name => 实体类属性:user_name
但是java中一般使用驼峰命名
数据库表列:user_name => 实体类属性:userName
- type-handlers-package: com.**.db.config
我遇到的一些问题
报错“Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required”
刚开始引入的是 druid-spring-boot-starter ,排查发现应该引入 druid =》当时查文档查到的,具体原因忘记记录了(⊙︿⊙) 具体为什么我们引入了这个 没有太仔细排查 欢迎大家指点迷津
<--项目原来依赖starter更换为下面的-->
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>druid-spring-boot-starter</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
报错“java.lang.IllegalStateException: No typehandler found for property addTime”
添加配置参考4.2
报错“Ambiguous mapping. Cannot map”
启动类添加注解:@EnableFeignClients
没有报错,数据可以顺利插入了,但是我发现我的加密列没有写数据
搜索了一圈各个大佬们说 加密重写sql主要流程在这里:org.apache.shardingsphere.underlying.pluggble.prepare.BasePrepareEngine#prepare
搜索了下发现我没有这个类,排查发现:
我实际依赖的是4.0.1版本的sharding-jdbc-spring-boot-starter,该版本没有
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.1.1</version> </dependency>
然后就开始里漫长的debug模式
首先我发现我每次创建数据源的时候,创建的都是这个:org.apache.shardingsphere.shardingjdbc.spring.boot.SpringBootConfiguration#shardingDataSource;不是创建的这个:org.apache.shardingsphere.shardingjdbc.spring.boot.SpringBootConfiguration#encryptDataSource
然后我就开始研究这个方法
然后我发现他有一个执行条件:EncryptRuleCondition
debug的时候发现我没有在这个条件里获取不到这两个配置
我的配置是Apollo配置的,本地启动的时候 对于依赖的Apollo本地拉取不到,所以我这个配置不成功后续加上他果然也成功了 哈哈 到这里为止我的本次改造算是成功了
后来上线发现不是上述那样,之所以拉不到远端Apollo配置,是因为没有配置springboot的Apollo配置
apollo.bootstrap.enabled: true
apollo.bootstrap.namespaces: application
启动类上配置的是spring的一个注解,不是springboot的
方案二
Spring Boot配置 :: ShardingSphere
最近在搞这个的专项,突然看到文档里 其实还可以通过Java配置实现数据脱敏,当时为了省事,直接吧所有的都按照配置搞了,没有深入研究原来java配置数据源相关的东西,这次 特意研究了下
很简单的说
只需通过文档在手动java配置数据源的时候,把数据脱敏的规则加载进去。
由于项目中存在sharing多表,所以在这个sharding-datasource构造器里完善加密信息:
@Primary
@Bean("shardingDataSource")
public DataSource shardingDataSource(@Qualifier("originDataSource") DataSource dataSource) throws SQLException {
ShardingRuleConfiguration shardingRuleConfiguration = new ShardingRuleConfiguration();
TableRuleConfiguration tableRuleConfiguration = new TableRuleConfiguration("sharding_tmp_table",
"ds0.original_table1,ds0.original_table2");
ComplexShardingStrategyConfiguration complexShardingStrategyConfiguration =
new ComplexShardingStrategyConfiguration("sharding_col", orderOilShardingAlgorithm);
tableRuleConfiguration.setTableShardingStrategyConfig(complexShardingStrategyConfiguration);
shardingRuleConfiguration.getTableRuleConfigs().addAll(Arrays.asList(tableRuleConfiguration));
shardingRuleConfiguration.getBindingTableGroups().addAll(Arrays.asList("sharding_tmp_table"));
Map<String, DataSource> dataSourceMap = Maps.newHashMap();
dataSourceMap.put("ds0", dataSource);
Properties props = new Properties();
props.setProperty("sql.show", "true");
props.setProperty("query.with.cipher.column", "true");
shardingRuleConfiguration.setEncryptRuleConfig(getEncryptRuleConfiguration(props));
return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfiguration, props);
}
private EncryptRuleConfiguration getEncryptRuleConfiguration(Properties props) {
props.setProperty("aes.key.value", "加密关键字");
EncryptorRuleConfiguration encryptorConfig = new EncryptorRuleConfiguration("PREFIXAES", props);
EncryptColumnRuleConfiguration columnConfig =
new EncryptColumnRuleConfiguration("明文列", "加密列",
"", "加密算法");
EncryptTableRuleConfiguration tableConfig =
new EncryptTableRuleConfiguration(Collections.singletonMap("明文列", columnConfig));
EncryptRuleConfiguration encryptRuleConfig = new EncryptRuleConfiguration();
encryptRuleConfig.getEncryptors().put("加密算法", encryptorConfig);
encryptRuleConfig.getTables().put("加密表", tableConfig);
return encryptRuleConfig;
}
分享的一些sharding上线后遇到的一些运行时exception:
一、mybatis中sql字符串用单引号,不能使用双引号
如果使用双引号,由于sharding语法校验严格,会解析为字段,导致sql解析错误
二、ON DUPLICATE KEY UPDATE语句中不能使用#{},${}
引入sharding数据源后,如果使用mysql 提供的 主键冲突时 进行更新操作的语句 ON DUPLICATE KEY UPDATE
因为数据库被代理,所以在 ON DUPLICATE KEY UPDATE语句中不能使用#{},${}, 需要使用VALUES方法。
三、不支持case when,having,union 等
-
可解析至第一个含表的子查询,在下层嵌套再次找到子查询抛错
子查询不支持聚合函数,不支持含shema的SQL:shardingsphere对sql的访问都是在同一个逻辑schema上
-
不支持CASE WHEN、HAVING、UNION (ALL)、有限支持子查询
-
insert语句中,VALUES语句不支持运算表达式
-
不支持INSERT .. SELECT
-
不支持包含schema的语句
-
脱敏字段无法支持比较操作,如:大于小于、ORDER BY、BETWEEN、LIKE等。
-
脱敏字段无法支持计算操作,如:AVG、SUM以及计算表达式 。
四、Mysql like会导致无法使用脱敏字段
如果对脱敏字段使用like,会导致sharding不会对where条件内的原文字段进行替换为脱敏字段,依然使用原文字段擦汗寻
五、Mysql 方法函数不能省略()
之前会在一些查询语句中 where条件使用函数匹配段时间数据
例如:select * from tab where (current_timestamp - create_time) > 30
例如:select * from tab where (current_timestamp() - create_time) > 30
如果sql中的方法函数省略(),会导致sql解析异常
六、使用between,in等关键字时不能使用DATE_FORMAT函数
-
使用in,between时候,如果使用DATE_FORMAT函数,会导致sql解析异常
-
使用DATE_FORMAT函数时候支持=,>,<等简单比较操作
七、sharding 在insert时候 原有sql有字段值直接写死的情况
-
sharding 在insert时候如果原有sql有字段值直接写死的情况,在编译的时候会导致sql注入的值位置错误,字段位置对应不上的情况
-
解决方法:1. 写死值改为参数注入 2.把加密字段的放在最前面,写死为常量的字段在其后面
八、数据库时间转换LocalDateTime,LocalDate,LocalTime错误
由于目前引入sharding版本,不支持jsr310日期标准,需要通过引入jar形式解决日期类型转换问题(也可参考4.2)
-
引入sharding后,mybatisPlus时间类型转为LocalDateTime,LocalDate,LocalTime时会转换失败,可以引入mybatis-typehandlers-jsr310解决,但是需要在mybatis-plus-boot-starter前引入
【mybatis-typehandlers-jsr310】和【mybatis】在org.apache.ibatis.type路径下的类重复,需保证优先加载前者的类。感兴趣的可以研究下类加载顺序的控制,暴力解决可以把mybatis-plus-boot-starter版本降到2.3.3,但需关注会不会引发其他问题
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-typehandlers-jsr310</artifactId> <version>1.0.2</version> </dependency>
九、使用sharding同时配置分库分表和脱敏时,配置方式和单独脱敏不一样
-
需要配置encryptors的type,不需要配aes.key.value,sdk升级>1.2.11,参考如下:(详细可参考官方文档 方案二)
spring.shardingsphere.sharding.encrypt-rule.encryptors.encryptor_prefixaes.type:加密类型 spring.shardingsphere.sharding.encrypt-rule.tables.cebbank_coupon.columns.phone.plainColumn: ,明文列 spring.shardingsphere.sharding.encrypt-rule.tables.cebbank_coupon.columns.phone.cipherColumn: 加密列 spring.shardingsphere.sharding.encrypt-rule.tables.cebbank_coupon.columns.phone.encryptor: 加密器
十、不支持语法:now() between start and end
-
可修改为 start > now() and end < now()
十一、启动报错:StringToDuration
-
不支持duration对象的配置解析,可升级guava至最新版本
十二、sharding与DruidDataSource的filter有兼容性问题,可能导致NPE
-
酌情删除相关filter配置
你还可以参考以下官方文档:
https://shardingsphere.apache.org/document/legacy/4.x/document/cn/features/orchestration/encrypt/
https://shardingsphere.apache.org/document/legacy/4.x/document/cn/manual/sharding-jdbc/usage/encrypt