springboot项目实现对加密数据自动解密

前段时间收到一个需求,需要对配置文件的加密数据在服务启动的时候进行自动解密,最主要的是要通用。理想的状态是别人直接引入jar,不需要对原有代码有任何改动就可以实现自动解密。

我们知道常规的加解密是通过在代码里面约定好(硬编码),对从配置文件读取出来的加密数据进行解密然后再使用的,每个项目需要解密都会有这样一段解密的代码,而且是硬编码进去的,不够灵活方便。既然要共用,主要是要解决两点问题:

一是要支持常规的加密方式。
二是要对原有项目要是无侵入的(如果要使用解密,只需要引入对应的jar)。

基于以上两点,解决方案也不复杂,针对第一点,我们可以内置常用的加密方式,并且针对对应加密方式指定规定的格式,使我们可以方便的辨别出到底使用的是哪种加密方式,然后按照对应的方式去解密。针对第二点,可以把 jar 做成自动装配的效果,只对满足我们条件的数据进行解密,由此也就诞生了这个小工具,闲话少说,开整!

因为考虑到使用的方便性,这里就使用对称加密方式,而常用的对称加密考虑到安全性等问题,这里选择AES和国密算法SM4(如果需要其他的算法可以自行扩展实现)。
上代码,先给出完整的 pom 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.info</groupId>
    <artifactId>custom-descriptor</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>compile</scope>
            <version>1.18.20</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.4.5</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.16</version>
        </dependency>

        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15to18</artifactId>
            <version>1.66</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.32</version>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

主要是使用了国产开源库类hutool,同时添加lombok以及一些spring相关的jar
接下来是要两个加解密相关的工具类,主要是对 hutool 的简单封装,方便自己调用。
Sm4Util:

package com.info.descriptor.util;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.symmetric.SM4;
import cn.hutool.crypto.symmetric.SymmetricCrypto;

import static cn.hutool.crypto.Mode.CBC;
import static cn.hutool.crypto.Padding.ZeroPadding;

public class Sm4Util {
    
    

    /**
     * 加密偏移量
     */
    private static final String IV_PARAMETERS = "1a2b3c4d5e6f7g8h";

    public static String encrypt(String plainTxt, String key) {
    
    
        SymmetricCrypto sm4 = new SM4(CBC, ZeroPadding, key.getBytes(CharsetUtil.CHARSET_UTF_8), IV_PARAMETERS.getBytes(CharsetUtil.CHARSET_UTF_8));
        return Base64.encode(sm4.encrypt(plainTxt));
    }

    public static String decrypt(String cipherTxt, String key) {
    
    
        SymmetricCrypto sm4 = new SM4(CBC, ZeroPadding, key.getBytes(CharsetUtil.CHARSET_UTF_8), IV_PARAMETERS.getBytes(CharsetUtil.CHARSET_UTF_8));
        byte[] cipherHex = Base64.decode(cipherTxt);
        return sm4.decryptStr(cipherHex, CharsetUtil.CHARSET_UTF_8);
    }

    public static String generateSm4Key() {
    
    
        return RandomUtil.randomString(16);
    }

    public static String getIvParameters() {
    
    
        return IV_PARAMETERS;
    }
}

AesUtil:

package com.info.descriptor.util;

import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.symmetric.AES;
import com.info.descriptor.enums.AesKeyTypeEnum;

public class AesUtil {
    
    

    private static final String IV = "1a2b3c4d5e6f7g8h";

    private static final String AES_MODE = "CBC";

    private static final String AES_PADDING_STR = "PKCS7Padding";

    public static String generateRandomKey() {
    
    
        return generateRandomKey(AesKeyTypeEnum.KEY_TYPE_128);
    }

    public static String getIv() {
    
    
        return IV;
    }

    public static String generateRandomKey(AesKeyTypeEnum KeyType) {
    
    
        int keyLength;
        switch (KeyType) {
    
    
            case KEY_TYPE_192:
                keyLength = 192 / 8;
                break;
            case KEY_TYPE_256:
                keyLength = 256 / 8;
                break;
            case KEY_TYPE_128:
            default:
                keyLength = 128 / 8;
                break;
        }
        return RandomUtil.randomString(keyLength);
    }

