一个注解搞定SpringBoot数据脱敏

1. 简述

在软件开发过程中,为了避免数据泄露,我们常常需要使用一些手段对特定的数据进行保护,例如用户的手机号、身份证、项目的输出日志等等,这些敏感的数据需要做特殊处理,有的是加密,有的是打掩码,我们可以借助一些工具包优雅的实现这些功能。这里我做了一个名为SensitiveBye的工具包,它可以在Java SE、Spring环境、SpringBoot环境中使用,实现接口字段、日志输出、数据库字段、敏感词库等类型数据脱敏需求,线上验证运行稳定。

项目地址:
github: https://github.com/eternalstone/SensitiveBye
gitee: https://gitee.com/eternalstone/SensitiveBye

2. 导包和配置

2.1 普通的Java SE和Spring项目使用

<dependency>
  <groupId>io.github.eternalstone</groupId>
  <artifactId>sensitivebye-core</artifactId>
  <version>1.0.4</version>
</dependency>

2.2 SpringBoot项目使用

<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);
    }

}

3. 字段脱敏

SensitiveBye字段脱敏的组件是SensitiveFieldProvider

3.1 接口字段脱敏

在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();

3.2 json序列化脱敏

  • jackson序列化脱敏

    ObjectMapper mapper = new ObjectMapper();
    LOGGER.info("jackson序列化脱敏:{}", mapper.writeValueAsString(user));
    
  • fastjson序列化脱敏

     //fastjson序列化, 需要添加一个fastjson的值过滤器,SensitiveBye已经内置实现了SensitiveByeFilter
    LOGGER.info("fastjson序列化脱敏:{}", JSONObject.toJSONString(user, SensitiveByeFilter.instance()));	
    

3.3 java对象脱敏

SensitiveFieldProvider.instance().handle(SensitiveType.MOBILE, "13100001111", "*")

4.日志脱敏

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和分隔符,后面的值部分就会根据正则配置进行脱敏

4.1 logback日志脱敏

如果项目使用的是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

4.2 logback日志脱敏

如果项目使用的是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>

测试实现的也是同样的效果

4.3 自定义日志脱敏规则

如需添加或删除或自定义脱敏规则,实现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*******

日志脱敏使用的是正则匹配的,请不要在高并发项目中开启日志脱敏。

5. 数据库字段加解密

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注解返回给接口时对解密明文做脱敏处理。

6. 其他工具使用

6.1 敏感词库

SensitiveBye的敏感词组件是SensitiveWordProvider,默认不自动注入,需要使用的时候初始化即可:

@Bean
public SensitiveWordProvider sensitiveWordProvider(){
    return new SensitiveWordProvider();
}

SensitiveWordProvider提供了一个有参构造器,用于以不同的方式获取词库,SensitiveBye内置了两种方式:

  • SensitiveWordSourceFromResource (获取resource目录下的sensitive.txt文件, 可自定义文件名)
  • SensitiveWordSourceFromUrl(传入一个url,从网络获取词库文件)

​ 你可以通过实现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 - 替换敏感词:*是一个发展中*

6.2 配置文件脱敏

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);

这里就不做演示了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值