最近碰到了一个新的需求,生产环境中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<E> extends FileAppender<E> {
File currentlyActiveFile;
TriggeringPolicy<E> 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<String, FileNamePattern> map = (Map)this.context.getObject("RFA_FILENAME_PATTERN_COLLISION_MAP");
if (map == null) {
return collisionsDetected;
} else {
Iterator var4 = map.entrySet().iterator();
while(var4.hasNext()) {
Map.Entry<String, FileNamePattern> entry = (Map.Entry)var4.next();
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<String, FileNamePattern> map = ContextUtil.getFilenamePatternCollisionMap(this.context);
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<E> getTriggeringPolicy() {
return this.triggeringPolicy;
}
public void setRollingPolicy(RollingPolicy policy) {
this.rollingPolicy = policy;
if (this.rollingPolicy instanceof TriggeringPolicy) {
this.triggeringPolicy = (TriggeringPolicy)policy;
}
}
public void setTriggeringPolicy(TriggeringPolicy<E> policy) {
this.triggeringPolicy = policy;
if (policy instanceof RollingPolicy) {
this.rollingPolicy = (RollingPolicy)policy;
}
}
}
这里是直接复制RollingFileAppender类的代码,对subAppend方法进行重写,在调用writeBytes()方法之前进行加密操作,将加密后的数据输出到本地。
本系统用的日志框架为SpringBoot内置的日志处理框架Logback。将logback-spring.xml文件中系统日志输出对应的Appender标签,class属性改为自定义Appender类的全路径:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProperty scope="context" name="logDir" source="logging.path"/>
<!-- 日志存放路径 -->
<property name="log.path" value="${logDir}" />
<!-- 日志输出格式 -->
<!-- <property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />-->
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<charset>utf8</charset>
<pattern>${log.pattern}</pattern>
</encoder>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${log.pattern}</pattern>
</layout>
</appender>
<!-- 系统日志输出 -->
<appender name="file_info" class="com.lg.coding.util.CustomRollingFileAppender">
<file>${log.path}/coding-info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/coding-info.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
<charset>utf8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 只会打印debug不会有info日志-->
<!-- <level>DEBUG</level>-->
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
<level>INFO</level>
</filter>
</appender>
<appender name="file_error" class="com.lg.coding.util.CustomRollingFileAppender">
<file>${log.path}/coding-error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>coding/.%d{yyyy-MM-dd-HH:mm:ss}.gz</fileNamePattern>
<fileNamePattern>${log.path}/coding-error.%d{yyyy-MM-dd-HH}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
<charset>utf8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_debug" class="com.lg.coding.util.CustomRollingFileAppender">
<file>${log.path}/coding-debug.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/coding-debug.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
<charset>utf8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 只会打印debug不会有info日志-->
<!-- <level>DEBUG</level>-->
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
<level>DEBUG</level>
</filter>
</appender>
<!-- 用户访问日志输出 -->
<appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-user.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily -->
<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统模块日志级别控制 -->
<logger name="com.example" level="debug" />
<!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" />
<root level="info">
<appender-ref ref="console" />
</root>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
<appender-ref ref="file_debug" />
</root>
<!--系统用户操作日志-->
<logger name="sys-user" level="info">
<appender-ref ref="sys-user"/>
</logger>
</configuration>
此时,启动项目,查看系统本地日志文件:
可以看到,日志文件的内容已成功加密。
再写一个文件解密接口:
@ApiOperation(value = "aes文件解密测试")
@PostMapping("/aesFileDecrypteTest")
public void aesFileDecrypteTest(String fileInputPath, String fileOutputPath) {
String key = "Sanyuan123456789";
AESUtil.decryptFile(key, fileInputPath, fileOutputPath);
}
其中,fileInputPath和fileOutputPath两个参数分别为待解密文件所在路径和解密后的文件所在路径,接口调用中输入对应参数:
调用接口,可以看到,指定路径下生成了一个名为“解密日志.log”的文件, 打开文件,查看内容:
可以看到,内容已被成功解密。