Spring MVC 配置文件敏感信息加密

本文将介绍在 Spring MVC 中怎样对配置文件中敏感信息进行加密和解密。

最近 Spring MVC 项目(版本 4.3.17.RELEASE)突然要做安全基线,其中最重要的一点是不能将密码明文存储在项目配置文件中,为了解决这个问题,我们通过继承 PropertyPlaceholderConfigurer 实现自定义占位符解析器,详细步骤如下。

DES加密解密工具

利用 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;
    }

}

配置 application.properties

# 模拟加密配置
jdbc.username=root

# 注意:必须使用 ENC() 的方式指定加密后的密码,这样便于组件识别
#      其中 53E35BFE7A4E6A458A7CABD9A629FFFF 为加密后的密文
#      通过调用 DesUtil.encrypt() 获得加密后的密文
jdbc.password=ENC(53E35BFE7A4E6A458A7CABD9A629FFFF)

配置 spring-mvc.xml

<?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 地址,效果如下图:

Spring MVC 配置文件敏感信息加密

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

如果在胜利前却步,往往只会拥抱失败;如果在困难时坚持,常常会获得新的成功。
0 不喜欢
说说我的看法 -
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号