配置文件、API接口脱敏
  TEZNKK3IfmPf 23天前 13 0
 

一、配置文件脱敏

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"
}
【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 23天前 0

暂无评论

推荐阅读
TEZNKK3IfmPf