最近碰到了一个新的需求,生产环境中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”的文件, 打开文件,查看内容:
可以看到,内容已被成功解密。