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

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]");
}
}