SpringBoot+ShardingSphere彻底解决生产环境数据库字段加解密问题
前言
互联网行业公司,对于数据库的敏感字段是一定要进行加密的,方案有很多,最直接的比如写个加解密的工具类,然后在每个业务逻辑中手动处理,在稍微有点规模的项目中这种方式显然是不现实的,不仅工作量大而且后期很难维护。
目前mybatis-plus已经提供了非常好的加解密方案,居士也试过效果很好,但很多互联网公司不一定会引入mybatis-plus作为数据层工具,反而就喜欢使用mybatis,甚至有不少使用SpringDataJPA,那么就没有必要为了加解密专门引入mybatis-plus。
那有什么更合适的方案呢?答案是肯定的,shardingsphere就提供了方案,为什么选择它呢,因为互联网公司大概率会考虑分库分表,目前最佳的分库分表方案实际上也就是shardingsphere了,既然如此,直接用它的数据库加解密方案就不需要再额外引入第三方工具了。
使用
1、案例准备
技术体系如下,其中引入MybatisPlus是为了方便案例更快成型,ShardingSphere不建议使用5.x版本,因为版本差异较大一直都是大家对其诟病的原因,甚至小版本都有不少差异,4.x版本相较来说资料更多。
技术 | 版本 |
---|---|
SpringBoot | 2.6.3 |
MybatisPlus | 3.5.1 |
ShardingSphere | 4.1.1 |
2、引入依赖
<dependencies>
<!-- spring web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- sharding-jdbc -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- 代码生成器 mybatisPlus自带的生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<!-- freemarker模板生成器 引入代码生成器需要 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
<!-- swagger 因为mybatisPlus代码生成器会自带swagger的注解 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
<!-- 启动后加载配置文件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- lombok 简化实体类管理工具-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- fastjson 解析json用到,也可以换成自己喜欢用的 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3、yml配置
server:
port: 8888
# 数据源
spring:
# shardingsphere配置
shardingsphere:
props:
sql:
show: false
query:
with:
cipher:
column: true # 查询是否使用密文列
datasource:
name: master
master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/encrypt_demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
initial-size: 20
max-active: 200
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
# 加密配置
encrypt:
encryptors:
my_encryptor:
type: mySharding # 声明加密处理器的类型,自定义。
props:
aes.key.value: fly13579@# # 加密处理器创建密钥会用到,10个数字英文字母组合,自定义。
# 需要加密哪张表中的哪些字段,每个字段使用哪个加密处理器,这里的my_encryptor就是上面配置的处理器名称。
tables:
tb_order:
columns:
id_card:
cipherColumn: id_card
encryptor: my_encryptor
name:
cipherColumn: name
encryptor: my_encryptor
4、自定义加解密处理器
ShardingSphere有自己的加解密处理器,可以直接使用,生产环境中还是偏向于自定义处理器,因为更安全,不容易被暴力破解。
1)、增加SPI指向
在resources目录下新建META-INF/services目录,编写文件指向自定义的处理器。
文件名称:org.apache.shardingsphere.encrypt.strategy.spi.Encryptor
PS:4.1.1版本和4.0.0版本的名称不同,因为许多类名和包名都更改了。
2)、编写自定义加解密处理器
package com.example.encrypt.config;
import com.google.common.base.Preconditions;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.StringUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.shardingsphere.encrypt.strategy.impl.AESEncryptor;
import org.apache.shardingsphere.encrypt.strategy.spi.Encryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Properties;
/**
* <p>
* Shardingsphere加密处理器
* </p>
*
* @author 福隆苑居士,公众号:【Java分享客栈】
* @since 2022-02-20
*/
@Configuration
public class MyShardingEncryptor implements Encryptor {
private final Logger log = LoggerFactory.getLogger(MyShardingEncryptor.class);
// AES KEY
private static final String AES_KEY = "aes.key.value";
private Properties properties = new Properties();
public MyShardingEncryptor(){
}
@Override
public void init() {
}
@Override
public String encrypt(Object plaintext) {
try {
byte[] result = this.getCipher(1).doFinal(StringUtils.getBytesUtf8(String.valueOf(plaintext)));
log.debug("[MyShardingEncryptor]>>>> 加密: {}", Base64.encodeBase64String(result));
return Base64.encodeBase64String(result);
} catch (Exception ex) {
log.error("[MyShardingEncryptor]>>>> 加密异常:", ex);
}
return null;
}
@Override
public Object decrypt(String ciphertext) {
try {
if (null == ciphertext) {
return null;
} else {
byte[] result = this.getCipher(2).doFinal(Base64.decodeBase64(ciphertext));
log.debug("[MyShardingEncryptor]>>>> 解密: {}", new String(result));
return new String(result);
}
} catch (Exception ex) {
log.error("[MyShardingEncryptor]>>>> 解密异常:", ex);
}
return null;
}
@Override
public String getType() {
return "mySharding"; // 和yml配置一致
}
@Override
public Properties getProperties() {
return this.properties;
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
/**
* 加解密算法
* @param decryptMode 1-加密,2-解密,还有其他类型可以点进去看源码。
*/
private Cipher getCipher(int decryptMode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
Preconditions.checkArgument(this.properties.containsKey("aes.key.value"), "No available secret key for `%s`.",
AESEncryptor.class.getName());
Cipher result = Cipher.getInstance("AES");
result.init(decryptMode, new SecretKeySpec(this.createSecretKey(), "AES"));
return result;
}
/**
* 创建密钥,规则根据自己需要定义。
* -- PS: 生产环境规范要求不能打印出密钥相关日志,以免发生意外泄露情况。
*/
private byte[] createSecretKey() {
// yml中配置的原始密钥
String oldKey = this.properties.get("aes.key.value").toString();
Preconditions.checkArgument(null != oldKey, String.format("%s can not be null.", "aes.key.value"));
/*
* 将原始密钥和自定义的盐一起再次加密生成新的密钥返回.
* 注意,因为我们用的AES加解密方式最终密钥必须16位,否则AES会报错,
* 而application.yml中配置的aes.key.value是10位字符组合,所以这里才substring(0,5),否则最终没有返回16位会抛AES异常,可以自己试验下。
*/
String secretKey = DigestUtils.sha1Hex(oldKey + AES_KEY).toUpperCase().substring(0, 5) + "!" + oldKey;
// 密钥打印在上线前一定要删掉,避免泄露引起安全事故。
log.debug("[MyShardingEncryptor]>>>> 密钥: {}", secretKey);
return secretKey.getBytes();
}
}
5、编写测试接口
package com.example.encrypt.controller;
import com.alibaba.fastjson.JSONObject;
import com.example.encrypt.entity.Order;
import com.example.encrypt.enums.ResponseCodeEnum;
import com.example.encrypt.service.IOrderService;
import com.example.encrypt.util.ResultEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author 福隆苑居士,公众号:【Java分享客栈】
* @since 2022-02-21
*/
@RestController
@RequestMapping("/api/order")
@Slf4j
public class OrderController {
private final IOrderService orderService;
public OrderController(IOrderService orderService) {
this.orderService = orderService;
}
/**
* 查询订单
*/
@GetMapping("/list")
public ResponseEntity<List<Order>> list() {
return ResponseEntity.ok().body(orderService.list());
}
/**
* 插入订单
*/
@PostMapping("/save")
public ResponseEntity<List<Order>> save(@RequestBody Order order) {
// 这里只是简单的演示,正式代码记得不要直接传实体对象,要传递VO对象进行转换,并且要做参数校验。
log.debug("[插入订单]>>>> order={}", JSONObject.toJSONString(order));
order.setCreatedAt(new Date());
order.setUpdatedAt(new Date());
boolean ret = orderService.save(order);
return ret ? ResponseEntity.ok().body(orderService.list())
: ResponseEntity.badRequest().body(new ArrayList<>());
}
}
6、效果
插入订单
我们yml中配置的对姓名和身份证号加密,看下数据库这笔订单记录,发现已经自动加密了。
再试下查询订单,看是否会对数据库加密字段进行解密后返回结果,发现解密也没问题。
7、密钥在数据库的用法
这里专门给大家说明一下密钥如何在数据库中通过SQL直接对字段值加解密,因为有很多公司会使用堡垒机或云桌面来访问生产环境数据库,这个时候排查线上问题时,往往要知道加密字段是什么。
1)、拿到密钥
PS:切记,这个密钥在上线前保留一次即可,打印密钥的日志一定要删掉,可以避免生产环境泄露密钥引起的事故,一般正规点的公司都会有要求。
密钥打印的地方在自定义的加解密处理器中,可以debug打印出来。
2)、数据库中使用
语句如下,保存下来,以后使用时复制出来即可。
# 加密
SELECT to_base64(AES_ENCRYPT('要加密的值','密钥'))
# 解密
SELECT AES_DECRYPT(FROM_BASE64('要解密的值'),'密钥')
# 中文解密,防乱码。
select CAST(AES_DECRYPT(FROM_BASE64('要解密的中文值'),'密钥') as char)
总结
ShardingSphere的加解密本身步骤简单,但这个工具其实不熟悉或者没用过的人会踩很多坑,包括版本差异、未解决的缺陷等等,可是它的优势远大于弊端,所以才会被非常多的公司所接受,也是学习分库分表的必修课。
居士给大家提供了可以直接运行起来的源码以及个人踩坑的小手记,有需要的小伙伴可以下载下来跑起来做做试验。
源码链接会在评论中分享出来哦~
本人原创文章纯手打,如果觉得有一滴滴帮助的话,就请伸出纤纤玉手点个推荐吧~~~
SpringBoot+ShardingSphere彻底解决生产环境数据库字段加解密问题的更多相关文章
- Vue Nginx反向代理配置 解决生产环境跨域
Vue本地代理举例: module.exports = { publicPath: './', devServer: { proxy: { '/api': { target: 'https://mov ...
- 如何将生产环境的字段类型从INT修改为BIGINT
介绍 改变数据类型是一个看起来很简单的事情,但是如果表非常大或者有最小停机时间的要求,又该如何处理那?这里我提供一个思路来解决这个问题. 背景 在一个常规SQL Server heath检查中,使用s ...
- SpringBoot整合Swagger2以及生产环境的安全问题处理
1.创建springboot项目 https://www.cnblogs.com/i-tao/p/8878562.html 这里我们使用多环境配置: application-dev.yml(开发环境) ...
- [转]SpringBoot整合Swagger2以及生产环境的安全问题处理
1.创建springboot项目 https://www.cnblogs.com/i-tao/p/8878562.html 这里我们使用多环境配置: application-dev.yml(开发环境) ...
- Nginx入门及如何反向代理解决生产环境跨域问题
1.Nginx入门与基本操作篇 注:由于服务器是windows系统,所以本文主要讲解Nginx在windows下的操作. 首先下载Nginx 解压缩,我们所有的配置基本都在万能的 nginx/conf ...
- IBM Thread and Monitor Dump Analyzer for Java解决生产环境中的性能问题
这个工具的使用和 HeapAnalyzer 一样,非常容易,同样提供了详细的 readme 文档,这里也简单举例如下: #/usr/java50/bin/java -Xmx1000m -jar jca ...
- Flink 实战:如何解决生产环境中的技术难题?
大数据作为未来技术的基石已成为国家基础性战略资源,挖掘数据无穷潜力,将算力推至极致是整个社会面临的挑战与难题. Apache Flink 作为业界公认为最好的流计算引擎,不仅仅局限于做流处理,而是一套 ...
- 使用Windbg找出死锁,解决生产环境中运行的软件不响应请求的问题
前言 本文介绍本人的一次使用Windbg分析dump文件找出死锁的过程,并重点介绍如何确定线程所等待的锁及判断是否出现了死锁. 对于如何安装及设置Windbg请参考:<使用Windbg和SoS扩 ...
- java实现工程配置文件敏感字段加解密
以下引自他人博客: 1. 需求背景我们在开发应用时,需要连接数据库,一般把数据库信息放在一个属性配置文件中,比如***.properties,具体的内容 #mysql的配置文件jdbc.url=jdb ...
随机推荐
- 使用nexus搭建一个maven私有仓库
使用nexus搭建一个maven私有仓库 大家好,我是程序员田同学.今天带大家搭建一个maven私有仓库. 很多公司都是搭建自己的Maven私有仓库,今天就带大家使用nexus搭建一个自己的私有仓库, ...
- vue3路由的使用,保证你有所收获!
路由变量 有的小伙伴,可能是第一次听见路由变量这个词. 什么是路由变量了,顾名思义就是这个路由地址是动态的,不是固定的. 它的运用场景是哪里呢? 比如说:1.详情页的地址,我们就可以去使用路由变量. ...
- vue组件实现图片的拖拽和缩放
vue实现一个组件其实很简单但是要写出一个好的可复用的组件那就需要多学习和钻研一下,一个好的组件必须有其必不可少的有优点:一是能提高应用开发效率.测试性.复用性等:二是组件应该是高内聚.低耦合的:三是 ...
- Mybatis 学习记录
1.先放上mybatis官网地址: https://mybatis.org/mybatis-3/zh/index.html 2.mybatis源码和有关包下载地址(GitHub): https://g ...
- JUC之线程池的实现原理以及拒绝策略
线程池实现原理 向线程池提交任务后,线程池如何来处理这个任务,之前我们了解了7个参数,我们通过这些参数来串联其线程池的实现原理. 1.在创建了线程池后,开始等待请求 2.当调用execute()方法添 ...
- AOP-基本概念
AOP(概念) 1,什么是AOP (1)面向切面(方面)编程 :利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率. (2)通 ...
- python25day
内容回顾 面向对象的回顾 类 对象 怎么用 怎么继承 pickle模块 今日内容 继承(进阶的知识点) 经典类和新式类 python3所有类都继承object类 所有继承object的类都是新式类 不 ...
- ABC220H - Security Camera
考虑折半,将点按照标号是否 \(\le \frac{n}{2}\) 分成两个集合 \(S_1, S_2\). 首先原问题的形式有点奇怪,我们不妨统计没有被覆盖覆盖的边为偶数条的情况. 这样一来问题转化 ...
- Net6 DI源码分析Part5 在Kestrel内Di Scope生命周期是如何根据请求走的?
Net6 DI源码分析Part5 在Kestrel内Di Scope生命周期是如何根据请求走的? 在asp.net core中的DI生命周期有一个Scoped是根据请求走的,也就是说在处理一次请求时, ...
- uniapp 使用iconfont图标
步骤一 新建项目 步骤二 导入需要的图标,然后下载图标代码 步骤三 打开下载的压缩文件中的iconfont.css 步骤四 复制粘贴到项目中 步骤四在项目中使用 use in page