You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
124 lines
5.5 KiB
124 lines
5.5 KiB
package com.mydreamjob.audit;
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
/**
|
|
* 审计日志服务:持久化到 audit_logs 表,写入失败不阻断主业务流程。
|
|
*/
|
|
@Slf4j
|
|
@Component
|
|
@RequiredArgsConstructor
|
|
public class AuditService {
|
|
|
|
private final AuditLogRepository auditLogRepository;
|
|
|
|
/**
|
|
* 记录一个审计事件(兼容旧签名,上下文字段为 null 时跳过填充)
|
|
*/
|
|
public void log(Long userId, String action, String entityType, Long entityId,
|
|
String details, String ipAddress, String userAgent, String requestId) {
|
|
logWithContext(userId, action, entityType, entityId, details,
|
|
AuditContext.builder().requestId(requestId).ipAddress(ipAddress).userAgent(userAgent).build(),
|
|
LocalDateTime.now());
|
|
}
|
|
|
|
/**
|
|
* 记录一个审计事件(统一上下文版本)
|
|
*/
|
|
public void log(Long userId, String action, String entityType, Long entityId,
|
|
String details, AuditContext context) {
|
|
logWithContext(userId, action, entityType, entityId, details, context, LocalDateTime.now());
|
|
}
|
|
|
|
/**
|
|
* 记录一个审计事件(显式时间戳版本,S02)。
|
|
*
|
|
* <p>供 S02 认证入口使用:登录、刷新、登出、重放处置与全局吊销在各自入口
|
|
* 只捕获一次 {@code capturedNow},审计时间必须与之同源,不得再次读时。</p>
|
|
*/
|
|
public void log(Long userId, String action, String entityType, Long entityId,
|
|
String details, AuditContext context, LocalDateTime capturedAt) {
|
|
logWithContext(userId, action, entityType, entityId, details, context, capturedAt);
|
|
}
|
|
|
|
private void logWithContext(Long userId, String action, String entityType, Long entityId,
|
|
String details, AuditContext ctx, LocalDateTime createdAt) {
|
|
try {
|
|
// 脱敏:details 中不得包含 code/password/token/secret 等关键词的值
|
|
String safeDetails = sanitize(details);
|
|
AuditLog auditLog = AuditLog.builder()
|
|
.userId(userId)
|
|
.action(action)
|
|
.entityType(entityType)
|
|
.entityId(entityId)
|
|
.details(safeDetails)
|
|
.ipAddress(ctx != null ? ctx.getIpAddress() : null)
|
|
.userAgent(ctx != null ? ctx.getUserAgent() : null)
|
|
.requestId(ctx != null ? ctx.getRequestId() : null)
|
|
.createdAt(createdAt)
|
|
.build();
|
|
auditLogRepository.save(auditLog);
|
|
} catch (Exception e) {
|
|
// 审计持久化失败不能阻断主业务流程
|
|
log.error("Failed to persist audit log: action={}, userId={}, error={}",
|
|
action, userId, e.getMessage());
|
|
}
|
|
}
|
|
|
|
public void logLogin(Long userId, String ipAddress, String userAgent, String requestId) {
|
|
log(userId, "LOGIN", "USER", userId, "User logged in", ipAddress, userAgent, requestId);
|
|
}
|
|
|
|
public void logLogin(Long userId, AuditContext ctx) {
|
|
log(userId, "LOGIN", "USER", userId, "User logged in", ctx);
|
|
}
|
|
|
|
public void logLogout(Long userId, String ipAddress, String userAgent, String requestId) {
|
|
log(userId, "LOGOUT", "USER", userId, "User logged out", ipAddress, userAgent, requestId);
|
|
}
|
|
|
|
public void logLogout(Long userId, AuditContext ctx) {
|
|
log(userId, "LOGOUT", "USER", userId, "User logged out", ctx);
|
|
}
|
|
|
|
/** S02:登录会话创建,使用显式 capturedNow。 */
|
|
public void logSessionCreated(Long userId, String sessionRef, AuditContext ctx, LocalDateTime capturedAt) {
|
|
log(userId, "AUTH_SESSION_CREATED", "AUTH_SESSION", null,
|
|
"{\"sessionRef\":\"" + sessionRef + "\"}", ctx, capturedAt);
|
|
}
|
|
|
|
/** S02:Refresh Token 轮换成功,使用显式 capturedNow。 */
|
|
public void logTokenRefreshed(Long userId, String sessionRef, AuditContext ctx, LocalDateTime capturedAt) {
|
|
log(userId, "AUTH_TOKEN_REFRESHED", "AUTH_SESSION", null,
|
|
"{\"sessionRef\":\"" + sessionRef + "\"}", ctx, capturedAt);
|
|
}
|
|
|
|
/** S02:会话被撤销(登出、重放处置或全局吊销)。 */
|
|
public void logSessionRevoked(Long userId, String sessionRef, String reason,
|
|
AuditContext ctx, LocalDateTime capturedAt) {
|
|
log(userId, "AUTH_SESSION_REVOKED", "AUTH_SESSION", null,
|
|
"{\"sessionRef\":\"" + sessionRef + "\",\"reason\":\"" + reason + "\"}", ctx, capturedAt);
|
|
}
|
|
|
|
/** S02:检测到 Refresh Token 重放。 */
|
|
public void logRefreshReuseDetected(Long userId, String sessionRef,
|
|
AuditContext ctx, LocalDateTime capturedAt) {
|
|
log(userId, "AUTH_REFRESH_REUSE_DETECTED", "AUTH_SESSION", null,
|
|
"{\"sessionRef\":\"" + sessionRef + "\"}", ctx, capturedAt);
|
|
}
|
|
|
|
/**
|
|
* 脱敏:不记录可能的敏感字段值
|
|
*/
|
|
private String sanitize(String details) {
|
|
if (details == null) return null;
|
|
return details.replaceAll("(?i)code[=:]\\s*\\S+", "code=[REDACTED]")
|
|
.replaceAll("(?i)password[=:]\\s*\\S+", "password=[REDACTED]")
|
|
.replaceAll("(?i)token[=:]\\s*\\S+", "token=[REDACTED]")
|
|
.replaceAll("(?i)secret[=:]\\s*\\S+", "secret=[REDACTED]");
|
|
}
|
|
}
|
|
|