审计日志
审计日志
业务用途
审计日志负责记录系统中所有关键操作,用于安全审计与问题追溯。平台审计日志分为两类:
- 操作日志:由
AuditLogAspect(AOP 切面)自动拦截所有 Controller 方法记录,无需业务代码打注解。 - 登录日志:由
AuthController在登录/退出流程中主动调用auditLogService.log记录(成功/失败/锁定)。
审计日志表 sys_audit_log 采用「数据变更」设计,记录操作动作、影响的表名与记录 ID、新旧值(JSON)、请求参数、IP 与 User-Agent,既可按时间/用户/动作查询,也可按「表名+记录 ID」查询某条业务数据的完整变更历史。
没有 @AuditLog 注解
平台未定义 @AuditLog 注解。AuditLogAspect 通过 @Pointcut("execution(public * com.lowcode.controller..*.*(..))") 拦截所有 Controller 公有方法,根据方法名前缀与 HTTP 方法自动推断动作类型(CREATE/UPDATE/DELETE/QUERY),业务代码无需任何侵入。如需精细控制,可在 shouldExclude 中排除特定路径。
涉及文件
| 层 | 文件路径 |
|---|---|
| AOP 切面 | backend/src/main/java/com/lowcode/config/AuditLogAspect.java |
| 控制器 | backend/src/main/java/com/lowcode/controller/system/SysAuditLogController.java |
| 服务接口 | backend/src/main/java/com/lowcode/service/ISysAuditLogService.java |
| 实体 | backend/src/main/java/com/lowcode/entity/SysAuditLog.java |
| 登录日志写入 | backend/src/main/java/com/lowcode/controller/AuthController.java(recordLoginLog) |
| 审计开关配置 | backend/src/main/java/com/lowcode/controller/system/SysConfigController.java(/audit 端点) |
关联文档
关于 AOP 切面、拦截器与全局过滤器的整体机制,参见 。
数据库表
sys_audit_log(审计日志表)
| 字段 | 类型 | 说明 |
|---|---|---|
user_id | Long | 操作用户 ID |
username | String | 操作用户名 |
action | String | 动作(CREATE/UPDATE/DELETE/QUERY/LOGIN/LOGIN_FAIL/LOGOUT) |
table_name | String | 影响的表名(如 sys_user;由 Controller 类名推导) |
record_id | Long | 影响的记录 ID |
old_value | String | 旧值(JSON,操作日志中一般为 null) |
new_value | String | 新值(JSON,序列化方法入参) |
ip | String | 客户端 IP |
request_params | String | 请求参数(JSON,受 audit.request.params.enabled 开关控制) |
user_agent | String | 浏览器 User-Agent |
后端实现
审计日志查询端点
SysAuditLogController 基础路径:/api/system/audit-log
| HTTP 方法 | 路径 | 方法名 | 说明 |
|---|---|---|---|
| GET | /api/system/audit-log/list | list | 分页查询审计日志(支持 userId/action/tableName/时间范围过滤) |
| GET | /api/system/audit-log/login | loginLogs | 登录日志(action 默认 LOGIN,LOGIN_FAIL,tableName=sys_user) |
| GET | /api/system/audit-log/logout | logoutLogs | 退出日志(action=LOGOUT) |
| GET | /api/system/audit-log/operation | operationLogs | 操作日志(action 默认 CREATE,UPDATE,DELETE,QUERY) |
| GET | /api/system/audit-log/history/{tableName}/{recordId} | getHistory | 查询某表某记录的变更历史 |
AuditLogAspect 工作原理
切面用 @AfterReturning 在 Controller 方法正常返回后记录日志,失败请求不记录(登录失败除外,由 AuthController 单独记):
@Aspect
@Component
public class AuditLogAspect {
@Pointcut("execution(public * com.lowcode.controller..*.*(..))")
public void controllerPointcut() {}
@AfterReturning(pointcut = "controllerPointcut()", returning = "result")
public void auditAfterReturning(JoinPoint joinPoint, Object result) {
try {
HttpServletRequest request = getRequest();
if (request == null || shouldExclude(request.getRequestURI())) return;
String action = resolveAction(joinPoint, request);
if (action == null || !isActionEnabled(action)) return;
// ... 组装日志并写入
auditLogService.log(userId, username, action, tableName, recordId,
null, newValue, getClientIp(request), requestParams, request.getHeader("User-Agent"));
} catch (Exception e) {
// 审计失败不影响业务
}
}
}动作推断逻辑(resolveAction):先看方法名前缀,再看 HTTP 方法兜底:
private String resolveAction(JoinPoint joinPoint, HttpServletRequest request) {
String methodName = joinPoint.getSignature().getName();
if (methodName.startsWith("create") || methodName.startsWith("save")
|| methodName.startsWith("insert") || methodName.startsWith("add")) return "CREATE";
if (methodName.startsWith("update") || methodName.startsWith("edit")
|| methodName.startsWith("change")) return "UPDATE";
if (methodName.startsWith("delete") || methodName.startsWith("remove")) return "DELETE";
return switch (request.getMethod()) {
case "POST" -> "CREATE";
case "PUT", "PATCH" -> "UPDATE";
case "DELETE" -> "DELETE";
case "GET" -> "QUERY";
default -> null;
};
}开关控制(isActionEnabled):所有动作受 audit.operation.enabled 总开关(默认 false)控制,再按动作类型细分开关:
private boolean isActionEnabled(String action) {
if (!configService.getBoolean("audit.operation.enabled", false)) return false;
return switch (action) {
case "CREATE" -> configService.getBoolean("audit.operation.create.enabled", true);
case "UPDATE" -> configService.getBoolean("audit.operation.update.enabled", true);
case "DELETE" -> configService.getBoolean("audit.operation.delete.enabled", true);
case "QUERY" -> configService.getBoolean("audit.operation.query.enabled", false);
default -> false;
};
}排除路径:登录、验证码、审计日志自身、配置、控制台等路径不记录操作日志:
private boolean shouldExclude(String path) {
return path.startsWith("/api/auth/login")
|| path.startsWith("/api/auth/logout")
|| path.startsWith("/api/auth/captcha")
|| path.startsWith("/api/system/audit-log")
|| path.startsWith("/api/system/config")
|| path.startsWith("/api/system/console");
}敏感字段脱敏:开启 audit.sensitive.mask.enabled(默认 true)时,用正则把 password/token/secret 等字段值替换为 ******:
private String sanitizeValue(String value) {
if (value == null || !configService.getBoolean("audit.sensitive.mask.enabled", true)) return value;
return value.replaceAll(
"(?i)(\\\"(?:password|oldPassword|newPassword|confirmPassword|token|accessToken|refreshToken|" +
"authorization|apiKey|apiSecret|secret|redisPassword|datasourcePassword|captcha)\\\"\\s*:\\s*)\\\"[^\\\"]*\\\"",
"$1\"******\"");
}表名推导:从 Controller 类名推导表名,如 SysUserController -> sys_user:
private String extractTableName(String className) {
String tableName = className.replace("Controller", "").replace("Service", "").replace("Mapper", "");
tableName = tableName.replaceAll("([A-Z])", "_$1").toLowerCase();
return tableName.startsWith("_") ? tableName.substring(1) : tableName;
}登录日志(AuthController)
登录成功/失败由 AuthController.recordLoginLog 主动写入,动作 LOGIN(成功)或 LOGIN_FAIL(失败),tableName 固定 sys_user。失败场景包括验证码错误、密码错误、用户禁用、租户停用/过期、登录锁定等。还内置 IP 维度的登录失败锁定(MAX_LOGIN_FAILS=5):
// AuthController 登录失败锁定
private static final ConcurrentHashMap<String, long[]> LOGIN_FAIL_COUNT = new ConcurrentHashMap<>();
private static final int MAX_LOGIN_FAILS = 5;
// 超过 5 次失败则锁定该 IP 一段时间审计开关配置端点
SysConfigController 提供 /api/system/config/audit 端点管理审计开关,配置项写入 sys_config 表:
| 配置键 | 默认值 | 说明 |
|---|---|---|
audit.login.enabled | true | 登录日志 |
audit.logout.enabled | true | 退出日志 |
audit.operation.enabled | false | 操作日志总开关 |
audit.operation.create.enabled | true | 新增操作日志 |
audit.operation.update.enabled | true | 修改操作日志 |
audit.operation.delete.enabled | true | 删除操作日志 |
audit.operation.query.enabled | false | 查询操作日志 |
audit.request.params.enabled | true | 记录请求参数 |
audit.sensitive.mask.enabled | true | 敏感字段脱敏 |
总开关默认关闭
audit.operation.enabled 默认为 false,即操作日志默认不记录。需在「系统管理 -> 配置 -> 审计配置」中打开总开关后,操作日志才会写入。登录/退出日志不受此总开关控制(由 audit.login.enabled/audit.logout.enabled 独立控制)。
前端实现
暂无审计日志前端页面
当前 frontend/src/views/system/ 下未发现 audit-log 目录或专用页面。审计日志的查询前端页面待建设。后端接口已就绪,前端可直接调用 /api/system/audit-log/* 系列端点开发日志查询页面。
审计配置开关的前端管理位于系统配置页面,调用 GET/PUT /api/system/config/audit 读写上述配置键。
操作步骤
- 进入「系统管理 -> 配置 -> 审计配置」,打开
audit.operation.enabled总开关,按需勾选 CREATE/UPDATE/DELETE/QUERY 子开关。 - 如需记录请求参数,保持
audit.request.params.enabled为 true(默认开)。 - 之后所有 Controller 操作将自动写入
sys_audit_log。 - 调用
GET /api/system/audit-log/operation查询操作日志,GET /api/system/audit-log/login查询登录日志。

- 调用
GET /api/system/audit-log/history/{tableName}/{recordId}查询某条业务数据的变更轨迹。
常见问题
为什么操作了但审计日志没记录?
按顺序检查:① audit.operation.enabled 总开关是否打开(默认关);② 该动作的子开关(如 audit.operation.query.enabled)是否开;③ 该路径是否在 shouldExclude 排除列表中;④ 方法是否正常返回(切面是 @AfterReturning,抛异常不记录)。
登录日志和操作日志为什么不一起记?
登录接口 /api/auth/login 在 shouldExclude 中被排除(避免记录密码)。登录日志由 AuthController 单独用 recordLoginLog 写入,动作为 LOGIN/LOGIN_FAIL,可记录失败场景。这样设计既避免密码泄露,又能完整记录登录尝试。
怎么查某条业务数据的变更历史?
调用 GET /api/system/audit-log/history/{tableName}/{recordId},如 /api/system/audit-log/history/sys_user/10,返回该用户所有变更记录,按时间排序,可用于追溯。
审计日志会影响性能吗?
切面在方法返回后异步执行(实际是同步但 try-catch 包裹,失败不影响业务)。如高并发下有性能压力,可关闭 audit.request.params.enabled(减少序列化)或关闭 QUERY 日志(查询量大)。
