在软件开发过程中,为了避免数据泄露,我们常常需要使用一些手段对特定的数据进行保护,例如用户的手机号、身份证、项目的输出日志等等,这些敏感的数据需要做特殊处理,有的是加密,有的是打掩码,我们可以借助一些工具包优雅的实现这些功能。这里我做了一个名为SensitiveBye的工具包,它可以在Java SE、Spring环境、SpringBoot环境中使用,实现接口字段、日志输出、数据库字段、敏感词库等类型数据脱敏需求,线上验证运行稳定。
项目地址:
github: https://github.com/eternalstone/SensitiveBye
gitee: https://gitee.com/eternalstone/SensitiveBye
<dependency>
<groupId>io.github.eternalstone</groupId>
<artifactId>sensitivebye-core</artifactId>
<version>1.0.4</version>
</dependency>
<dependency>
<groupId>io.github.eternalstone</groupId>
<artifactId>sensitivebye-spring-boot-starter</artifactId>
<version>1.0.4</version>
</dependency>
SpringBoot的相关配置
sensitive-bye:
field:
enabled: true #默认为true, 开启字段脱敏开关
log:
enabled: false #默认为false, 开启日志脱敏开关
mybatis:
enabled: false #默认为false, 开启mybatis数据库脱敏开关
启动类使用注解@EnableGlobalSensitiveBye
开启全局开关
@EnableGlobalSensitiveBye
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
SensitiveBye字段脱敏的组件是SensitiveFieldProvider
在SpringBoot项目中,启动类注解@EnableGlobalSensitiveBye
将自动注入SensitiveFieldProvider
组件,我们直接编写业务代码即可。
创建一个User对象,对象中有手机号、密码、邮箱、地址等字段需要脱敏,SensitiveBye内置了一些字段的脱敏规则,可以直接使用
public class User implements Serializable {
private Integer id;
private String username;
private String password;
@SensitiveBye(strategy = SensitiveType.MOBILE)
private String mobile;
@SensitiveBye(strategy = SensitiveType.EMAIL)
private String email;
@SensitiveBye(strategy = SensitiveType.ADDRESS)
private String address;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", mobile='" + mobile + '\'' +
", email='" + email + '\'' +
", address='" + address + '\'' +
'}';
}
}
@SensitiveBye是工具包内置的注解,通过strategy指定字段的使用内置的脱敏规则,内置字段脱敏规则有如下几个:
public enum SensitiveType {
CHINESE_NAME,
ID_CARD,
PASSWORD,
MOBILE,
PHONE,
EMAIL,
ADDRESS,
BANK_CARD,
CAR_NUMBER,
CUSTOME,
;
}
直接编写controller请求测试脱敏效果
@GetMapping("/user/{id}")
public User getUser(@PathVariable Integer id){
User user = new User();
user.setId(id);
user.setUsername("张三");
user.setPassword("123456");
user.setMobile("13988881111");
user.setEmail("123@qq.com");
user.setAddress("浙江省杭州市西湖区xxx街道xxx社区xxx");
return user;
}
请求结果如下:
{
"id": 1,
"username": "张三",
"password": "123456",
"mobile": "139****1111",
"email": "1**@qq.com",
"address": "浙江省杭州市西湖区xxx街道********"
}
这里的规则是可以自定义的,Spring项目的自定义字段脱敏策略可以直接Bean一个CustomeFieldStrategy对象
@Bean
public CustomeFieldStrategy customeFieldStrategy(){
CustomeFieldStrategy strategy = new CustomeFieldStrategy();
//自定义策略key=test, var1表示原始值,var2表示脱敏符号, 后面的表达式即是自定义脱敏逻辑,这里演示是 var1拼接*
strategy.add("test", (var1, var2)-> var1.concat(var2));
return strategy;
}
给User的username增加自定义脱敏规则
@SensitiveBye("test")
private String username;
测试请求返回数据
{
"id": 1,
"username": "张三*", //实现了自定义规则
"password": "123456",
"mobile": "139****1111",
"email": "1**@qq.com",
"address": "浙江省杭州市西湖区xxx街道********"
}
在普通的Java SE或者Spring项目中,SensitiveFieldProvider示例需要我们自己获取,它是单例实例:
SensitiveFieldProvider provider = SensitiveFieldProvider.instance();
jackson序列化脱敏
ObjectMapper mapper = new ObjectMapper();
LOGGER.info("jackson序列化脱敏:{}", mapper.writeValueAsString(user));
fastjson序列化脱敏
//fastjson序列化, 需要添加一个fastjson的值过滤器,SensitiveBye已经内置实现了SensitiveByeFilter
LOGGER.info("fastjson序列化脱敏:{}", JSONObject.toJSONString(user, SensitiveByeFilter.instance()));
SensitiveFieldProvider.instance().handle(SensitiveType.MOBILE, "13100001111", "*")
SensitiveBye日志脱敏的组件是SensitiveLogProvider
SpringBoot项目配置sensitive-bye.log.enabled=true
自动注入此组件,其他java项目需要初始化此组件:
@Bean
public SensitiveLogProvider sensitiveFieldProvider(){
SensitiveLogProvider sensitiveLogProvider = SensitiveLogProvider.instance();
//如果存在自定义策略,可以设置一个SensitiveRule对象
//sensitiveLogProvider.setSensitiveRule();
return sensitiveLogProvider
}
SensitiveBye集成了以下默认的日志脱敏规则:
public enum LoggerRule {
/**
* 姓名
*/
CHINESE_NAME("姓名|真实姓名", "=|=\\[|='|\\\":\\\"|:|:|':'", "([\\u4e00-\\u9fa5]{1}+)([\\u4e00-\\u9fa5]{1,3}+)", "$1$2$3**"),
/**
* 身份证
*/
ID_CARD("idcard|身份证|身份证号", "=|=\\[|='|\\\":\\\"|:|:|':'", "(\\d{1}+)(\\d{16}+)([\\d|X|x]{1}+)", "$1$2$3****************$5"),
/**
* 密码
*/
PASSWORD("password|pwd|密码", "=|=\\[|='|\\\":\\\"|:|:|':'", "(\\d{6}+)", "$1$2******"),
/**
* 手机号
*/
MOBILE("mobile|手机号|手机", "=|=\\[|='|\\\":\\\"|:|:|':'", "(1)([1-9]{2}+)(\\d{4}+)(\\d{4}+)", "$1$2$3$4****$6"),
/**
* 固定电话
*/
PHONE("固定电话|座机", "=|=\\[|='|\\\":\\\"|:|:|':'", "([\\d]{3,4}-)(\\d{2}+)(\\d{4}+)(\\d{2}+)", "$1$2$3$4****$6"),
/**
* 邮箱
*/
EMAIL("email|邮箱", "=|=\\[|='|\\\":\\\"|:|:|':'", "(\\w{1}+)(\\w*)(\\w{1}+)@(\\w+).com", "$1$2$3****$4@$5.com"),
/**
* 地址
*/
ADDRESS("address|地址|家庭地址|详细地址", "=|=\\[|='|\\\":\\\"|:|:|':'", "([\\u4e00-\\u9fa5]{3}+)(\\w|[\\u4e00-\\u9fa5]|-)*", "$1$2$3****"),
/**
* 银行卡
*/
BANK_CARD("bankCard|银行卡", "=|=\\[|='|\\\":\\\"|:|:|':'", "(\\d{15}+)(\\d{4}+)", "$1$2***************$4"),
;
LoggerRule(String keys, String separators, String regex, String replacement) {
this.keys = keys;
this.separators = separators;
this.regex = regex;
this.replacement = replacement;
}
private String keys;
private String separators;
private String regex;
private String replacement;
public String getKeys() {
return keys;
}
public String getSeparators() {
return separators;
}
public String getRegex() {
return regex;
}
public String getReplacement() {
return replacement;
}
}
只要日志中命中了keys和分隔符,后面的值部分就会根据正则配置进行脱敏
如果项目使用的是logback日志框架,在logback.xml中添加如下配置即可:
<conversionRule conversionWord="msg" converterClass ="io.github.eternalstone.attachment.log.converter.LogbackSensitiveConverter"/>
测试代码:
@GetMapping("/log/test")
public String getUser(){
logger.info("mobile=13125101810");
return "sccuess";
}
输出日志:
2024-01-04 11:13:37 [http-nio-8080-exec-1] [] INFO i.g.e.e.c.TestController - mobile=131****11111
2024-01-04 11:13:37 [http-nio-8080-exec-1] [] INFO i.g.e.e.c.TestController - idcard=4****************1
2024-01-04 11:13:37 [http-nio-8080-exec-1] [] INFO i.g.e.e.c.TestController - 身份证=4****************1
如果项目使用的是log4j2日志框架,? 在log4j2-spring.xml中,原日志内容格式为 %msg,需要将其替换为%sdmsg。例如:
<appenders>
<console name="STDOUT" target="SYSTEM_OUT">
<patternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ---- [%thread] %logger Line:%-3L - %sdmsg%n" />
</console>
</appenders>
测试实现的也是同样的效果
如需添加或删除或自定义脱敏规则,实现ISensitiveLogRule接口的custome(Map<String, SensitiveLogRuleWrapper> ruleMap)
方法即可,例如:
@Component
public class CustomeLogRule implements ISensitiveLogRule {
@Override
public void custome(Map<String, SensitiveLogRuleWrapper> ruleMap) {
SensitiveLogRuleWrapper wrapper = new SensitiveLogRuleWrapper();
//规则名称
wrapper.setName("wechat");
//规则前缀匹配词
wrapper.setKeys(new HashSet<String>(){{
add("微信");
add("wechat");
}});
//规则匹配词与匹配值之间的分隔符
wrapper.setSeparators(new HashSet<String>(){{
add("=");
add(":");
add("\\[");
}});
//正则表达式
wrapper.setPattern(Pattern.compile("([a-zA-Z]{1})([-_a-zA-Z0-9]{5,19}+$)"));
//替换表达式,注意需要带上匹配词和分隔符的占位符 $1表示keys, $2表示分隔符,后续就是对内容的拆分和替换
wrapper.setReplacement("$1$2$3*******");
//新增规则
ruleMap.put(wrapper.getName(), wrapper);
//或者移除默认规则
ruleMap.remove(LoggerRule.BANK_CARD.name().toLowerCase());
}
}
测试效果如下:
@GetMapping("/log/test")
public String getUser(){
logger.info("mobile=131251011111");
logger.info("idcard=422202111111111111");
logger.info("身份证=422202111111111111,这里的风景美如画,姓名=王五六,mobile=131251011111,身份证=422202111111111122");
logger.info("微信=asdfsdsdf123sf");
return "sccuess";
}
TestController Line:44 - mobile=131****11111
TestController Line:45 - idcard=4****************1
TestController Line:46 - 身份证=4****************1,这里的风景美如画,姓名=王**,mobile=131****11111,身份证=4****************2
TestController Line:47 - 微信=a*******
日志脱敏
使用的是正则匹配的,请不要在高并发项目中开启日志脱敏。
SensitiveBye的mybatis脱敏组件是MybatisSensitiveInterceptor
,它是基于Mybatis拦截器实现的。使用时需要依赖mybatis坐标,并且初始化此组件:
@Bean
public MybatisSensitiveInterceptor mybatisSensitiveInterceptor() {
return new MybatisSensitiveInterceptor();
}
mybatis数据库字段脱敏用到了两个核心注解@EnableCipher
和@CipherField
:
//@EnableCipher作用于Mapper接口的方法上,标注入参是加密还是解密,返回值是加密还是解密
@Mapper
public interface UserMapper {
@EnableCipher(parameter = CipherType.ENCRYPT)
int insertAndReturnId(User user);
@EnableCipher(result = CipherType.DECRYPT)
User selectById(@Param("id") Integer id);
}
//@CipherField作用于对象字段上,标注此字段需要加解密,并且指定加解密算法,加解密算法需要实现ICipherAlgorithm接口
public class User
@CipherField(PasswordAlgorithm.class)
private String password;
@CipherField(MobileAlgorithm.class)
private String mobile;
}
? 1.@SensitiveBye注解和@CipherField注解虽然都是标注在对象属性上的,但是两个注解的作用互不影响,可以叠加使用,例如手机号从数据库密文查出来解密成明文,再用@SensitiveBye(strategy = SensitiveType.MOBILE)将明文手机号打上掩码。
? 2.如果项目中存在多个Mybatis拦截器,需要指定拦截器的执行顺序,可以写个配置类:
@Configuration
public class MybatisConfig {
@Bean
public ConfigurationCustomizer mybatisConfigurationCustomizer() {
return new ConfigurationCustomizer() {
@Override
public void customize(Configuration configuration) {
configuration.addInterceptor(new MybatisInterceptor());
}
};
}
}
我们创建一个数据库实体对象User,使用Mybatis编写User对象的增改查功能
public class User implements Serializable {
private Integer id;
private String username;
@CipherField(PasswordAlgorithm.class)
private String password;
@CipherField(MobileAlgorithm.class)
private String mobile;
private String email;
private String address;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", mobile='" + mobile + '\'' +
", email='" + email + '\'' +
", address='" + address + '\'' +
'}';
}
}
@Mapper
public interface UserMapper {
@EnableCipher(parameter = CipherType.ENCRYPT)
int insertAndReturnId(User user);
}
以上实体中使用了@CipherField注解了password和mobile字段,在UserMapper中insertAndReturnId方法上注解了@EnableCipher表示User参数需要对应@CipherField字段做加密操作,加密算法即是@CipherField指定的算法。
我们编写测试代码
@Test
public void testInsertAndReturnId() {
User user = new User();
user.setUsername("张三");
user.setPassword("123456");
user.setMobile("13988881111");
user.setEmail("123@qq.com");
user.setAddress("浙江省杭州市西湖区xxx街道xxx社区xxx");
userMapper.insertAndReturnId(user);
//输出日志时可关闭日志脱敏避免日志打印带来的影响
LOGGER.info("数据参数:{}", user.toString());
//此时数据库中的password已经编码成了'123456******',但user对象中的password依然是'123456'
}
结果打印
2024-01-04 14:12:20 [main] [] INFO i.g.e.e.SensitiveMybatisTest - 数据参数:User{id=17, username='张三', password='123456', mobile='13988881111', email='123@qq.com', address='浙江省杭州市西湖区xxx街道xxx社区xxx'}
数据库里的记录
可以看到已经经过加解密处理(这里的加密算法只是简单的字符串加工了一下)
我们再测试一下根据手机号查询,此时需要入参手机号加密去匹配数据库,返回的参数需要解密成明文
@Mapper
public interface UserMapper {
@EnableCipher(parameter = CipherType.ENCRYPT)
int insertAndReturnId(User user);
@EnableCipher(parameter = CipherType.ENCRYPT, result = CipherType.DECRYPT)
User selectByUser(User user);
}
@Test
public void testSelectOne(){
//可以同时使用参数加密和结果解密,适用于传递明文参数查询数据密文相对应的结果,并且返回结果明文
//例如 数据库已经存在mobile为密文值为 '*13911112222*', 通过明文'13911112222'查询出user对象中mobile也为明文
User param = new User();
param.setMobile("13911112222");
User user = userMapper.selectByUser(param);
LOGGER.info("返回数据:{}", user.toString());
}
2024-01-04 14:16:12 [main] [] INFO i.g.e.e.SensitiveMybatisTest - 返回数据:User{id=18, username='张三', password='123456', mobile='13988881111', email='123@qq.com', address='浙江省杭州市西湖区xxx街道xxx社区xxx'}
此时,从数据库中查询的记录对应的加密字段已经全部解密出来,可以结合@SensitiveBye注解返回给接口时对解密明文做脱敏处理。
SensitiveBye的敏感词组件是SensitiveWordProvider
,默认不自动注入,需要使用的时候初始化即可:
@Bean
public SensitiveWordProvider sensitiveWordProvider(){
return new SensitiveWordProvider();
}
SensitiveWordProvider提供了一个有参构造器,用于以不同的方式获取词库,SensitiveBye内置了两种方式:
? 你可以通过实现ISensitiveWordSource接口的loadSource()自定义获取词库的方式。
? SensitiveWordProvider提供了三个方法:
//handle方法用于将传入的字符串中的敏感词替换成输入的符号
String handle(String word, String symbol);
//contain方法用于检测传入的字符串中包含的敏感词组
List<String> contain(String word);
//reload方法用于重新载入词库
void reload();
我们初始化这个组件,并且让它加载resource目录下的test.txt文件
@Configuration
public class BeanConfig {
@Bean
public SensitiveWordProvider sensitiveWordProvider(){
return new SensitiveWordProvider(new SensitiveWordSourceFromResource("test.txt"));
}
}
test.txt文本内容为
中国
俄罗斯
乌克兰
测试代码
@Test
public void test(){
String str = "俄罗斯攻打乌克兰";
List<String> words = sensitiveWordProvider.contain(str);
LOGGER.info("包含敏感词:{}", JSONObject.toJSONString(words));
LOGGER.info("替换敏感词:{}", sensitiveWordProvider.handle(str, "*"));
LOGGER.info("--------------------");
String str2 = "中国是一个发展中国家";
List<String> words2 = sensitiveWordProvider.contain(str2);
LOGGER.info("包含敏感词:{}", JSONObject.toJSONString(words2));
LOGGER.info("替换敏感词:{}", sensitiveWordProvider.handle(str2, "*"));
}
2024-01-04 13:39:26 [main] [] INFO i.g.e.e.SensitiveWordTest - 包含敏感词:["俄罗斯","乌克兰"]
2024-01-04 13:39:26 [main] [] INFO i.g.e.e.SensitiveWordTest - 替换敏感词:*攻打乌克*
2024-01-04 13:39:26 [main] [] INFO i.g.e.e.SensitiveWordTest - --------------------
2024-01-04 13:39:26 [main] [] INFO i.g.e.e.SensitiveWordTest - 包含敏感词:["中国"]
2024-01-04 13:39:26 [main] [] INFO i.g.e.e.SensitiveWordTest - 替换敏感词:*是一个发展中*
SensitiveBye实现了对SpringBoot的配置文件相关的配置项进行打掩码的工具SensitiveFileUtil
, 支持对yml, yaml, properties三种配置文件,它提供了以下几个方法:
//将source路径的配置文件进行配置项脱敏后输出到target目录
public static void sensitiveByeToFile(String source, String target);
//将source路径的配置文件进行配置项脱敏后输出到target目录,可传入handler自定义实现对配置项自定义操作
public static void sensitiveByeToFile(String source, String target, IFileHandler handler);
//将source路径的配置文件进行配置项脱敏后输出成字符串
public static String sensitiveByeToString(String source);
//将source路径的配置文件进行配置项脱敏后输出成字符串,可传入handler自定义实现对配置项自定义操作
public static String sensitiveByeToString(String source, IFileHandler handler);
SensitiveFileUtil对配置项脱敏的处理器是SensitiveFileHandler
,它是默认的实现,你可以继承AbstractFileHandler
类实现doFilter()对配置项进行操作:
public class SensitiveCustomeFilterHandler extends AbstractFileHandler {
@Override
public void doFilter(LinkedHashMap<String, Object> param) {
//删除test配置项
param.remove("test");
}
}
? 你可以将自定义的handler加入SensitiveFileHandler的后续执行链中,也可以直接传递自定义handler跳过SensitiveBye的SensitiveFileHandler的实现
SensitiveFileHandler handler = new SensitiveFileHandler();
handler.setNextHandler(new SensitiveCustomeFilterHandler());
String s2 = SensitiveFileUtil.sensitiveByeToString(source, handler);
这里就不做演示了。