    public static String encrypt(String data, String key, String iv) {
    
    
        AES aes = new AES(AES_MODE, AES_PADDING_STR, key.getBytes(), iv.getBytes());
        return aes.encryptHex(data);
    }

    public static String decrypt(String data, String key, String iv) {
    
    
        AES aes = new AES(AES_MODE, AES_PADDING_STR, key.getBytes(), iv.getBytes());
        return aes.decryptStr(data);
    }

    public static String encrypt(String data, String key) {
    
    
        AES aes = new AES(AES_MODE, AES_PADDING_STR, key.getBytes(), IV.getBytes());
        return aes.encryptHex(data);
    }

    public static String decrypt(String data, String key) {
    
    
        AES aes = new AES(AES_MODE, AES_PADDING_STR, key.getBytes(), IV.getBytes());
        return aes.decryptStr(data);
    }
}

AesKeyTypeEnum:

package com.info.descriptor.enums;

public enum AesKeyTypeEnum {
    
    

    KEY_TYPE_128,

    KEY_TYPE_192,

    KEY_TYPE_256;
}

工具类都有了,接下来就是代码的主题,其实也就一个类,其核心思想是,通过spring的环境对象获取所有的配置信息,然后过滤对需要解密的值按指定方式进行解密,解密完成后覆盖原来读取到的值。

package com.info.descriptor;

import com.info.descriptor.util.AesUtil;
import com.info.descriptor.util.Sm4Util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.env.*;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

/**
 * 和BeanPostProcessor原理一致,Spring提供了对BeanFactory进行操作的处理器BeanFactoryProcessor,
 * 简单来说就是获取容器BeanFactory,这样就可以在真正初始化bean之前对bean做一些处理操作。
 * 允许我们在工厂里所有的bean被加载进来后但是还没初始化前,对所有bean的属性进行修改也可以add属性值。
 */
@Slf4j
public class Descriptor implements BeanFactoryPostProcessor, EnvironmentAware, PriorityOrdered {
    
    

    private ConfigurableEnvironment environment;

    private String secretKey;

    private static final String DEFAULT_KEY_NAME = "secret.key";

    private static final String AES_REGEX = "^((?i)AES-)(\w+)((?i)-AES)$";

    private static final String SM4_REGEX = "^((?i)SM4-)([\w=]+)((?i)-SM4)$";

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
    
    
        final Iterator<PropertySource<?>> iterator = this.environment.getPropertySources().iterator();

        while (iterator.hasNext()) {
    
    
            final PropertySource<?> source = iterator.next();
            if (source instanceof SimpleCommandLinePropertySource) {
    
    
                SimpleCommandLinePropertySource commandLinePropertySource = (SimpleCommandLinePropertySource) source;
                this.secretKey = commandLinePropertySource.getProperty(DEFAULT_KEY_NAME);
                break;
            }
        }
        if (!StringUtils.hasLength(this.secretKey)) {
    
    
            this.secretKey = !StringUtils.hasLength(System.getProperty(DEFAULT_KEY_NAME)) ? System.getenv(DEFAULT_KEY_NAME) : System.getProperty(DEFAULT_KEY_NAME);
        }

        if (!StringUtils.hasLength(this.secretKey)) {
    
    
            this.secretKey = environment.getProperty(DEFAULT_KEY_NAME);
        }
        if (!StringUtils.hasLength(this.secretKey)) {
    
    
            return;
        }

        log.info("this.secretKey = {}", this.secretKey);
        Map<String, Object> map = new ConcurrentHashMap<>();
        final Iterator<PropertySource<?>> propertyIterator = this.environment.getPropertySources().iterator();
        List<PropertySource<?>> propertySourceList = new ArrayList<>(1 << 3);
        while (propertyIterator.hasNext()) {
    
    
            propertySourceList.add(propertyIterator.next());
        }
        propertySourceList.stream()
                .filter(propertySource -> propertySource instanceof EnumerablePropertySource)
                .forEach(source -> this.processValue(map, (EnumerablePropertySource) source));

