数据安全之数据库字段加解密检索和前端返回脱敏?看看我这个最强解决方案
数据安全之数据库字段加解密检索和前端返回脱敏?看看我这个最强解决方案前言
数据安全一直是我们老生常谈的话题了,随着国产化的日渐推进和数字化信息改革,数据安全越来越被人们所重视。数据库作为存储、管理和检索数据的核心基础设施,其中可能包含着大量的敏感信息,如个人手机号、身份证号码、银行账户、家庭地址等信息。为了保障这些敏感信息在部分情况下被明文泄露和未授权访问等恶意行为的侵害,数据库字段敏感信息加密变得至关重要。但是数据库列一旦加密那么就牵扯到很多问题。如何对数据库字段进行加密变得非常重要,目前主要有两个解决方案:
[*]数据库自带加密函数或者使用数据库自定义函数方法进行加密解密
[*]使用应用代码比如java、c#等语言自带的加密解密函数库
为了助力国产化的推进下面我将用solon + easy-query对其进行实践演练和原理进行解析。
当前项目地址demo https://gitee.com/xuejm/solon-encrypt
方法优点缺点数据库函数对实现简单,占用磁盘空间少,由数据库自行实现模糊搜索效率低,与数据库函数绑定,兼容性差,仅可以使用数据库提供的函数或者自行编程数据库支持的加密解密java代码实现复杂,占用磁盘空间多模糊搜索效率高,不与数据库函数绑定,兼容性好,可以自行扩展实现国密等对称非对称加密solon
文档地址 https://xuejm.gitee.io/easy-query-doc/
GITHUB地址 https://github.com/noear/solon
GITEE地址 https://gitee.com/noear/solon
easy-qeury
文档地址 https://xuejm.gitee.io/easy-query-doc/
GITHUB地址 https://github.com/xuejmnet/easy-query
GITEE地址 https://gitee.com/xuejm/easy-query
数据库处理
这边我们以mysql为例实现数据库函数的加密解密,对数据库列进行数据保护处理。
方法默认值to_base64(AES_ENCRYPT('手机号值'),'秘钥')将数据进行aes加密,然后进行base64编码AES_DECRYPT(from_base64('手机号列'),'秘钥')将数据进行base64解码,然后进行aes进行解密这两个方法其实很好理解,就是通过调用数据库函数让其在数据库层面就实现了加密和解密,应用程序获取到的数据本身就是解密好了的,但是缺点就是如果需要支持列的like搜索性能会变得非常低下,因为需要对加密列进行AES_DECRYPT(from_base64('手机号列'),'秘钥')的解密处理
select id,name,AES_DECRYPT(from_base64('phone'),'秘钥') as `phone` from user where AES_DECRYPT(from_base64('phone'),'秘钥') like '%567%'当数据量少的时候那么数据库是比较轻松并且相对性能是可以接受的,但是随着数据量的越来越大,这种sql会慢慢变成瓶颈,那么是否有一种方案可以兼顾两者呢,其实是有的.
应用代码处理
分段加密,采用分段加密可以实现like语句的模糊搜索,并且支持数据的加密,但是这种方案也有缺点就是会比较占用空间,具体原理可以看下《阿里巴巴密文字段检索方案》https://jaq-doc.alibaba.com/docs/doc.htm?treeId=1&articleId=106213&docType=1 文章给出了具体的实现方式,约定最少4位数字或者2位中文字符4位英文字符(半角),2个中文字符(全角),比如12345678901这么一串
分别对其进行分段分成8份,并且对每一份进行等长数据加密,也就是加密后的结果需要等长,比如都是16位或者都是8位
算法/模式/填充16 字节加密后数据长度不满 16 字节加密后长度本次采用AES/CBC/NoPadding16不支持❌AES/CBC/PKCS5Padding3216✅AES/CBC/ISO10126Padding3216❌AES/CFB/NoPadding16原始数据长度❌AES/CFB/PKCS5Padding3216❌AES/CFB/ISO10126Padding3216❌AES/ECB/NoPadding16不支持❌AES/ECB/PKCS5Padding3216❌AES/ECB/ISO10126Padding3216❌AES/OFB/NoPadding16原始数据长度❌AES/OFB/PKCS5Padding3216❌AES/OFB/ISO10126Padding3216❌AES/PCBC/NoPadding16不支持❌AES/PCBC/PKCS5Padding3216❌AES/PCBC/ISO10126Padding3216❌加密
我们可以选择任意一种这次选择AES/CBC/PKCS5Padding 因为我们的数据原因最终肯定不满16字节,所以加密后肯定都是16字节长度,然后可以对其进行base64编码,编码后会变成24字节在对其进行合并存储
base64(aes('1234'))+base64(aes('2345'))+base64(aes('3456'))+......+base64(aes('7890'))+base64(aes('8901')),然后存入数据库,之后需要对其like的话限制最小like的数据应该满足最少4位数字或者2位中文字符4位英文字符(半角),2个中文字符(全角),比如我要查询包含45678的手机号只需要现对其进行分段然后对其进行加密base64(aes('4567'))+base64(aes('5678'))
解密
因为我们采用aes加密后用base64编码拼接存入数据库,所以我们只需要对数据库的数据进行获取,之后判断其长度%24是否余数为0,如果是的话那么就将其进行以每24个长度为一组进行base64解码,然后通过aes解密.解密后在对其进行拼接还原出最初的明文数据
限制或缺点
[*]通过上述可以知晓解密片段必须小于16字节长度base64后的加密信息必须是定长
[*]字段会扩大,原本的n位明文如果需要支持加密那么将会让字段变得非常长,但是好处是支持非常高性能的like搜索
[*]建议使用到定长的数据信息中,譬如手机号,身份证号码登
[*]数据库函数加密解密不支持like操作符需要注意
[*]不同的数据库需要适配不同的函数
有点
[*]数据库函数实现简单
[*]支持任意存储对象比如es
select * from user where phone like '%xxxxx%' -- 其中xxxxx就是base64(aes('4567'))+base64(aes('5678'))这样我们就实现了即支持加密又支持like的方式了,但是对于大部分用户来说虽然原理有了,但是实现起来还是太麻烦了,所以接下来我们就助力国产配合国产web框架和国产orm实现这两个功能,让用户在使用时无感知
实践案例
添加依赖
新建solon的web项目并且添加依赖
<dependencies>
<dependency>
<groupId>com.easy-query</groupId>
<artifactId>sql-solon-plugin</artifactId>
<version>1.3.18</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.31</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon.logging.simple</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
</dependencies>配置
在resources目录下新建一个app.yml文件
# 添加配置文件
db1:
jdbcUrl: jdbc:mysql://127.0.0.1:3306/solon_encrypt_db?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
easy-query:
# 配置自定义日志
# log-class: ...
db1:
# 支持mysql pgsql h2 mssql dameng mssql_row_number kingbase_es等其余数据库在适配中
database: mysql
# 支持underlined default lower_camel_case upper_camel_case upper_underlined
name-conversion: underlined
default-track: true
# 记录器级别的配置示例
solon.logging.logger:
"root": #默认记录器配置
level: TRACE
"com.zaxxer.hikari":
level: WARN具体更多参数请参考solon官方文档 和easy-query官方文档
启动类
WebApp.java
public class WebApp {
public static void main(String[] args) {
Solon.start(WebApp.class,args);
}
}加密策略
//支持like的java方法加密解密
public class JavaEncryptionStrategy extends AbstractAesBase64EncryptionStrategy {
@Override
public String getIv() {
return "1234567890123456";
}
@Override
public String getKey() {
return "1234561234567890";
}
}
//数据库函数加密解密
public class MySQLAESColumnValueSQLConverter implements ColumnValueSQLConverter {
/**
* 数据加密秘钥
*/
private static final String SECRET="1234567890123456";
@Override
public void columnConvert(TableAvailable table, ColumnMetadata columnMetadata, SQLPropertyConverter sqlPropertyConverter, QueryRuntimeContext runtimeContext) {
sqlPropertyConverter.sqlNativeSegment("AES_DECRYPT(from_base64({0}),{1})",context->{
context
.expression(columnMetadata.getPropertyName())//采用变量是因为可能出现join附带别名所以需要变量
.value(SECRET)
.setAlias(columnMetadata.getName());
});
}
@Override
public void valueConvert(TableAvailable table, ColumnMetadata columnMetadata, SQLParameter sqlParameter, SQLPropertyConverter sqlPropertyConverter, QueryRuntimeContext runtimeContext) {
sqlPropertyConverter.sqlNativeSegment("to_base64(AES_ENCRYPT({0},{1}))",context->{
context.value(sqlParameter).value(SECRET);
});
}
}数据库对象
//用户信息表
@Data
@Table("sys_user")
public class SysUser {
@Column(primaryKey = true)
private String id;
private String name;
@Column(sqlConversion = MySQLAESColumnValueSQLConverter.class)
private String phone;
@Encryption(strategy = JavaEncryptionStrategy.class,supportQueryLike = true)
private String Address;
private LocalDateTime createTime;
@Navigate(value = RelationTypeEnum.OneToMany,targetProperty = "userId")
private List<UserBook> books;
}
//为了演示复杂查询这边在新增一张用户书本表
@Data
@Table("user_book")
public class UserBook {
@Column(primaryKey = true)
private String id;
private String userId;
private String name;
}数据库脚本
CREATE DATABASE IF NOT EXISTS solon_encrypt_db CHARACTER SET 'utf8mb4';
create table solon_encrypt_db.sys_user
(
id varchar(32) not null comment '主键ID'primary key,
name varchar(50) not null comment '姓名',
phone varchar(256) null comment '手机号',-- 手机号不需要模糊搜索
address varchar(512) null comment '用户地址',-- 用户地址需要模糊搜索
create_time datetime not null comment '创建时间'
)comment '用户表';
create table solon_encrypt_db.user_book
(
id varchar(32) not null comment '主键ID'primary key,
user_id varchar(32) not null comment '姓名',
name varchar(50) not null comment '姓名'
)comment '用户书本表';配置文件
@Configuration
public class DefaultConfiguration {
@Bean(name = "db1",typed=true)
public DataSource db1DataSource(@Inject("${db1}") HikariDataSource dataSource){
return dataSource;
}
@Bean
public void db1QueryConfiguration(@Db("db1") QueryConfiguration configuration){
configuration.applyEncryptionStrategy(new JavaEncryptionStrategy());
configuration.applyColumnValueSQLConverter(new MySQLAESColumnValueSQLConverter());
}
}测试
新增控制器
@Controller
@Mapping("/test")
public class TestController {
@Db
private EasyQuery easyQuery;
@Mapping(value = "/init",method = MethodType.GET)
@Tran
public String init(){
{
SysUser sysUser = new SysUser();
sysUser.setId("1");
sysUser.setName("用户1");
sysUser.setPhone("12345678901");
sysUser.setAddress("浙江省绍兴市越城区城市广场1234号");
sysUser.setCreateTime(LocalDateTime.now());
ArrayList<UserBook> userBooks = new ArrayList<>();
UserBook userBook = new UserBook();
userBook.setId("1");
userBook.setUserId("1");
userBook.setName("语文");
userBooks.add(userBook);
UserBook userBook1 = new UserBook();
userBook1.setId("2");
userBook1.setUserId("1");
userBook1.setName("数学");
userBooks.add(userBook1);
easyQuery.insertable(sysUser).executeRows();
easyQuery.insertable(userBooks).executeRows();
}
{
SysUser sysUser = new SysUser();
sysUser.setId("2");
sysUser.setName("用户2");
sysUser.setPhone("19012345678");
sysUser.setAddress("浙江省杭州市上城区武林广场1234号");
sysUser.setCreateTime(LocalDateTime.now());
ArrayList<UserBook> userBooks = new ArrayList<>();
UserBook userBook = new UserBook();
userBook.setId("3");
userBook.setUserId("2");
userBook.setName("语文");
userBooks.add(userBook);
UserBook userBook1 = new UserBook();
userBook1.setId("4");
userBook1.setUserId("2");
userBook1.setName("英语");
userBooks.add(userBook1);
easyQuery.insertable(sysUser).executeRows();
easyQuery.insertable(userBooks).executeRows();
}
return "初始化完成";
}
}
==> Preparing: INSERT INTO `sys_user` (`id`,`name`,`phone`,`address`,`create_time`) VALUES (?,?,to_base64(AES_ENCRYPT(?,?)),?,?)
==> Parameters: 1(String),用户1(String),12345678901(String),1234567890123456(String),miaKEctf5bGBi4yFHvSV6A==i9CdpEU+Ji+g0pPYOpTcWA==9RprkhoOPwcA13Ye0eE0NA==f0ryEfO7ajP2qQ9Yia/dwA==bFZZS42+JmMlvK+6t9a2xQ==O+TkblfoJWgGu6o/w3RuBQ==urDZztVNP45UWWQrQsneOg==+n2a0u3gq1V4L8aKa/eyEg==8u/RP9cyz8l7udgay5Tbnw==oLi10kERsXzxuJdSFAZN9w==Sgm9i3O/7FtvC4ryFziNug==9gkm5m1HD8qS4ITJ0r/W4A==zppH8USinNqLsEPxJ2jfiQ==RY3Ji2Exl1StrrdrzSVvDQ==lMnY0leaGzXqeK/mukEIQQ==NlthvCsk4jaQkEioF/SWsA==(String),2023-08-13T09:17:01.503(LocalDateTime)
<== Total: 1
==> Preparing: INSERT INTO `user_book` (`id`,`user_id`,`name`) VALUES (?,?,?)
==> Parameters: 1(String),1(String),语文(String)
<== Total: 1
==> Preparing: INSERT INTO `user_book` (`id`,`user_id`,`name`) VALUES (?,?,?)
==> Parameters: 2(String),1(String),数学(String)
<== Total: 1
==> Preparing: INSERT INTO `sys_user` (`id`,`name`,`phone`,`address`,`create_time`) VALUES (?,?,to_base64(AES_ENCRYPT(?,?)),?,?)
==> Parameters: 2(String),用户2(String),19012345678(String),1234567890123456(String),miaKEctf5bGBi4yFHvSV6A==i9CdpEU+Ji+g0pPYOpTcWA==JdzWF3gRqqCuHO+fiRTsGQ==Ydc2v/Ghy3MbHTvTiLqHIg==B9zPkalGKbJMzyFgw8W6bA==yIJYfG5BGqQnnR5+GhdV4g==V7Zu1p3qHPjOBj+vAc1MQA==+n2a0u3gq1V4L8aKa/eyEg==MEsrlm3QnRdt4entjjf97w==rBJCNrGBSjKI6T77OXD2dg==k75blBdYdH81FSIB4AVjeA==9gkm5m1HD8qS4ITJ0r/W4A==zppH8USinNqLsEPxJ2jfiQ==RY3Ji2Exl1StrrdrzSVvDQ==lMnY0leaGzXqeK/mukEIQQ==NlthvCsk4jaQkEioF/SWsA==(String),2023-08-13T09:17:01.775(LocalDateTime)
<== Total: 1
==> Preparing: INSERT INTO `user_book` (`id`,`user_id`,`name`) VALUES (?,?,?)
==> Parameters: 3(String),2(String),语文(String)
<== Total: 1
==> Preparing: INSERT INTO `user_book` (`id`,`user_id`,`name`) VALUES (?,?,?)
==> Parameters: 4(String),2(String),英语(String)
<== Total: 1按用户加密地址模糊匹配
@Mapping(value = "/query",method = MethodType.GET)
public Object query(){
List<SysUser> list = easyQuery.queryable(SysUser.class)
.include(o -> o.many(SysUser::getBooks))
.toList();
return list;
}
[{"id":"1","name":"用户1","phone":"12345678901","Address":"浙江省绍兴市越城区城市广场1234号","createTime":1691889422000,"books":[{"id":"1","userId":"1","name":"语文"},{"id":"2","userId":"1","name":"数学"}]},{"id":"2","name":"用户2","phone":"19012345678","Address":"浙江省杭州市上城区武林广场1234号","createTime":1691889422000,"books":[{"id":"3","userId":"2","name":"语文"},{"id":"4","userId":"2","name":"英语"}]}] @Mapping(value = "/initSM4",method = MethodType.GET) @Tran public String initSM4(){ { SysUserSM4 sysUser = new SysUserSM4(); sysUser.setId("5"); sysUser.setName("用户5"); sysUser.setPhone("12345678901"); sysUser.setAddress("浙江省绍兴市越城区城市广场1234号"); sysUser.setCreateTime(LocalDateTime.now()); easyQuery.insertable(sysUser).executeRows(); } { SysUserSM4 sysUser = new SysUserSM4(); sysUser.setId("6"); sysUser.setName("用户6"); sysUser.setPhone("19012345678"); sysUser.setAddress("浙江省杭州市上城区武林广场1234号"); sysUser.setCreateTime(LocalDateTime.now()); easyQuery.insertable(sysUser).executeRows(); } return "初始化完成"; }==> Preparing: INSERT INTO `sys_user` (`id`,`name`,`phone`,`address`,`create_time`) VALUES (?,?,to_base64(AES_ENCRYPT(?,?)),?,?)==> Parameters: 5(String),用户5(String),12345678901(String),1234567890123456(String),KHxVEDHBxB0x9kgAltKrMA==llZIL8h9i+2b7sPaSt6qpw==/WFPdFPf569dkeGI2Q9r9A==CAvnuJp9Lz30LTVaZi5U5A==JKhjq5f94+MJgJK7Fc4lRA==flZDUtkyeOZJrdUE0DxlZg==jlVLlk9iVJCOCdln+G11Mg==wFIL7wK7nBctC0slOEomrg==zztIbbTcuUyyS+Zj2JgQ1w==X6DWRoQjqCunrA9w6ZlJ3Q==hN0Bm2/qS3XRK2Xxe8/MIw==iDZGTAU/WlMkwLAoiYuh8Q==R4pbp78Ig7qHCLzn9IF7rw==woPrxebr8Xvyo1qG8QxAUA==65pvnL+1Og20OW+xunqHCA==vJKXxvzbvWtZB9hrWrioCg==(String),2023-08-13T22:28:27.204(LocalDateTime) Preparing: INSERT INTO `sys_user` (`id`,`name`,`phone`,`address`,`create_time`) VALUES (?,?,to_base64(AES_ENCRYPT(?,?)),?,?)==> Parameters: 6(String),用户6(String),19012345678(String),1234567890123456(String),KHxVEDHBxB0x9kgAltKrMA==llZIL8h9i+2b7sPaSt6qpw==66gmuLlsoaX1sHDabqd/XA==YGGmh56Hc5MS+Wf8dZdl8w==SjvWmsqOacq5Kui8xDCxxw==cQrhVkPp3Hf5s/GKHpNOaw==BDvJOrbklVQGHodEa+eyCA==wFIL7wK7nBctC0slOEomrg==BZcCFFYJzjQzZ7R23fmOUA==M8WFvyffOu6BeTpQgghhUA==Jw8BjPktNN8CPRyi1f5Vrg==iDZGTAU/WlMkwLAoiYuh8Q==R4pbp78Ig7qHCLzn9IF7rw==woPrxebr8Xvyo1qG8QxAUA==65pvnL+1Og20OW+xunqHCA==vJKXxvzbvWtZB9hrWrioCg==(String),2023-08-13T22:28:27.622(LocalDateTime) Preparing: SELECT `id`,`name`,AES_DECRYPT(from_base64(`phone`),?) AS `phone`,`address`,`create_time` FROM `sys_user` WHERE `id` IN (?,?)==> Parameters: 1234567890123456(String),5(String),6(String) Preparing: SELECT `id`,`name`,AES_DECRYPT(from_base64(`phone`),?) AS `phone`,`address`,`create_time` FROM `sys_user` WHERE `id` IN (?,?) AND `address` LIKE ?==> Parameters: 1234567890123456(String),5(String),6(String),%M8WFvyffOu6BeTpQgghhUA==Jw8BjPktNN8CPRyi1f5Vrg==iDZGTAU/WlMkwLAoiYuh8Q==%(String)
页:
[1]