feat(logging): add structured audit logging with file export

- Add logback-spring.xml with daily rolling file appenders
- Add structured audit events to RequireRoleAspect
- Add logging export configuration to application.yaml
- Add janino dependency for logback evaluation
- Ignore /log/ export directory
This commit is contained in:
2026-05-24 00:38:11 +08:00
parent 21dc992971
commit 57683ad64c
5 changed files with 96 additions and 7 deletions
+1
View File
@@ -44,3 +44,4 @@ out/
**/application-dev.yml
**/application-dev.properties
**/.env
/log/
+1
View File
@@ -37,6 +37,7 @@ dependencies {
implementation("io.jsonwebtoken:jjwt-api:0.13.0")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.13.0")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.13.0")
runtimeOnly("org.codehaus.janino:janino")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
@@ -1,7 +1,7 @@
package com.msksbr.bookmgr.config
import com.msksbr.bookmgr.annotation.RequireRole
import com.msksbr.bookmgr.script.log
import com.msksbr.bookmgr.script.audit
import com.msksbr.bookmgr.template.ApiResult
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
@@ -43,20 +43,25 @@ class RequireRoleAspect(
val role = request.getAttribute("role") as? String
if (username == null) {
log.warn("[AUDIT] unauthenticated | ip={} | path={} | required={}", ip, path, requireRole.role)
audit.warn(
"event=CTRL | user=${username ?: "N/A"} | ip={} | op={} | result=unauthenticated | required={}",
ip, path, requireRole.role
)
return ApiResult.unauthorized<Any?>("Missing or invalid token")
}
val allowedRoles = rolePermissions[role] ?: emptySet()
if (requireRole.role !in allowedRoles) {
log.warn(
"[AUDIT] access denied | user={} | ip={} | path={} | required={} | actual={}",
audit.warn(
"event=CTRL | user={} | ip={} | op={} | result=denied | required={} | actual={}",
username, ip, path, requireRole.role, role
)
return ApiResult.forbidden<Any?>("Access denied: insufficient permissions")
}
log.info("[AUDIT] access allowed | user={} | ip={} | path={} | role={}",
username, ip, path, role)
audit.info(
"event=CTRL | user={} | ip={} | op={} | result=allowed | role={}",
username, ip, path, role
)
return joinPoint.proceed()
}
}
+7
View File
@@ -36,6 +36,13 @@ mybatis-plus:
logging:
level:
com.msksbr.bookmgr: ${LOG_LEVEL:INFO} # 包级别日志级别,dev 配置中覆盖为 DEBUG
export:
path: ${LOG_PATH:} # 导出根目录,未设置则不写文件
audit-enabled: ${LOG_EXPORT_AUDIT:true} # 审计日志持久化开关,默认开启
runtime-enabled: ${LOG_EXPORT_RUNTIME:false} # 运行日志持久化开关,默认关闭
# 导出路径:
# $LOG_PATH/audit/audit.log ← 审计日志(当天),旧文件压缩为 .gz
# $LOG_PATH/runtime/runtime.log ← 运行日志(当天),旧文件压缩为 .gz
# ---- JWT ----
jwt:
+75
View File
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--
日志导出控制
配置来源:application.yaml → logging.export.*(通过 springProperty 注入)
AUDIT logger → 控制台 + (可选) 文件 audit/audit.log
ROOT / 其他 → 控制台 + (可选) 文件 runtime/runtime.log
按天滚动,旧文件压缩为 .gz
-->
<springProperty scope="context" name="LOG_PATH" source="logging.export.path" defaultValue=""/>
<springProperty scope="context" name="LOG_EXPORT_AUDIT" source="logging.export.audit-enabled" defaultValue="true"/>
<springProperty scope="context" name="LOG_EXPORT_RUNTIME" source="logging.export.runtime-enabled" defaultValue="false"/>
<!-- ====== CONSOLE:运行日志(始终开启) ===== -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%thread] %cyan(%logger{36}) - %msg%n</pattern>
</encoder>
</appender>
<!-- ====== CONSOLE_AUDIT:审计日志(始终开启) ===== -->
<appender name="CONSOLE_AUDIT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [AUDIT] - %msg%n</pattern>
</encoder>
</appender>
<!-- ====== AUDIT_FILE:审计日志持久化 ===== -->
<appender name="AUDIT_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/audit/audit.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/audit/audit.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>365</maxHistory>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level - %msg%n</pattern>
</encoder>
</appender>
<!-- ====== RUNTIME_FILE:运行日志持久化 ===== -->
<appender name="RUNTIME_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/runtime/runtime.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/runtime/runtime.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- ====== AUDIT Logger:固定名称 "AUDIT",不传播到 root ===== -->
<logger name="AUDIT" level="INFO" additivity="false">
<appender-ref ref="CONSOLE_AUDIT"/>
<if condition='!property("LOG_PATH").isEmpty() &amp;&amp; "true".equals(property("LOG_EXPORT_AUDIT"))'>
<then>
<appender-ref ref="AUDIT_FILE"/>
</then>
</if>
</logger>
<!-- ====== ROOT Logger ===== -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<if condition='!property("LOG_PATH").isEmpty() &amp;&amp; "true".equals(property("LOG_EXPORT_RUNTIME"))'>
<then>
<appender-ref ref="RUNTIME_FILE"/>
</then>
</if>
</root>
</configuration>