一、配置文件脱敏
1、概述
核心隐私数据无论对于企业还是用户来说尤其重要,因此要想办法杜绝各种隐私数据的泄漏。对于工程中的敏感信息,与数据库打交道的的工程肯定是配置数据源,尤其是数据库的账号密码
,我们不想把它们明文
写在配置文件里,又想让工程能正确跑起来,那应该怎么做呢?
**由于数据库的敏感性,所以不可能让每个人都知道su ****root
的密码。且数据库中权限、视图这些机制也是为了保证安全性,所以要保证让尽可能少的人知道root
**的密码。所以我们可以利用非对称加密算法的思想:
- 在工程的配置文件中写入加密好的
密文
。 - 启动工程时让知道
密钥
的人,以启动参数的形式将密钥
输入进去。 - 工程根据
密钥
,将密文
自动解密为明文
,用来完成配置。 - 无关的人是无法知道敏感信息的
明文
是什么的
这里我们使用一款开源插件:
jasypt-spring-boot
来进行脱敏处理,项目地址:https:///ulisesbocchio/jasypt-spring-boot
2、利用jar包手动加密
# 使用秘钥和密码明文生成密码
java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input=待加密内容 password=加密秘钥 algorithm=PBEWithMD5AndDES
# 检查生成的密码是否正确
java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringDecryptionCLI input=加密后内容 password=加密秘钥 algorithm=PBEWithMD5AndDES
3、SpringBoot实现脱敏
3.1 代码实现
首先引入依赖
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
然后进行密钥的配置(配置文件),当然将秘钥直接放在配置文件中也是不安全的,我们可以在项目启动的时候配置秘钥java -jar xxx.jar -Djasypt.encryptor.password=dGNtLW1hbmFnZS1zeXN0ZW
jasypt:
encryptor:
password: dGNtLW1hbmFnZS1zeXN0ZW
algorithm: PBEWithMD5AndDES
然后可以对明文进行加密
@SpringBootTest
class DesensitizationApplicationTests {
/**
* 注入加密方法
*/
@Autowired
private StringEncryptor encryptor;
/**
* 手动生成密文,此处演示了url,user,password
*/
@Test
public void encrypt() {
String url = encryptor.encrypt("jdbc\\:mysql\\://127.0.0.1\\:3306/test?useUnicode\\=true&characterEncoding\\=UTF-8&zeroDateTimeBehavior\\=convertToNull&useSSL\\=false&allowMultiQueries\\=true&serverTimezone=Asia/Shanghai");
String name = encryptor.encrypt("root");
String password = encryptor.encrypt("123456");
System.out.println("database url: " + url);
System.out.println("database name: " + name);
System.out.println("database password: " + password);
assert url.length() > 0;
assert name.length() > 0;
assert password.length() > 0;
}
/**
* 第二种手动加密的方法,不过加密解密要配合,与上面的方法不能混合使用
*/
@Test
public void test(){
BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
//加密所需的密钥
textEncryptor.setPassword("shawn");
//要加密的数据(数据库的用户名或密码)
String username = textEncryptor.encrypt("root");
String password = textEncryptor.encrypt("123456");
String url = textEncryptor.encrypt("jdbc:mysql://10.18.104.78:4000/testdb?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true");
String redisHost = textEncryptor.encrypt("172.16.110.85");
//String decrypt = textEncryptor.decrypt("L+tOtB0YuK78F12PtS5c1Q==");
System.out.println("username:"+username);
System.out.println("password:"+password);
System.out.println("url:"+url);
System.out.println("redisHost:"+redisHost);
System.out.println(textEncryptor.decrypt("c9oILGKe4gHAs/FxJEcLug=="));
}
}
然后将加密后的密文写入配置,jasypt
默认使用ENC()
包裹,此时的数据源配置如下
spring:
datasource:
# 数据源基本配置
username: ENC(L8I2RqYPptEtQNL4x8VhRVakSUdlsTGzEND/3TOnVTYPWe0ZnWsW0/5JdUsw9ulm)
password: ENC(EJYCSbBL8Pmf2HubIH7dHhpfDZcLyJCEGMR9jAV3apJtvFtx9TVdhUPsAxjQ2pnJ)
driver-class-name: com.mysql.jdbc.Driver
url: ENC(szkFDG56WcAOzG2utv0m2aoAvNFH5g3DXz0o6joZjT26Y5WNA+1Z+pQFpyhFBokqOp2jsFtB+P9b3gB601rfas3dSfvS8Bgo3MyP1nojJgVp6gCVi+B/XUs0keXPn+pbX/19HrlUN1LeEweHS/LCRZslhWJCsIXTwZo1PlpXRv3Vyhf2OEzzKLm3mIAYj51CrEaN3w5cMiCESlwvKUhpAJVz/uXQJ1spLUAMuXCKKrXM/6dSRnWyTtdFRost5cChEU9uRjw5M+8HU3BLemtcK0vM8iYDjEi5zDbZtwxD3hA=)
type: com.alibaba.druid.pool.DruidDataSource
上述配置是使用默认的prefix=ENC(
、suffix=)
,当然我们可以根据自己的要求更改,只需要在配置文件中更改即可,如下:
jasypt:
encryptor:
## 指定前缀、后缀
property:
prefix: 'PASS('
suffix: ')'
那么此时的配置就必须使用PASS()
包裹才会被解密,如下:
spring:
datasource:
# 数据源基本配置
username: PASS(L8I2RqYPptEtQNL4x8VhRVakSUdlsTGzEND/3TOnVTYPWe0ZnWsW0/5JdUsw9ulm)
password: PASS(EJYCSbBL8Pmf2HubIH7dHhpfDZcLyJCEGMR9jAV3apJtvFtx9TVdhUPsAxjQ2pnJ)
driver-class-name: com.mysql.jdbc.Driver
url: PASS(szkFDG56WcAOzG2utv0m2aoAvNFH5g3DXz0o6joZjT26Y5WNA+1Z+pQFpyhFBokqOp2jsFtB+P9b3gB601rfas3dSfvS8Bgo3MyP1nojJgVp6gCVi+B/XUs0keXPn+pbX/19HrlUN1LeEweHS/LCRZslhWJCsIXTwZo1PlpXRv3Vyhf2OEzzKLm3mIAYj51CrEaN3w5cMiCESlwvKUhpAJVz/uXQJ1spLUAMuXCKKrXM/6dSRnWyTtdFRost5cChEU9uRjw5M+8HU3BLemtcK0vM8iYDjEi5zDbZtwxD3hA=)
type: com.alibaba.druid.pool.DruidDataSource
3.2 运行原理
- **jasypt的加密方式 **
jasypt采用PBEWithMD5AndDES
加密方式,在相同的秘钥情况下,每次生成的密文都不相同,但可以使用秘钥解密获得相同的明文,可以确保秘钥的安全性 - **jasypt-spring-boot-starter的运行原理 **
先使用原先的配置加载方式加载配置信息(因此系统使用何种配置方式,对加解密没有影响),再通过代理的方式代理了配置获取类,在Bean生成时加载配置的地方使用代理类执行密码转换获得明文 - **jasypt-spring-boot-starter工作原理 **
在spring中的加载方式:EnableEncryptablePropertiesConfiguration 类
负责配置文件加载将当前的environment环境配置进行代理或包装(返回EnableEncryptablePropertiesBeanFactoryPostProcessor
)对所有的参数值使用Encryptable进行代理,生成PropertySource
的AOP代理类 并使用EncryptableMapPropertySourceWrapper
类来包装PropertySource 使用的时候通过DefaultPropertyDetector
来判断数据是否符合ENC(**********)
这样的数据结构 符合时,使用EncryptablePropertyResolver
来解码数据
4、其他注意事项说明
对于密文使用,可以支持nacos配置中心,yml等多种配置文件;项目启动参数增加【-Djasypt.encryptor.password=秘钥】 来实现密文解密 ;最后关于加密结果,每次加密的结果都不一样,但使用秘钥都能得到正确的明文,注意加解密一定需要使用jasypt-spring-boot-starter
中引用的jasypt-x.x.x.jar
对应版本,否则可能导致密文无法解析的情况
二、接口返回数据脱敏
1、概述
通常接口返回值中的一些敏感数据也是要脱敏的,因为不脱敏的敏感数据,可能会引起用户的不满。比如身份证号、手机号码、地址等通常的手段就是用*
隐藏一部分数据,当然也可以根据自己需求定制。言归正传,如何优雅的实现呢?有两种实现方案:
- 整合Mybatis插件,在查询的时候针对特定的字段进行脱敏
- 整合Jackson,在序列化阶段对特定字段进行脱敏
这里我们就先使用第二种方法对数据进行脱敏
2、SpringBoot实战
首先定制脱敏策略,针对项目需求,定制不同字段的脱敏规则,比如手机号中间几位用*
替代,不同项目可以根据自己需求进行删减
/**
* 脱敏策略,枚举类,针对不同的数据定制特定的策略
*/
public enum SensitiveStrategy {
/**
* 用户名
*/
USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
/**
* 身份证
*/
ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1****$2")),
/**
* 手机号
*/
PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
/**
* 地址
*/
ADDRESS(s -> s.replaceAll("(\\S{3})\\S{2}(\\S*)\\S{2}", "$1****$2****"));
private final Function<String, String> desensitizer;
SensitiveStrategy(Function<String, String> desensitizer) {
this.desensitizer = desensitizer;
}
public Function<String, String> desensitizer() {
return desensitizer;
}
}
第二步自定义一个脱敏注解,一旦有属性被标注,则进行对应得脱敏
/**
* 自定义jackson注解,标注在属性上
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {
//脱敏策略
SensitiveStrategy strategy();
}
下面将是重要实现,对标注注解@Sensitive
的字段进行脱敏,实现如下,其中createContextual
的作用是通过字段已知的上下文信息定制JsonSerializer
对象
/**
* 序列化注解自定义实现
* JsonSerializer<String>:指定String 类型,serialize()方法用于将修改后的数据载入
*/
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
private SensitiveStrategy strategy;
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(strategy.desensitizer().apply(value));
}
/**
* 获取属性上的注解属性
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
Sensitive annotation = property.getAnnotation(Sensitive.class);
if (Objects.nonNull(annotation)&&Objects.equals(String.class, property.getType().getRawClass())) {
this.strategy = annotation.strategy();
return this;
}
return prov.findValueSerializer(property.getType(), property);
}
}
然后定义Person类,对其数据脱敏,使用注解@Sensitive
注解进行数据脱敏
@Data
public class Person {
/**
* 真实姓名
*/
@Sensitive(strategy = SensitiveStrategy.USERNAME)
private String realName;
/**
* 地址
*/
@Sensitive(strategy = SensitiveStrategy.ADDRESS)
private String address;
/**
* 电话号码
*/
@Sensitive(strategy = SensitiveStrategy.PHONE)
private String phoneNumber;
/**
* 身份证号码
*/
@Sensitive(strategy = SensitiveStrategy.ID_CARD)
private String idCard;
/**
* 昵称
*/
private String nickName;
}
最后模拟接口测试
@RestController
public class TestController {
@GetMapping("/test")
public Person test(){
Person user = new Person();
user.setRealName("接口测试");
user.setPhoneNumber("17683456578");
user.setAddress("浙江省杭州市温州市....");
user.setIdCard("4333333333334334333");
user.setNickName("shawn");
return user;
}
}
输出结果
{
"realName": "接*测试",
"address": "浙江省****市温州市..****",
"phoneNumber": "176****6578",
"idCard": "4333****34333",
"nickName": "shawn"
}