SpringBoot作为现在最为热门的后端框架,用的人非常多,不知道你们有没有注意到,绝大部分的人application.yml配置文件里的东西都是明文密码,这其实是有风险的,比如之前就有过某某公司不小心把代码开源了,然后配置文件的里数据库密码被别有用心的人利用,导致用户隐私泄露。
我最近也遇到同样的诉求,所以研究了一下怎么才能规避这样的风险。如果你Google“如何加密SpringBoot配置文件”类似的描述,绝大部分都会推荐你使用一个叫做jasypt的插件。说实话,这是一个很强大,而且简单好用的Java加解密工具库,但是我最终没有使用。
为啥没有使用?因为没有必要,自己撸更香!
原理剖析
当你使用了jasypt插件,会有一些配置需要你填写,其中一个必填项就是jasypt.encryptor.password,这是用来加解密的密码。对了,jasypt默认使用的是对称加密,如果你觉得安全性不够好,也有堆对称加密可以选。我觉得默认的就足够了。我们的目的就是防君子不防小人。
另外还有的配置是加密字段的前缀和后缀,这个默认为ENC(
和)
,中间的就是加密后的密文。
jasypt实现了BeanFactoryPostProcessor接口,重写了postProcessBeanFactory方法,在应用启动的时候,扫描所有配置项,对配置项中符合上述前缀后缀的值进行解密,并且替换。
实战
现在你已经知道了原理,非常的简单,实现起来也没有什么难度。
首先我们需要写一个工具类,用于加密和解密信息。在类变量部分,我使用固定的算法、迭代次数和盐值长度,主要是方便使用的时候少传些参数,其实完全可以作为可选参数,来实现更复杂的加密。
EncryptUtil
/**
* 信息加密工具类,使用PBEWithMD5AndDES加密算法,可对有一定安全性要求的信息进行加密。
* 安全性要求较高的信息请勿使用此工具进行加密。
*
* @author xueye
*/
public final class EncryptUtil {
private EncryptUtil() {
throw new AssertionError("No com.cicdi.utils.EncryptUtil instances for you!");
}
/**
* 随机字符生成
*/
private static final SecureRandom RANDOM = new SecureRandom();
/**
* 使用的加密算法
*/
private static final String ALGORITHM = "PBEWithMD5AndDES";
/**
* 迭代次数
*/
private static final int ITERATIONS = 1000;
/**
* 盐值长度
*/
private static final int SALT_LENGTH = 8;
/**
* 使用PBEWithMD5AndDES算法对信息进行加密
*
* @param message 需要加密的信息
* @return 加密后的信息
*/
public static String encrypt(String message, String password) {
try {
return Base64.getEncoder().encodeToString(encrypt(message.getBytes(StandardCharsets.UTF_8), password));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 对使用PBEWithMD5AndDES算法加密后的信息进行解密
*
* @param encryptedMessage 经过PBEWithMD5AndDES算法加密后的信息
* @return 解密后的信息
*/
public static String decrypt(String encryptedMessage, String password) {
try {
return new String(decrypt(Base64.getDecoder().decode(encryptedMessage), password), StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static byte[] encrypt(byte[] message, String password) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, IOException, BadPaddingException, IllegalBlockSizeException {
// 创建Key
final SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM);
byte[] salt = generateSalt(SALT_LENGTH);
final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS);
SecretKey key = factory.generateSecret(keySpec);
// 构建Cipher.
final Cipher cipherEncrypt = Cipher.getInstance(ALGORITHM);
cipherEncrypt.init(Cipher.ENCRYPT_MODE, key);
// 保存参数
byte[] params = cipherEncrypt.getParameters().getEncoded();
// 加密信息
byte[] encryptedMessage = cipherEncrypt.doFinal(message);
return ByteBuffer
.allocate(1 + params.length + encryptedMessage.length)
.put((byte) params.length)
.put(params)
.put(encryptedMessage)
.array();
}
private static byte[] decrypt(byte[] encryptedMessage, String password) throws BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, InvalidKeyException, IOException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException {
int paramsLength = Byte.toUnsignedInt(encryptedMessage[0]);
int messageLength = encryptedMessage.length - paramsLength - 1;
byte[] params = new byte[paramsLength];
byte[] message = new byte[messageLength];
System.arraycopy(encryptedMessage, 1, params, 0, paramsLength);
System.arraycopy(encryptedMessage, paramsLength + 1, message, 0, messageLength);
// 创建Key
final SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM);
final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKey key = factory.generateSecret(keySpec);
// 构建参数
AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(ALGORITHM);
algorithmParameters.init(params);
// 构建Cipher
final Cipher cipherDecrypt = Cipher.getInstance(ALGORITHM);
cipherDecrypt.init(Cipher.DECRYPT_MODE, key, algorithmParameters);
return cipherDecrypt.doFinal(message);
}
/**
* 生成指定长度的随机字节数组
*
* @param length 字节数组长度
* @return 字节数组
*/
private static byte[] generateSalt(int length) {
byte[] salt = new byte[length];
synchronized (RANDOM) {
RANDOM.nextBytes(salt);
return salt;
}
}
}
接下来实现接口,对配置文件进行处理。OriginTrackedMapPropertySource这个类对应的就是application.yml的配置,我们只对它进行处理,其他的配置没有处理的必要,这一点上,jasypt是把能处理的配置都纳入了。当然人家也可以通过配置白名单跳过。
/**
* Spring配置文件解密,启用加密功能时,解密加密的配置项
*
* @author xueye
*/
@Slf4j
public class EncryptedPropertiesBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {
public static final String PREFIX_PROPERTY = "encryptor.prefix";
public static final String SUFFIX_PROPERTY = "encryptor.suffix";
public static final String PASSWORD_PROPERTY = "encryptor.password";
private final ConfigurableEnvironment environment;
private final String prefix;
private final String suffix;
private final String password;
public EncryptedPropertiesBeanFactoryPostProcessor(ConfigurableEnvironment environment) {
this.environment = environment;
// 优先从系统环境变量java -jar运行时指定的参数获取
if (StringUtils.hasText(System.getProperty(PASSWORD_PROPERTY, ""))) {
password = System.getProperty(PASSWORD_PROPERTY, "");
} else if (StringUtils.hasText(environment.getProperty(PASSWORD_PROPERTY))) {
password = environment.getProperty(PASSWORD_PROPERTY);
} else {
throw new RuntimeException("没有找到加密解密所需密码!");
}
if (StringUtils.hasText(environment.getProperty(PREFIX_PROPERTY))) {
prefix = environment.getProperty(PREFIX_PROPERTY);
} else {
prefix = "encrypt[";
}
if (StringUtils.hasText(environment.getProperty(SUFFIX_PROPERTY))) {
suffix = environment.getProperty(SUFFIX_PROPERTY);
} else {
suffix = "]";
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
MutablePropertySources propertySources = environment.getPropertySources();
StreamSupport.stream(propertySources.spliterator(), false)
.map(this::propertySourceConverter)
.collect(Collectors.toList())
.forEach(props -> propertySources.replace(props.getName(), props));
}
/**
* 提高优先级
*/
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 100;
}
/**
* 判断是否是加密信息
*/
private boolean isEncrypted(String property) {
if (property == null) {
return false;
}
final String trimmedValue = property.trim();
return trimmedValue.startsWith(prefix) && trimmedValue.endsWith(suffix);
}
/**
* 去除前后缀包裹
*/
private String unwrapEncryptedValue(String property, String prefix, String suffix) {
return property.substring(prefix.length(), property.length() - suffix.length());
}
/**
* 配置文件转换
*/
private PropertySource<?> propertySourceConverter(PropertySource<?> propertySource) {
if (!(propertySource instanceof OriginTrackedMapPropertySource)) {
return propertySource;
}
Map<String, Object> decryptedProperties = new HashMap<>();
Map<String, Object> source = ((OriginTrackedMapPropertySource) propertySource).getSource();
for (String key : source.keySet()) {
String property = environment.getProperty(key);
if (isEncrypted(property)) {
log.info("解密配置项:{}", key);
try {
String relay = unwrapEncryptedValue(property, prefix, suffix);
String decrypt = EncryptUtil.decrypt(relay, password);
decryptedProperties.put(key, decrypt);
} catch (Exception e) {
log.error("解密配置文件异常: {}", e.getMessage());
e.printStackTrace();
}
} else {
decryptedProperties.put(key, property);
}
}
return new OriginTrackedMapPropertySource(propertySource.getName(), decryptedProperties, true);
}
}
最后一步,注入Bean,让这个处理器生效即可,我很贴心的配置了一个开关,只有配置文件中对应项为true才启用加密功能。
/**
* 配置文件解密处理器,默认不启用,需要在配置文件中开启
*
* @author xueye
* @see EncryptedPropertiesBeanFactoryPostProcessor
*/
@Configuration
@ConditionalOnProperty(name = "encryptor.enable", havingValue = "true")
public class EncryptedPropertiesConfiguration {
@Bean
public static EncryptedPropertiesBeanFactoryPostProcessor enableEncryptedPropertySourcesPostProcessor(ConfigurableEnvironment environment) {
return new EncryptedPropertiesBeanFactoryPostProcessor(environment);
}
}
哦,忘记说配置了。如下:
mail:
host: mail.qq.com
protocal: smpt
port: 587
sender: xueye@qq.com
username: xueye
password: encrypt[DzANBAgYZGAnL6IKUQIBCrT5F3H9hEtkljC1WYaWsEk=]
encryptor:
enable: true
password: xueye.io
prefix: "encrypt["
suffix: "]"
如此我们的需求就完成了,比不上jasypt那般强大,但是我们需要的功能全部实现了,而且就只有一个工具类,一个处理类,和一个配置类。顺便学习了加密算法,spring原理,岂不美哉?