Java修改文件扩展属性_扩展PropertyPlaceholderConfigurer对prop文件中的属性加密(修正1)...

一、背景

处于安全考虑需要对.properties中的数据库用户名与密码等敏感数据进行加密。项目中使用了Spring3框架统一加载属性文件,所以最好可以干扰这个加载过程来实现对.properties文件中的部分属性进行加密。

属性文件中的属性最初始时敏感属性值可以为明文,程序第一次执行后自动加密明文为密文。

修正1:

修正了一个小bug,当属性值中包含“=”号时会被截断。但还是没有完全按Java Properties标准进行实现(没考虑“:”、"\"等情况)。

二、问题分析

扩展PropertyPlaceholderConfigurer最好的方式就是编写一个继承该类的子类。

外部设置locations时,记录全部locations信息,为加密文件保留属性文件列表。重写setLocations与setLocation方法(在父类中locations私有)

寻找一个读取属性文件属性的环节,检测敏感属性加密情况。对有已有加密特征的敏感属性进行解密。重写convertProperty方法来实现。

属性文件第一次加载完毕后,立即对属性文件中的明文信息进行加密。重写postProcessBeanFactory方式来实现。

三、程序开发

1、目录结构

07be16bed20a8783f086e7a7e205c57a.png

注: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 encryptedProps = Collections.emptySet();

public void setEncryptedProps(Set 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 outputLine = new ArrayList(); //定义输出行缓存

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

}

}

2、EncryptPropertyPlaceholderConfigurer(详见注释)(修正1)

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 encryptedProps = Collections.emptySet();

public void setEncryptedProps(Set 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 outputLine = new ArrayList(); //定义输出行缓存

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]; //属性值

int eIndex = line.indexOf("="); //将属性名与值分离(修正1)

String key = line.substring(0,eIndex); // 属性名

String value = line.substring(eIndex+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 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};

}

} 注意:134行的变化,把split换为了indexof

3、spring.xml

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">

class="org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer">

/WEB-INF/spring/spring.properties

db.jdbc.username

db.jdbc.password

db.jdbc.url

四、运行效果

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)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值