        if (!CollectionUtils.isEmpty(map)) {
    
    
            this.environment.getPropertySources().addFirst(new MapPropertySource("custom-decode", map));
        }
    }

    /**
     * @param map    处理完成后的键值对的存放集合
     * @param source 待处理的键值对
     */
    private void processValue(Map<String, Object> map, EnumerablePropertySource source) {
    
    
        final String[] names = source.getPropertyNames();

        for (String name : names) {
    
    
            final Object value = source.getProperty(name);
            if (!(value instanceof String)) {
    
    
                continue;
            }
            String str = (String) value;
            if (match(AES_REGEX, str)) {
    
    
                map.put(name, AesUtil.decrypt(Pattern.compile(AES_REGEX).matcher(str).replaceAll("$2"), this.secretKey));
            }
            if (match(SM4_REGEX, str)) {
    
    
                map.put(name, Sm4Util.decrypt(Pattern.compile(SM4_REGEX).matcher(str).replaceAll("$2"), this.secretKey));
            }
        }
    }

    @Override
    public void setEnvironment(Environment environment) {
    
    
        this.environment = (ConfigurableEnvironment) environment;
    }

    @Override
    public int getOrder() {
    
    
        return Integer.MAX_VALUE;
    }

    private boolean match(String regex, String str) {
    
    
        if (!StringUtils.hasLength(regex) || !StringUtils.hasLength(str)) {
    
    
            return false;
        }
        Pattern pattern = Pattern.compile(regex);
        return pattern.matcher(str).matches();
    }
}

当然,为了是它能在被其他项目使用的时候自动生成,我们还需要把我们的解密器做成自动装配的效果,这点可以利用自定义spring-boot-starter的方式来实现。

首先新建一个配置类

package com.info.descriptor.config;

import com.info.descriptor.Descriptor;
import com.info.descriptor.annotation.DecodeAutoConfigAnnotation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DecodeAutoConfig {
    
    

    @Bean
    public static Descriptor descriptor() {
    
    
        return new Descriptor();
    }
}

resources目录下新建META-INF文件夹,在其内新建 spring.factories 文件,在spring.factories文件内添加如下内容

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.info.descriptor.config.DecodeAutoConfig

至此,所有工作基本已完成,下面来测试看下效果:
新建springboot项目,首先引入我们写好的jar

<dependency>
      <groupId>com.info</groupId>
      <artifactId>custom-descriptor</artifactId>
      <version>1.0-SNAPSHOT</version>
</dependency>

配置文件application.yml配置如下信息

secret:
  # 解密秘钥
  key: 8gsidftmog4851zt


test:
  # 待解密的数据 AES加密的数据应该是 ‘AES-加密后的内容-AES’ 的形式,SM4加密的数据应该是 ‘SM4-加密后的内容-SM4’ 的形式
  # 当然 aes  sm4也可以是小写字母
  key: AES-aaba5a242cd5002715b5531dac3b0562-AES

新建一个controller获取test.key的进行测试

package com.example.demo.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IndexController {
    
    

    @Value("${test.key}")
    private String key;

    @GetMapping("/index")
    public String index() {
    
    
        return this.key;
    }
}

打开浏览器,输入http://localhost:8080/index,我们发现发挥的是已经解密的结果123test
当然,还可以给这个加解密做一个开关:
自定义注解:

package com.info.descriptor.annotation;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

import java.lang.annotation.*;

@Target({
    
    ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ConditionalOnProperty(name = "decode.enabled",havingValue = "true")
public @interface DecodeAutoConfigAnnotation {
    
    
}

在配置类加上检查是否启用自动解密的注解:

package com.info.descriptor.config;

import com.info.descriptor.Descriptor;
import com.info.descriptor.annotation.DecodeAutoConfigAnnotation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DecodeAutoConfig {
    
    

    @Bean
    @DecodeAutoConfigAnnotation
    public static Descriptor descriptor() {
    
    
        return new Descriptor();
    }
}

这时,如果如果需要开启自动解密,需要在配置文件添加配置:

decode:
  # 开启自动解密
  enabled: true

至此,我们的需求已经完全实现了,有需要类似使用场景的的,可以参考这个demo来完成。

由于aes、sm4的具体加密方式可能存在差别(例如 是否对加密后的结果进行了base64编码等),必要的时候可能需要对配皮是否加密的正则进行调整,否则可能会出现匹配不到加密字符串的情况,匹配不到自然也就不会进行解密。

今天的内容就到这里了,由于笔者认知有限,文中肯定有描述错误的地方,希望大家多多包涵,及时指正,感谢。

猜你喜欢

转载自blog.csdn.net/hxj413977035/article/details/121466477