最近 Spring MVC 项目(版本 4.3.17.RELEASE)突然要做安全基线,其中最重要的一点是不能将密码明文存储在项目配置文件中,为了解决这个问题,我们通过继承 PropertyPlaceholderConfigurer 实现自定义占位符解析器,详细步骤如下。
利用 JDK 内置的 API 实现 DES 对称加密算法,代码如下:
package com.hxstrive.utils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
/**
* DES常用解密加密工具类
*
* @author hxstrive.com
*/
public class DesUtil {
/**
* 默认的字符编码
*/
private static final String DEFAULT_CHARSET = "UTF-8";
/**
* 秘钥字符串
*/
private static final String PASSWORD = "E6oQo-Tbqv^kVwQtT90sRJ9yQ534gXTvosRgm5$OWu8brv3ZE4PUHi-Ul%YisBrC";
/**
* 算法名称
*/
private static final String ALGORITHM = "DES";
private static SecretKey getSecretkey() throws InvalidKeyException, NoSuchAlgorithmException,
InvalidKeySpecException {
// 创建一个DESKeySpec对象,PASSWORD可任意指定
DESKeySpec desKey = new DESKeySpec(PASSWORD.getBytes());
// 创建一个密匙工厂
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
// 生成密钥
return keyFactory.generateSecret(desKey);
}
/**
* 解密DES
*
* @param datasource 需要加密的内容
* @return 解密后的明文数据
*/
public static String decrypt(String datasource) {
try {
// 生成密钥
SecretKey secretkey = getSecretkey();
// 指定获取DES的Cipher对象
Cipher cipher = Cipher.getInstance(ALGORITHM);
// 用密匙初始化Cipher对象
cipher.init(Cipher.DECRYPT_MODE, secretkey, new SecureRandom());
// 真正开始解密操作
return new String(cipher.doFinal(parseHexStr2Byte(datasource)));
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
/**
* 加密DES
*
* @param datasource 需要加密的内容
* @return 加密的内容
*/
public static String encrypt(String datasource) {
try {
SecretKey secretKey = getSecretkey();
//指定获取DES的Cipher对象
Cipher cipher = Cipher.getInstance(ALGORITHM);
//用密匙初始化Cipher对象
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new SecureRandom());
//数据加密
return parseByte2HexStr(cipher.doFinal(datasource.getBytes(DEFAULT_CHARSET)));
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
public static String parseByte2HexStr(byte[] buf) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; ++i) {
String hex = Integer.toHexString(buf[i] & 255);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
private static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1) {
return null;
} else {
byte[] result = new byte[hexStr.length() / 2];
for (int i = 0; i < hexStr.length() / 2; ++i) {
int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
}
}通过继承 PropertyPlaceholderConfigurer 类,重写 convertProperty 方法实现自定义解析器。在该方法中,通过 isEncryptProp() 判断当前属性是否符合解密条件,即属性值的格式为 “ENC(***)” 。代码如下:
package com.hxstrive.custom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.util.StringUtils;
import com.hxstrive.utils.DesUtil;
/**
* 自定义配置占位符解析器
*
* @author hxstrive.com
*/
public class PropertyPlaceholderConfigurerExt extends PropertyPlaceholderConfigurer {
private static final Logger LOG = LoggerFactory.getLogger(PropertyPlaceholderConfigurerExt.class);
/**
* 实现配置文件中的参数项解密
* @param propertyName 属性配置名称
* @param propertyValue 属性配置值
* @return
*/
@Override
protected String convertProperty(String propertyName, String propertyValue){
String decryptValue = "";
//如果在加密属性名单中发现该属性
if (isEncryptProp(propertyValue)){
try {
decryptValue = propertyValue.replace("ENC(","").replace(")","");
LOG.info("解密前密码:" + propertyName + " = " + decryptValue);
decryptValue = DesUtil.decrypt(decryptValue);
LOG.info("配置信息解密成功:" + propertyName + " = " + decryptValue);
return decryptValue;
} catch (Exception e) {
LOG.error("服务启动中:配置秘钥信息解密失败,请检查配置是否正确!");
}
}
return propertyValue;
}
/**
* 判断属性值是否需要进行解密操作
* @param propertyValue 属性值,待解密的格式 ENC(*****)
* @return
*/
private boolean isEncryptProp(String propertyValue){
if (StringUtils.hasText(propertyValue)
&& propertyValue.startsWith("ENC(")
&& propertyValue.endsWith(")")){
return true;
}
return false;
}
}# 模拟加密配置 jdbc.username=root # 注意:必须使用 ENC() 的方式指定加密后的密码,这样便于组件识别 # 其中 53E35BFE7A4E6A458A7CABD9A629FFFF 为加密后的密文 # 通过调用 DesUtil.encrypt() 获得加密后的密文 jdbc.password=ENC(53E35BFE7A4E6A458A7CABD9A629FFFF)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.hxstrive" /> <!-- 使用切面 --> <aop:aspectj-autoproxy /> <!-- 读取资源文件 --> <bean id="pros" class="com.hxstrive.custom.PropertyPlaceholderConfigurerExt"> <property name="location" value="classpath:application.properties"></property> </bean> </beans>
通过编写一个简单的 Controller 测试一下功能是否实现,代码如下:
package com.hxstrive.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Value("${jdbc.username}")
private String jdbcUsername;
@Value("${jdbc.password}")
private String jdbcPassword;
@RequestMapping("/")
@ResponseBody
public String index() {
System.out.println("jdbcUsername=" + jdbcUsername);
System.out.println("jdbcPassword=" + jdbcPassword);
return "jdbcUsername=" + jdbcUsername + ", jdbcPassword=" + jdbcPassword;
}
}使用 tomcat 运行项目,浏览器访问 http://localhost:8080 地址,效果如下图:

从上图可知,属性文件的加密属性 jdbc.password 成功被解密。