百味皆苦 java后端开发攻城狮

Spring实战场景案例

2021-05-24
百味皆苦

hutool接口数据脱敏

接口数据脱敏的实现方式

手动脱敏:直接在业务逻辑层代码中对敏感数据进行逐一处理,这种方式虽然提供了较高的灵活性,但容易因人为疏忽而导致脱敏遗漏,同时也会导致代码中存在大量的重复处理逻辑,增加了维护成本。

AOP(面向切面编程):利用AOP技术,通过定义切面来拦截API接口返回的数据流,从而实现对敏感字段的统一处理。这种方法能够将脱敏逻辑从业务代码中抽离出来,实现集中管理,提高了代码的可维护性和可扩展性。然而,由于AOP的拦截机制会增加一定的处理开销,因此可能会对系统性能产生一定的影响。

自定义序列化器:在数据序列化阶段,通过集成JSON序列化框架(如Jackson)提供的自定义序列化器功能,实现对敏感字段的自动化处理。这种方法既保持了较好的性能表现,又能够将脱敏逻辑与业务逻辑完全解耦,使得代码更加清晰和易于管理。

注解+反射:通过定义自定义注解来标记那些需要进行脱敏处理的字段,然后在数据返回前,利用Java的反射机制在运行时动态地遍历这些字段并进行脱敏处理。这种方式简化了脱敏操作的使用过程,使得开发者只需通过简单的注解标记即可实现脱敏功能,同时也有利于后续对脱敏逻辑的维护和扩展。

自定义注解实现脱敏

需要使用hutool和json

	<dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.25</version>
    </dependency>
    
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.9.2</version>
    </dependency>

创建自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizeSerializer.class)
public @interface Desensitize {

    
    DesensitizeType type() default DesensitizeType.DEFAULT;

    
    int startInclude() default 0;

    
    int endExclude() default 0;
    
}

脱敏枚举类

public enum DesensitizeType {

    //默认
    DEFAULT,
    
    CUSTOM_RULE,
    
    PHONE,
    
    EMAIL,
    
    ID_CARD,
    
    BANK_CARD,
    
    ADDRESS,
    
    CHINESE_NAME,
    
    PASSWORD,
}

自定义序列化类

这个序列化器的主要用途是在 JSON 序列化过程中自动对标记了 @Desensitize 注解的字段进行脱敏处理

Hutool支持的脱敏数据类型包括:

  1. 用户id
  2. 中文姓名
  3. 身份证号
  4. 座机号
  5. 手机号
  6. 地址
  7. 电子邮件
  8. 密码
  9. 中国大陆车牌,包含普通车辆、新能源车辆
  10. 银行卡

