前段时间收到一个需求,需要对配置文件的加密数据在服务启动的时候进行自动解密,最主要的是要通用。理想的状态是别人直接引入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编码等),必要的时候可能需要对配皮是否加密的正则进行调整,否则可能会出现匹配不到加密字符串的情况,匹配不到自然也就不会进行解密。
今天的内容就到这里了,由于笔者认知有限,文中肯定有描述错误的地方,希望大家多多包涵,及时指正,感谢。