最近 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 成功被解密。