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:
+2
-1
@@ -43,4 +43,5 @@ out/
|
|||||||
**/application-dev.yaml
|
**/application-dev.yaml
|
||||||
**/application-dev.yml
|
**/application-dev.yml
|
||||||
**/application-dev.properties
|
**/application-dev.properties
|
||||||
**/.env
|
**/.env
|
||||||
|
/log/
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ dependencies {
|
|||||||
implementation("io.jsonwebtoken:jjwt-api:0.13.0")
|
implementation("io.jsonwebtoken:jjwt-api:0.13.0")
|
||||||
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.13.0")
|
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.13.0")
|
||||||
runtimeOnly("io.jsonwebtoken:jjwt-jackson: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.springframework.boot:spring-boot-starter-test")
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.msksbr.bookmgr.config
|
package com.msksbr.bookmgr.config
|
||||||
|
|
||||||
import com.msksbr.bookmgr.annotation.RequireRole
|
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 com.msksbr.bookmgr.template.ApiResult
|
||||||
import org.aspectj.lang.ProceedingJoinPoint
|
import org.aspectj.lang.ProceedingJoinPoint
|
||||||
import org.aspectj.lang.annotation.Around
|
import org.aspectj.lang.annotation.Around
|
||||||
@@ -43,20 +43,25 @@ class RequireRoleAspect(
|
|||||||
val role = request.getAttribute("role") as? String
|
val role = request.getAttribute("role") as? String
|
||||||
|
|
||||||
if (username == null) {
|
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")
|
return ApiResult.unauthorized<Any?>("Missing or invalid token")
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowedRoles = rolePermissions[role] ?: emptySet()
|
val allowedRoles = rolePermissions[role] ?: emptySet()
|
||||||
if (requireRole.role !in allowedRoles) {
|
if (requireRole.role !in allowedRoles) {
|
||||||
log.warn(
|
audit.warn(
|
||||||
"[AUDIT] access denied | user={} | ip={} | path={} | required={} | actual={}",
|
"event=CTRL | user={} | ip={} | op={} | result=denied | required={} | actual={}",
|
||||||
username, ip, path, requireRole.role, role
|
username, ip, path, requireRole.role, role
|
||||||
)
|
)
|
||||||
return ApiResult.forbidden<Any?>("Access denied: insufficient permissions")
|
return ApiResult.forbidden<Any?>("Access denied: insufficient permissions")
|
||||||
}
|
}
|
||||||
log.info("[AUDIT] access allowed | user={} | ip={} | path={} | role={}",
|
audit.info(
|
||||||
username, ip, path, role)
|
"event=CTRL | user={} | ip={} | op={} | result=allowed | role={}",
|
||||||
|
username, ip, path, role
|
||||||
|
)
|
||||||
return joinPoint.proceed()
|
return joinPoint.proceed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ mybatis-plus:
|
|||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.msksbr.bookmgr: ${LOG_LEVEL:INFO} # 包级别日志级别,dev 配置中覆盖为 DEBUG
|
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 ----
|
||||||
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