擴展PropertyPlaceholderConfigurer對prop文件中的屬性加密
一、背景
處于安全考慮需要對.properties中的數據庫用戶名與密碼等敏感數據進行加密。項目中使用了Spring3框架統一加載屬性文件,所以最好可以干擾這個加載過程來實現對.properties文件中的部分屬性進行加密。
屬性文件中的屬性最初始時敏感屬性值可以為明文,程序第一次執行后自動加密明文為密文。
二、問題分析
- 擴展PropertyPlaceholderConfigurer最好的方式就是編寫一個繼承該類的子類。
- 外部設置locations時,記錄全部locations信息,為加密文件保留屬性文件列表。重寫setLocations與setLocation方法(在父類中locations私有)
- 尋找一個讀取屬性文件屬性的環節,檢測敏感屬性加密情況。對有已有加密特征的敏感屬性進行解密。重寫convertProperty方法來實現。
- 屬性文件第一次加載完畢后,立即對屬性文件中的明文信息進行加密。重寫postProcessBeanFactory方式來實現。
三、程序開發
1、目錄結構
注:aes包中為AES加密工具類,可以根據加密習慣自行修改
2、EncryptPropertyPlaceholderConfigurer(詳見注釋)
package org.noahx.spring.propencrypt;
import org.noahx.spring.propencrypt.aes.AesUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.core.io.Resource;
import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created with IntelliJ IDEA.
* User: noah
* Date: 9/16/13
* Time: 10:36 AM
* To change this template use File | Settings | File Templates.
*/
public class EncryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {
private static final String SEC_KEY = "@^_^123aBcZ*"; //主密鑰
private static final String ENCRYPTED_PREFIX = "Encrypted:{";
private static final String ENCRYPTED_SUFFIX = "}";
private static Pattern encryptedPattern = Pattern.compile("Encrypted:\\{((\\w|\\-)*)\\}"); //加密屬性特征正則
private Logger logger = LoggerFactory.getLogger(this.getClass());
private Set<String> encryptedProps = Collections.emptySet();
public void setEncryptedProps(Set<String> encryptedProps) {
this.encryptedProps = encryptedProps;
}
@Override
protected String convertProperty(String propertyName, String propertyValue) {
if (encryptedProps.contains(propertyName)) { //如果在加密屬性名單中發現該屬性
final Matcher matcher = encryptedPattern.matcher(propertyValue); //判斷該屬性是否已經加密
if (matcher.matches()) { //已經加密,進行解密
String encryptedString = matcher.group(1); //獲得加密值
String decryptedPropValue = AesUtils.decrypt(propertyName + SEC_KEY, encryptedString); //調用AES進行解密,SEC_KEY與屬性名聯合做密鑰更安全
if (decryptedPropValue != null) { //!=null說明正常
propertyValue = decryptedPropValue; //設置解決后的值
} else {//說明解密失敗
logger.error("Decrypt " + propertyName + "=" + propertyValue + " error!");
}
}
}
return super.convertProperty(propertyName, propertyValue); //將處理過的值傳給父類繼續處理
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
super.postProcessBeanFactory(beanFactory); //正常執行屬性文件加載
for (Resource location : locations) { //加載完后,遍歷location,對properties進行加密
try {
final File file = location.getFile();
if (file.isFile()) { //如果是一個普通文件
if (file.canWrite()) { //如果有寫權限
encrypt(file); //調用文件加密方法
} else {
if (logger.isWarnEnabled()) {
logger.warn("File '" + location + "' can not be write!");
}
}
} else {
if (logger.isWarnEnabled()) {
logger.warn("File '" + location + "' is not a normal file!");
}
}
} catch (IOException e) {
if (logger.isWarnEnabled()) {
logger.warn("File '" + location + "' is not a normal file!");
}
}
}
}
private boolean isBlank(String str) {
int strLen;
if (str == null || (strLen = str.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if ((Character.isWhitespace(str.charAt(i)) == false)) {
return false;
}
}
return true;
}
private boolean isNotBlank(String str) {
return !isBlank(str);
}
/**
* 屬性文件加密方法
*
* @param file
*/
private void encrypt(File file) {
List<String> outputLine = new ArrayList<String>(); //定義輸出行緩存
boolean doEncrypt = false; //是否加密屬性文件標識
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new FileReader(file));
String line = null;
do {
line = bufferedReader.readLine(); //按行讀取屬性文件
if (line != null) { //判斷是否文件結束
if (isNotBlank(line)) { //是否為空行
line = line.trim(); //取掉左右空格
if (!line.startsWith("#")) {//如果是非注釋行
String[] lineParts = line.split("="); //將屬性名與值分離
String key = lineParts[0]; // 屬性名
String value = lineParts[1]; //屬性值
if (key != null && value != null) {
if (encryptedProps.contains(key)) { //發現是加密屬性
final Matcher matcher = encryptedPattern.matcher(value);
if (!matcher.matches()) { //如果是非加密格式,則`進行加密
value = ENCRYPTED_PREFIX + AesUtils.encrypt(key + SEC_KEY, value) + ENCRYPTED_SUFFIX; //進行加密,SEC_KEY與屬性名聯合做密鑰更安全
line = key + "=" + value; //生成新一行的加密串
doEncrypt = true; //設置加密屬性文件標識
if (logger.isDebugEnabled()) {
logger.debug("encrypt property:" + key);
}
}
}
}
}
}
outputLine.add(line);
}
} while (line != null);
} catch (FileNotFoundException e) {
logger.error(e.getMessage(), e);
} catch (IOException e) {
logger.error(e.getMessage(), e);
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
}
if (doEncrypt) { //判斷屬性文件加密標識
BufferedWriter bufferedWriter = null;
File tmpFile = null;
try {
tmpFile = File.createTempFile(file.getName(), null, file.getParentFile()); //創建臨時文件
if (logger.isDebugEnabled()) {
logger.debug("Create tmp file '" + tmpFile.getAbsolutePath() + "'.");
}
bufferedWriter = new BufferedWriter(new FileWriter(tmpFile));
final Iterator<String> iterator = outputLine.iterator();
while (iterator.hasNext()) { //將加密后內容寫入臨時文件
bufferedWriter.write(iterator.next());
if (iterator.hasNext()) {
bufferedWriter.newLine();
}
}
bufferedWriter.flush();
} catch (IOException e) {
logger.error(e.getMessage(), e);
} finally {
if (bufferedWriter != null) {
try {
bufferedWriter.close();
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
}
File backupFile = new File(file.getAbsoluteFile() + "_" + System.currentTimeMillis()); //準備備份文件名
//以下為備份,異常恢復機制
if (!file.renameTo(backupFile)) { //重命名原properties文件,(備份)
logger.error("Could not encrypt the file '" + file.getAbsoluteFile() + "'! Backup the file failed!");
tmpFile.delete(); //刪除臨時文件
} else {
if (logger.isDebugEnabled()) {
logger.debug("Backup the file '" + backupFile.getAbsolutePath() + "'.");
}
if (!tmpFile.renameTo(file)) { //臨時文件重命名失敗 (加密文件替換原失敗)
logger.error("Could not encrypt the file '" + file.getAbsoluteFile() + "'! Rename the tmp file failed!");
if (backupFile.renameTo(file)) { //恢復備份
if (logger.isInfoEnabled()) {
logger.info("Restore the backup, success.");
}
} else {
logger.error("Restore the backup, failed!");
}
} else { //(加密文件替換原成功)
if (logger.isDebugEnabled()) {
logger.debug("Rename the file '" + tmpFile.getAbsolutePath() + "' -> '" + file.getAbsoluteFile() + "'.");
}
boolean dBackup = backupFile.delete();//刪除備份文件
if (logger.isDebugEnabled()) {
logger.debug("Delete the backup '" + backupFile.getAbsolutePath() + "'.(" + dBackup + ")");
}
}
}
}
}
protected Resource[] locations;
@Override
public void setLocations(Resource[] locations) { //由于location是父類私有,所以需要記錄到本類的locations中
super.setLocations(locations);
this.locations = locations;
}
@Override
public void setLocation(Resource location) { //由于location是父類私有,所以需要記錄到本類的locations中
super.setLocation(location);
this.locations = new Resource[]{location};
}
} 3、spring.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="/WEB-INF/spring/spring.properties"/>
<!--對spring.properties配置文件中的指定屬性進行加密-->
<bean id="encryptPropertyPlaceholderConfigurer"
class="org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>/WEB-INF/spring/spring.properties</value>
</list>
</property>
<property name="encryptedProps">
<set>
<value>db.jdbc.username</value>
<value>db.jdbc.password</value>
<value>db.jdbc.url</value>
</set>
</property>
</bean>
</beans> 四、運行效果
1、日志
[RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - encrypt property:db.jdbc.url [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - encrypt property:db.jdbc.username [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - encrypt property:db.jdbc.password [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - Create tmp file '/nautilus/workspaces/idea/spring-prop-encrypt/target/spring-prop-encrypt-1.0-SNAPSHOT/WEB-INF/spring/spring.properties2420183175827237221.tmp'. [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - Backup the file '/nautilus/workspaces/idea/spring-prop-encrypt/target/spring-prop-encrypt-1.0-SNAPSHOT/WEB-INF/spring/spring.properties_1379959755837'. [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - Rename the file '/nautilus/workspaces/idea/spring-prop-encrypt/target/spring-prop-encrypt-1.0-SNAPSHOT/WEB-INF/spring/spring.properties2420183175827237221.tmp' -> '/nautilus/workspaces/idea/spring-prop-encrypt/target/spring-prop-encrypt-1.0-SNAPSHOT/WEB-INF/spring/spring.properties'. [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - Delete the backup '/nautilus/workspaces/idea/spring-prop-encrypt/target/spring-prop-encrypt-1.0-SNAPSHOT/WEB-INF/spring/spring.properties_1379959755837'.(true)
2、原屬性文件
db.jdbc.driver=com.mysql.jdbc.Driver db.jdbc.url=jdbc:mysql://localhost:3306/noah?useUnicode=true&characterEncoding=utf8 db.jdbc.username=noah db.jdbc.password=noah
3、加密后的文件
db.jdbc.driver=com.mysql.jdbc.Driver
db.jdbc.url=Encrypted:{e5ShuhQjzDZrkqoVdaO6XNQrTqCPIWv8i_VR4zaK28BrmWS_ocagv3weYNdr0WwI}
db.jdbc.username=Encrypted:{z5aneQi_h4mk4LEqhjZU-A}
db.jdbc.password=Encrypted:{v09a0SrOGbw-_DxZKieu5w}注:因為密鑰與屬性名有關,所以相同值加密后的內容也不同,而且不能互換值。
五、源碼下載
六、總結
在成熟加密框架中jasypt(http://www.jasypt.org/)很不錯,包含了spring,hibernate等等加密。試用了一些功能后感覺并不太適合我的需要。
加密的安全性是相對的,沒有絕對安全的東西。如果有人反編譯了加密程序獲得了加密解密算法也屬正常。希望大家不要因為是否絕對安全而討論不休。
如果追求更高級別的加密可以考慮混淆class的同時對class文件本身進行加密,改寫默認的classloader加載加密class(調用本地核心加密程序,非Java)。
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!