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:
@@ -44,3 +44,4 @@ out/
|
||||
**/application-dev.yml
|
||||
**/application-dev.properties
|
||||
**/.env
|
||||
/log/
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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() && "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() && "true".equals(property("LOG_EXPORT_RUNTIME"))'>
|
||||
<then>
|
||||
<appender-ref ref="RUNTIME_FILE"/>
|
||||
</then>
|
||||
</if>
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user