整体来说,所谓脱敏就是隐藏掉信息中的一部分关键信息,用*代替。DesensitizedUtil类中方法,其实就是replace方法和hide`方法的使用,想要自定义规则进行隐藏可以仿照进行实现。

public class DesensitizeSerializer extends JsonSerializer<String> implements ContextualSerializer {

    
    private DesensitizeType type;
    
    private int startInclude;
    
    private int endExclude;

    public DesensitizeSerializer() {
        this.type = DesensitizeType.DEFAULT;
    }


    public DesensitizeSerializer(DesensitizeType type) {
        this.type = type;
    }

    //在序列化字符串时被调用,根据脱敏类型对字符串进行相应的脱敏处理。根据不同的脱敏类型,使用不同的处理方法对字符串进行脱敏,并将处理后的字符串写入JSON生成器中。
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        switch (type) {
            case CUSTOM_RULE:
                
                gen.writeString(StrUtil.hide(value, startInclude, endExclude));
                break;
            case PHONE:
                gen.writeString(DesensitizedUtil.mobilePhone(value));
                break;
            case EMAIL:
                gen.writeString(DesensitizedUtil.email(value));
                break;
            case ID_CARD:
                gen.writeString(DesensitizedUtil.idCardNum(value, 1, 2));
                break;
            case BANK_CARD:
                gen.writeString(DesensitizedUtil.bankCard(value));
                break;
            case ADDRESS:
                gen.writeString(DesensitizedUtil.address(value, 8));
                break;
            case CHINESE_NAME:
                gen.writeString(DesensitizedUtil.chineseName(value));
                break;
            case PASSWORD:
                gen.writeString(DesensitizedUtil.password(value));
                break;
            default:
                gen.writeString(value);
                break;
        }
    }

    //根据上下文信息创建自定义的序列化器,用于处理带有@Desensitize注解的属性。它通过获取注解中的脱敏类型和自定义规则的起始位置和结束位置,对实例进行相应的设置,并返回自定义的序列化器实例。
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
        if (property != null) {
            Desensitize annotation = property.getAnnotation(Desensitize.class);
            if (annotation != null) {
                this.type = annotation.type();
                if (annotation.type() == DesensitizeType.CUSTOM_RULE) {
                    this.startInclude = annotation.startInclude();
                    this.endExclude = annotation.endExclude();
                }
            }
        }
        return this;
    }

}

验证

@Data
public class UserDTO {

    
    @Desensitize(type = DesensitizeType.CHINESE_NAME)
    private String name;

    
    @Desensitize(type = DesensitizeType.PHONE)
    private String phoneNumber;

    
    @Desensitize(type = DesensitizeType.EMAIL)
    private String email;

    
    @Desensitize(type = DesensitizeType.PASSWORD)
    private String password;

    
    @Desensitize(type = DesensitizeType.ID_CARD)
    private String idCard;

    
    @Desensitize(type = DesensitizeType.BANK_CARD)
    private String bankCard;

    
    @Desensitize(type = DesensitizeType.ADDRESS)
    private String address;

    
    @Desensitize(type = DesensitizeType.CUSTOM_RULE, startInclude = 2, endExclude = 6)
    private String gameName;
}

动态切换实现类

系统可能对接多个不同的短信服务商,有时候某一个挂了需要马上切换为另一个,希望在不改动业务代码的情况下实现动态切换

Spring-smart-di

它是一个对spring @Autowired注解的扩展,能够自定义用户自己的Autowired注入逻辑,目前实现了两个功能分别是 @SmartAutowired@AutowiredProxySPI 注解,我们这里要使用的便是AutowiredProxySPI 去实现我们的动态切换逻辑。

依赖

<dependency>
    <groupId>io.github.burukeyou</groupId>
    <artifactId>spring-smart-di-all</artifactId>
    <version>0.2.0</version>
</dependency>

在启动类上添加注解@EnableSmartDI启动功能

编写顶层接口和各个实现类

@EnvironmentProxySPI("${sms.impl}")
public interface SmsService {
}

// 给实现类定义别名
@BeanAliasName("某腾短信服务")
@Component
public class ASmsService implements SmsService {
}

@BeanAliasName("某移短信服务")
@Component
public class BSmsService implements SmsService {
}

在配置文件中指定现在使用的服务商

sms.impl=某腾短信服务

在业务逻辑中注入后就可以具体使用

// 依赖注入
@AutowiredProxySPI
private SmsService smsService;

自定义数据库配置动态切换

配置除了可以写在配置文件中,还可以写在数据库中

比如自定义DBProxySPI注解,并标记上@ProxySPI实现并指定具体配置获取逻辑实现类AnnotationProxyFactory即可。

然后DBProxySPI就可以像@EnvironmentProxySPI一样去使用了

@Inherited
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ProxySPI(DbProxyFactory.class) // 指定配置获取逻辑
public @interface DBProxySPI {
    
    String value();

}

@Component
public class DbProxyFactory implements AnnotationProxyFactory<DBProxySPI> {

    @Autowired
    private SysConfigMapper sysConfigDao;
    
    @Override
    public Object getProxy(Class<?> targetClass,DBProxySPI spi) {
        // todo 根据注解从数据库获取要注入的实现类
        String configName = sysConfigDao.getConfig(spi.value());
        return springContext.getBean(configName);
    }
}


@DBProxySPI("${sms.impl}")
public interface SmsService {
}

配置文件数据库敏感信息加密

jasypt简介

Jasypt(Java 简化加密)是一个Java库,它为密码、API密钥、数据库凭证和其他配置属性等敏感信息的保护提供了简单的加密和解密功能。其目的在于简化敏感数据的加密和解密过程,使配置文件和环境变量中的敏感信息保护变得更容易。

Jasypt目标

  • 安全配置:Jasypt 使开发人员能够加密敏感的配置属性,防止未经授权访问存储在配置文件或环境变量中的敏感信息。
  • 简化加密:Jasypt 为加密和解密提供了简单明了的应用程序接口,将复杂的加密算法和密钥管理抽象化。
  • 与Spring Boot集成:Jasypt 与 Spring Boot 无缝集成,为 Spring Boot 应用程序中的配置属性加密和解密提供开箱即用的支持。

在 Spring Boot 应用程序中,Jasypt 可用于加密和解密敏感的配置属性,如数据库密码、API 密钥和其他机密。Spring Boot 通过 spring-boot-starter-parent 和 spring-boot-starter-security 依赖项提供与 Jasypt 的集成。

官网:http://www.jasypt.org/

应用:https://blog.csdn.net/qq_48428343/article/details/139474152

使用

依赖

<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

配置文件

注意:这里是解密配置,加密时的算法和盐值必须与以上配置中的algorithm与password(盐值)一致。 盐值概念: 在密码学中,盐值(Salt)是一种随机数据,通常用于加强密码的哈希过程,以增加破解的难度。在Jasypt(Java Simplified Encryption)中,盐值(也称为密钥或加密密码)是用于加密和解密过程的一个重要组成部分。它是一个额外的输入,与待加密的数据一起使用,以生成一个特定的加密结果。这样,即使相同的明文数据被加密多次,每次都会因为不同的盐值而得到不同的密文。

jasypt:
  encryptor:
    # 盐值
    password: 123
    # 指定加密方式
    algorithm: PBEWithMD5AndDES
    iv-generator-classname: org.jasypt.iv.NoIvGenerator
    property:
      # 标识为加密属性的前缀
      prefix: ENC(
      # 标识为加密属性的后缀
      suffix: )

加解密工具

public class JasyptUtil {
 
    /**
     * PBE 算法
     */
    public static final String PBE_ALGORITHMS_MD5_DES = "PBEWITHMD5ANDDES";
    public static final String PBE_ALGORITHMS_MD5_TRIPLEDES = "PBEWITHMD5ANDTRIPLEDES";
    public static final String PBE_ALGORITHMS_SHA1_DESEDE = "PBEWITHSHA1ANDDESEDE";
    public static final String PBE_ALGORITHMS_SHA1_RC2_40 = "PBEWITHSHA1ANDRC2_40";
 
    private JasyptUtil() {
    }
 
    /**
     * Jasypt 加密
     *
     * @param encryptedStr 加密字符串
     * @param password     盐值
     * @return
     */
    public static String encrypt(String encryptedStr, String password) {
        return encrypt(encryptedStr, PBE_ALGORITHMS_MD5_DES, password);
    }
 
    /**
     * Jasypt 加密
     *
     * @param encryptedStr 加密字符串
     * @param algorithm    加密算法
     *                     PBE ALGORITHMS: [PBEWITHMD5ANDDES, PBEWITHMD5ANDTRIPLEDES, PBEWITHSHA1ANDDESEDE, PBEWITHSHA1ANDRC2_40]
     * @param password     盐值
     * @return
     */
    public static String encrypt(String encryptedStr, String algorithm, String password) {
        // StandardPBEStringEncryptor、StandardPBEBigDecimalEncryptor、StandardPBEBigIntegerEncryptor、StandardPBEByteEncryptor
        StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
        EnvironmentPBEConfig config = new EnvironmentPBEConfig();
 
        // 指定加密算法
        config.setAlgorithm(algorithm);
        // 加密盐值
        config.setPassword(password);
        //config.setIvGeneratorClassName("org.jasypt.iv.NoIvGenerator");
        encryptor.setConfig(config);
 
        // 加密
        return encryptor.encrypt(encryptedStr);
    }
 
    /**
     * Jasypt 解密
     *
     * @param decryptStr 解密字符串
     * @param password   盐值
     * @return
     */
    public static String decrypt(String decryptStr, String password) {
        return decrypt(decryptStr, PBE_ALGORITHMS_MD5_DES, password);
    }
 
    /**
     * Jasypt 解密
     *
     * @param decryptStr 解密字符串
     * @param algorithm  指定解密算法:解密算法要与加密算法一一对应
     *                   PBE ALGORITHMS: [PBEWITHMD5ANDDES, PBEWITHMD5ANDTRIPLEDES, PBEWITHSHA1ANDDESEDE, PBEWITHSHA1ANDRC2_40]
     * @param password   盐值
     * @return
     */
    public static String decrypt(String decryptStr, String algorithm, String password) {
        // StandardPBEStringEncryptor、StandardPBEBigDecimalEncryptor、StandardPBEBigIntegerEncryptor、StandardPBEByteEncryptor
        StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
        EnvironmentPBEConfig config = new EnvironmentPBEConfig();
 
        // 指定解密算法:解密算法要与加密算法一一对应
        config.setAlgorithm(algorithm);
        // 加密秘钥
        config.setPassword(password);
        //config.setIvGeneratorClassName("org.jasypt.iv.NoIvGenerator");
        encryptor.setConfig(config);
 
        // 解密
        return encryptor.decrypt(decryptStr);
    }
 
    public static void main(String[] args) {
        String encryptedStr = "I am the string to be encrypted";
        String algorithm = PBE_ALGORITHMS_SHA1_RC2_40;
        String password = "salt";
 
        String str = JasyptUtil.encrypt(encryptedStr, algorithm, password);
        System.out.println("加密后的字符串:" + str);
        System.out.println("解密后的字符串:" + JasyptUtil.decrypt(str, algorithm, password));
    }
}

启动类添加自动解密注解

@EnableEncryptableProperties
public class Application{
    public static void main(String[] args){
        SpringApplication.run(Application.class,args);
    }
}

测试类

package com.agileboot.admin.jasypt;
import com.agileboot.common.utils.jasypt.JasyptUtil;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
 
@SpringBootTest
public class jasyptTest {
 
    /**
     * PBE 算法
     */
    public static final String PBE_ALGORITHMS_MD5_DES = "PBEWITHMD5ANDDES";
    public static final String PBE_ALGORITHMS_MD5_TRIPLEDES = "PBEWITHMD5ANDTRIPLEDES";
    public static final String PBE_ALGORITHMS_SHA1_DESEDE = "PBEWITHSHA1ANDDESEDE";
    public static final String PBE_ALGORITHMS_SHA1_RC2_40 = "PBEWITHSHA1ANDRC2_40";
 
 
    @Test
    public void TsetJasypt() {
        String encryptedStr = "root";
        String algorithm = PBE_ALGORITHMS_MD5_DES;
        String password = "salt";
        String str = JasyptUtil.encrypt(encryptedStr, algorithm, password);
        System.out.println("加密后的字符串:" + str);
        System.out.println("解密后的字符串:" + JasyptUtil.decrypt(str, algorithm, password));
    }
 
 
}

修改配置文件中的数据库密码明文


spring:
  datasource:
    password: ENC(A+0fOw9iTjCm8RQ8F2+rMQ==)

解密自定义配置文件

我们还可以通过@EncryptablePropertySource注解来加载自定配置文件中的加密属性,可以同时指定多个配置文件

@Configuration
@EncryptablePropertySource({"classpath:app.properties"})
public class AppConfig {
    
    // 加密的数据在代码中通过@Value注解获取后自动转为明文
    @Value("${spring.datasource.password}")
	private String password ;
}

其实在Spring Boot启动过程中都已经将所有的密文数据解密为明文,这时候在创建bean解析注入对应的@Value注解的属性时一定是明文。


Comments

Content