最近碰到了一个新的需求,生产环境中Java程序部署的服务器会定期清理数据,需要将保存在程序所在服务器上的日志文件挂载到网盘上,但又不想让用户看到日志文件中的信息,因此需要对日志文件中的内容进行加密。
这里,并不是对日志文件中的敏感信息进行加密,而是对所有数据都进行加密。上网查了一圈资料之后,最终到了解决方案:自定义Appender,使用AES进行加密。下面贴出具体代码。
AES加密解密工具类
package com.lg.coding.util;
import java.io.*;
import java.security.Key;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class AESUtil {
private static final String ALGORITHM = "AES";
private static int offset = 16;
private static final String transformation = "AES/CBC/PKCS5Padding";
/**
* AES加密字符串
* @param password 密钥
* @param value 待加密字符串
*/
public static String encrypt(String password, String value) {
try {
Key key = generateKey(password);
//创建初始向量iv用于指定密钥偏移量
IvParameterSpec iv = new IvParameterSpec(password.getBytes(), 0, offset);
Cipher cipher = Cipher.getInstance(transformation);
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] encryptedByteValue = cipher.doFinal(value.getBytes("utf-8"));
String encryptedValue64 = Base64.getEncoder().encodeToString(encryptedByteValue);
return encryptedValue64;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* AES解密字符串
* @param password 密钥
* @param value 待解密字符串
* @return
*/
public static String decrypt(String password, String value) {
try {
Key key = generateKey(password);
//创建初始向量iv用于指定密钥偏移量
IvParameterSpec iv = new IvParameterSpec(password.getBytes(), 0, offset);
Cipher cipher = Cipher.getInstance(transformation);
cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] decryptedValue64 = Base64.getDecoder().decode(value);
byte[] decryptedByteValue = cipher.doFinal(decryptedValue64);
String decryptedValue = new String(decryptedByteValue,"utf-8");
return decryptedValue;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* AES解密文件
* @param password 密钥
* @param inputFilePath 待解密文件路径
* @param outputFilePath 输出文件路径
*/
public static void decryptFile(String password, String inputFilePath, String outputFilePath) {
InputStream inputStream = null;
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
try {
inputStream = new FileInputStream(inputFilePath);
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
bufferedWriter = new BufferedWriter(new FileWriter(outputFilePath));
String s;
while ((s = bufferedReader.readLine()) != null) {
bufferedWriter.write(decrypt(password, s));
bufferedWriter.newLine();
bufferedWriter.flush();
}
} catch (FileNotFoundException e) {
System.out.println("找不到指定文件!");
e.printStackTrace();
} catch (IOException e) {
System.out.println("文件读取错误!");
e.printStackTrace();
} finally {
try {
inputStream.close();
bufferedReader.close();
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 生成key
* @param password
* @return
* @throws Exception
*/
private static Key generateKey(String password) {
Key key = new SecretKeySpec(password.getBytes(),ALGORITHM);
return key;
}
}
在这里,加密操作和解密操作都是针对字符串进行的,在自定义Appender类中,重写subAppend方法,在执行输出文件操作之前对内容进行字符串加密;解密时,逐行读取文件内容后再进行字符串解密。
自定义Appender类
package com.lg.coding.util;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.Layout;
import ch.qos.logback.core.rolling.RollingPolicy;
import ch.qos.logback.core.rolling.RollingPolicyBase;
import ch.qos.logback.core.rolling.RolloverFailure;
import ch.qos.logback.core.rolling.TriggeringPolicy;
import ch.qos.logback.core.rolling.helper.CompressionMode;
import ch.qos.logback.core.rolling.helper.FileNamePattern;
import ch.qos.logback.core.spi.DeferredProcessingAware;
import ch.qos.logback.core.status.ErrorStatus;
import ch.qos.logback.core.util.ContextUtil;
import org.slf4j.event.LoggingEvent;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
public class CustomRollingFileAppender
File currentlyActiveFile;
TriggeringPolicy
RollingPolicy rollingPolicy;
private static String RFA_NO_TP_URL = "http://logback.qos.ch/codes.html#rfa_no_tp";
private static String RFA_NO_RP_URL = "http://logback.qos.ch/codes.html#rfa_no_rp";
private static String COLLISION_URL = "http://logback.qos.ch/codes.html#rfa_collision";
private static String RFA_LATE_FILE_URL = "http://logback.qos.ch/codes.html#rfa_file_after";
public CustomRollingFileAppender() {
}
public void start() {
if (this.triggeringPolicy == null) {
this.addWarn("No TriggeringPolicy was set for the RollingFileAppender named " + this.getName());
this.addWarn("For more information, please visit " + RFA_NO_TP_URL);
} else if (!this.triggeringPolicy.isStarted()) {
this.addWarn("TriggeringPolicy has not started. RollingFileAppender will not start");
} /*else if (this.checkForCollisionsInPreviousRollingFileAppenders()) {
this.addError("Collisions detected with FileAppender/RollingAppender instances defined earlier. Aborting.");
this.addError("For more information, please visit " + COLLISION_WITH_EARLIER_APPENDER_URL);
}*/ else {
if (!this.append) {
this.addWarn("Append mode is mandatory for RollingFileAppender. Defaulting to append=true.");
this.append = true;
}
if (this.rollingPolicy == null) {
this.addError("No RollingPolicy was set for the RollingFileAppender named " + this.getName());
this.addError("For more information, please visit " + RFA_NO_RP_URL);
} /*else if (this.checkForFileAndPatternCollisions()) {
this.addError("File property collides with fileNamePattern. Aborting.");
this.addError("For more information, please visit " + COLLISION_URL);
}*/ else {
if (this.isPrudent()) {
if (this.rawFileProperty() != null) {
this.addWarn("Setting \"File\" property to null on account of prudent mode");
this.setFile((String)null);
}
if (this.rollingPolicy.getCompressionMode() != CompressionMode.NONE) {
this.addError("Compression is not supported in prudent mode. Aborting");
return;
}
}
this.currentlyActiveFile = new File(this.getFile());
this.addInfo("Active log file name: " + this.getFile());
super.start();
}
}
}
/*private boolean checkForFileAndPatternCollisions() {
if (this.triggeringPolicy instanceof RollingPolicyBase) {
RollingPolicyBase base = (RollingPolicyBase)this.triggeringPolicy;
FileNamePattern fileNamePattern = base.fileNamePattern;
if (fileNamePattern != null && this.fileName != null) {
String regex = fileNamePattern.toRegex();
return this.fileName.matches(regex);
}
}
return false;
}
private boolean checkForCollisionsInPreviousRollingFileAppenders() {
boolean collisionResult = false;
if (this.triggeringPolicy instanceof RollingPolicyBase) {
RollingPolicyBase base = (RollingPolicyBase)this.triggeringPolicy;
FileNamePattern fileNamePattern = base.fileNamePattern;
boolean collisionsDetected = this.innerCheckForFileNamePatternCollisionInPreviousRFA(fileNamePattern);
if (collisionsDetected) {
collisionResult = true;
}
}
return collisionResult;
}*/
private boolean innerCheckForFileNamePatternCollisionInPreviousRFA(FileNamePattern fileNamePattern) {
boolean collisionsDetected = false;
Map
if (map == null) {
return collisionsDetected;
} else {
Iterator var4 = map.entrySet().iterator();
while(var4.hasNext()) {
Map.Entry
if (fileNamePattern.equals(entry.getValue())) {
this.addErrorForCollision("FileNamePattern", ((FileNamePattern)entry.getValue()).toString(), (String)entry.getKey());
collisionsDetected = true;
}
}
if (this.name != null) {
map.put(this.getName(), fileNamePattern);
}
return collisionsDetected;
}
}
public void stop() {
super.stop();
if (this.rollingPolicy != null) {
this.rollingPolicy.stop();
}
if (this.triggeringPolicy != null) {
this.triggeringPolicy.stop();
}
Map
if (map != null && this.getName() != null) {
map.remove(this.getName());
}
}
public void setFile(String file) {
if (file != null && (this.triggeringPolicy != null || this.rollingPolicy != null)) {
this.addError("File property must be set before any triggeringPolicy or rollingPolicy properties");
this.addError("For more information, please visit " + RFA_LATE_FILE_URL);
}
super.setFile(file);
}
public String getFile() {
return this.rollingPolicy.getActiveFileName();
}
public void rollover() {
this.lock.lock();
try {
this.closeOutputStream();
this.attemptRollover();
this.attemptOpenFile();
} finally {
this.lock.unlock();
}
}
private void attemptOpenFile() {
try {
this.currentlyActiveFile = new File(this.rollingPolicy.getActiveFileName());
this.openFile(this.rollingPolicy.getActiveFileName());
} catch (IOException var2) {
this.addError("setFile(" + this.fileName + ", false) call failed.", var2);
}
}
private void attemptRollover() {
try {
this.rollingPolicy.rollover();
} catch (RolloverFailure var2) {
this.addWarn("RolloverFailure occurred. Deferring roll-over.");
this.append = true;
}
}
protected void subAppend(E event) {
if (this.isStarted()) {
try {
if (event instanceof DeferredProcessingAware) {
((DeferredProcessingAware)event).prepareForDeferredProcessing();
}
byte[] byteArray = this.encoder.encode(event);
//加密前数据
String originalString = new String(byteArray, "UTF-8");
//加密后数据
String encryptedString = AESUtil.encrypt("Sanyuan123456789", originalString);
this.writeBytes((encryptedString + "\n").getBytes());
} catch (IOException var3) {
this.started = false;
this.addStatus(new ErrorStatus("IO failure in appender", this, var3));
}
}
}
private void writeBytes(byte[] byteArray) throws IOException {
if (byteArray != null && byteArray.length != 0) {
this.lock.lock();
try {
this.getOutputStream().write(byteArray);
if (this.isImmediateFlush()) {
this.getOutputStream().flush();
}
} finally {
this.lock.unlock();
}
}
}
public RollingPolicy getRollingPolicy() {
return this.rollingPolicy;
}
public TriggeringPolicy
return this.triggeringPolicy;
}
public void setRollingPolicy(RollingPolicy policy) {
this.rollingPolicy = policy;
if (this.rollingPolicy instanceof TriggeringPolicy) {
this.triggeringPolicy = (TriggeringPolicy)policy;
}
}
public void setTriggeringPolicy(TriggeringPolicy
this.triggeringPolicy = policy;
if (policy instanceof RollingPolicy) {
this.rollingPolicy = (RollingPolicy)policy;
}
}
}
这里是直接复制RollingFileAppender类的代码,对subAppend方法进行重写,在调用writeBytes()方法之前进行加密操作,将加密后的数据输出到本地。
本系统用的日志框架为SpringBoot内置的日志处理框架Logback。将logback-spring.xml文件中系统日志输出对应的Appender标签,class属性改为自定义Appender类的全路径:
此时,启动项目,查看系统本地日志文件:
可以看到,日志文件的内容已成功加密。
再写一个文件解密接口:
@ApiOperation(value = "aes文件解密测试")
@PostMapping("/aesFileDecrypteTest")
public void aesFileDecrypteTest(String fileInputPath, String fileOutputPath) {
String key = "Sanyuan123456789";
AESUtil.decryptFile(key, fileInputPath, fileOutputPath);
}
其中,fileInputPath和fileOutputPath两个参数分别为待解密文件所在路径和解密后的文件所在路径,接口调用中输入对应参数:
调用接口,可以看到,指定路径下生成了一个名为“解密日志.log”的文件, 打开文件,查看内容:
可以看到,内容已被成功解密。