Skip to content

Actuator 审计功能

概述

在现代企业应用中,安全审计是一个至关重要的功能。它帮助我们追踪用户的行为、记录安全事件,并为合规性报告提供必要的数据。Spring Boot Actuator 提供了一个灵活的审计框架,能够自动发布各种安全相关的事件。

IMPORTANT

审计功能只有在 Spring Security 启用后才会生效。这是因为大部分审计事件都与安全认证和授权相关。

审计功能解决的核心问题

在实际业务场景中,我们经常需要:

  1. 安全监控:追踪谁在什么时候访问了系统
  2. 合规要求:满足行业法规对数据访问记录的要求
  3. 异常检测:识别可疑的登录尝试或访问模式
  4. 故障排查:通过审计日志快速定位问题

默认审计事件

Spring Boot Actuator 默认会发布以下类型的审计事件:

  • 认证成功:用户成功登录系统
  • 认证失败:用户登录失败(如密码错误)
  • 访问拒绝:用户尝试访问没有权限的资源

启用审计功能

要启用审计功能,你需要在应用配置中提供一个 AuditEventRepository 类型的 Bean。

使用内存存储(开发环境)

kotlin
import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository
import org.springframework.boot.actuate.audit.AuditEventRepository
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class AuditConfiguration {

    /**
     * 配置内存审计事件存储
     * 注意:仅适用于开发环境,生产环境请使用持久化存储
     */
    @Bean
    fun auditEventRepository(): AuditEventRepository {
        return InMemoryAuditEventRepository()
    }
}
java
import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository;
import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AuditConfiguration {

    /**
     * 配置内存审计事件存储
     * 注意:仅适用于开发环境,生产环境请使用持久化存储
     */
    @Bean
    public AuditEventRepository auditEventRepository() {
        return new InMemoryAuditEventRepository();
    }
}

> `InMemoryAuditEventRepository` 功能有限,仅推荐在开发环境中使用。生产环境应该实现自己的持久化存储方案。

自定义审计存储(生产环境)

在生产环境中,我们通常需要将审计事件持久化到数据库中:

kotlin
import org.springframework.boot.actuate.audit.AuditEvent
import org.springframework.boot.actuate.audit.AuditEventRepository
import org.springframework.stereotype.Repository
import java.time.Instant

@Repository
class DatabaseAuditEventRepository(
    private val auditEventJpaRepository: AuditEventJpaRepository
) : AuditEventRepository {

    /**
     * 添加审计事件到数据库
     */
    override fun add(event: AuditEvent) {
        val auditEntity = AuditEventEntity(
            principal = event.principal,
            type = event.type,
            timestamp = event.timestamp,
            data = event.data
        )
        auditEventJpaRepository.save(auditEntity)
    }

    /**
     * 根据条件查找审计事件
     */
    override fun find(
        principal: String?,
        after: Instant?,
        type: String?
    ): List<AuditEvent> {
        return auditEventJpaRepository
            .findByConditions(principal, after, type)
            .map { it.toAuditEvent() }
    }
}

/**
 * 审计事件实体类
 */
@Entity
@Table(name = "audit_events")
data class AuditEventEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column(name = "principal")
    val principal: String,

    @Column(name = "event_type")
    val type: String,

    @Column(name = "event_timestamp")
    val timestamp: Instant,

    @Column(name = "event_data", columnDefinition = "TEXT")
    val data: Map<String, Any> = emptyMap()
) {
    fun toAuditEvent(): AuditEvent {
        return AuditEvent(timestamp, principal, type, data)
    }
}

自定义审计监听器

自定义认证审计监听器

你可以通过继承 AbstractAuthenticationAuditListener 来自定义认证相关的审计事件:

kotlin
import org.springframework.boot.actuate.security.AbstractAuthenticationAuditListener
import org.springframework.security.authentication.event.AbstractAuthenticationEvent
import org.springframework.security.authentication.event.AuthenticationSuccessEvent
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent
import org.springframework.stereotype.Component

@Component
class CustomAuthenticationAuditListener : AbstractAuthenticationAuditListener() {

    /**
     * 自定义认证成功事件处理
     */
    override fun onApplicationEvent(event: AbstractAuthenticationEvent) {
        when (event) {
            is AuthenticationSuccessEvent -> {
                // 记录额外的成功登录信息
                val details = mapOf(
                    "userAgent" to getCurrentUserAgent(),
                    "ipAddress" to getCurrentClientIp(),
                    "loginTime" to System.currentTimeMillis()
                )

                publish(
                    AuditEvent(
                        event.authentication.name,
                        "AUTHENTICATION_SUCCESS",
                        details
                    )
                )
            }
            is AbstractAuthenticationFailureEvent -> {
                // 记录失败原因的详细信息
                val details = mapOf(
                    "failureReason" to event.exception.message,
                    "attemptedUsername" to getAttemptedUsername(event),
                    "ipAddress" to getCurrentClientIp()
                )

                publish(
                    AuditEvent(
                        getAttemptedUsername(event),
                        "AUTHENTICATION_FAILURE",
                        details
                    )
                )
            }
        }
    }

    private fun getCurrentUserAgent(): String {
        // 获取当前请求的 User-Agent
        return RequestContextHolder.currentRequestAttributes()
            ?.let { it as ServletRequestAttributes }
            ?.request
            ?.getHeader("User-Agent") ?: "Unknown"
    }

    private fun getCurrentClientIp(): String {
        // 获取客户端真实 IP 地址
        return RequestContextHolder.currentRequestAttributes()
            ?.let { it as ServletRequestAttributes }
            ?.request
            ?.let { request ->
                request.getHeader("X-Forwarded-For")
                    ?: request.getHeader("X-Real-IP")
                    ?: request.remoteAddr
            } ?: "Unknown"
    }
}

自定义授权审计监听器

kotlin
import org.springframework.boot.actuate.security.AbstractAuthorizationAuditListener
import org.springframework.security.access.event.AbstractAuthorizationEvent
import org.springframework.security.access.event.AuthorizationFailureEvent
import org.springframework.stereotype.Component

@Component
class CustomAuthorizationAuditListener : AbstractAuthorizationAuditListener() {

    /**
     * 自定义授权事件处理
     */
    override fun onApplicationEvent(event: AbstractAuthorizationEvent) {
        when (event) {
            is AuthorizationFailureEvent -> {
                val details = mapOf(
                    "requiredAuthorities" to event.configAttributes.map { it.attribute },
                    "userAuthorities" to event.authentication.authorities.map { it.authority },
                    "accessedResource" to event.source.toString(),
                    "timestamp" to System.currentTimeMillis()
                )

                publish(
                    AuditEvent(
                        event.authentication.name,
                        "ACCESS_DENIED",
                        details
                    )
                )
            }
        }
    }
}

业务事件审计

除了安全相关的事件,你还可以为自己的业务逻辑添加审计功能。

方式一:直接注入 AuditEventRepository

kotlin
import org.springframework.boot.actuate.audit.AuditEvent
import org.springframework.boot.actuate.audit.AuditEventRepository
import org.springframework.stereotype.Service
import java.time.Instant

@Service
class OrderService(
    private val auditEventRepository: AuditEventRepository
) {

    /**
     * 创建订单并记录审计事件
     */
    fun createOrder(order: Order, currentUser: String): Order {
        // 业务逻辑:创建订单
        val savedOrder = orderRepository.save(order)

        // 审计记录:订单创建事件
        val auditEvent = AuditEvent(
            Instant.now(),
            currentUser,
            "ORDER_CREATED",
            mapOf(
                "orderId" to savedOrder.id,
                "orderAmount" to savedOrder.amount,
                "customerInfo" to savedOrder.customerInfo,
                "createdAt" to savedOrder.createdAt
            )
        )

        auditEventRepository.add(auditEvent)

        return savedOrder
    }

    /**
     * 取消订单并记录审计事件
     */
    fun cancelOrder(orderId: Long, reason: String, currentUser: String) {
        val order = orderRepository.findById(orderId)
            ?: throw OrderNotFoundException("订单不存在: $orderId")

        // 业务逻辑:取消订单
        order.status = OrderStatus.CANCELLED
        order.cancelReason = reason
        orderRepository.save(order)

        // 审计记录:订单取消事件
        val auditEvent = AuditEvent(
            Instant.now(),
            currentUser,
            "ORDER_CANCELLED",
            mapOf(
                "orderId" to orderId,
                "cancelReason" to reason,
                "originalAmount" to order.amount,
                "cancelledAt" to Instant.now()
            )
        )

        auditEventRepository.add(auditEvent)
    }
}

方式二:发布应用事件

kotlin
import org.springframework.boot.actuate.audit.AuditApplicationEvent
import org.springframework.boot.actuate.audit.AuditEvent
import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.ApplicationEventPublisherAware
import org.springframework.stereotype.Service
import java.time.Instant

@Service
class PaymentService : ApplicationEventPublisherAware {

    private lateinit var eventPublisher: ApplicationEventPublisher

    override fun setApplicationEventPublisher(applicationEventPublisher: ApplicationEventPublisher) {
        this.eventPublisher = applicationEventPublisher
    }

    /**
     * 处理支付并发布审计事件
     */
    fun processPayment(payment: Payment, currentUser: String): PaymentResult {
        try {
            // 业务逻辑:处理支付
            val result = paymentGateway.process(payment)

            // 发布支付成功审计事件
            publishAuditEvent(
                currentUser,
                "PAYMENT_SUCCESS",
                mapOf(
                    "paymentId" to payment.id,
                    "amount" to payment.amount,
                    "method" to payment.method,
                    "transactionId" to result.transactionId
                )
            )

            return result

        } catch (e: PaymentException) {
            // 发布支付失败审计事件
            publishAuditEvent(
                currentUser,
                "PAYMENT_FAILURE",
                mapOf(
                    "paymentId" to payment.id,
                    "amount" to payment.amount,
                    "errorMessage" to e.message,
                    "errorCode" to e.errorCode
                )
            )

            throw e
        }
    }

    /**
     * 统一的审计事件发布方法
     */
    private fun publishAuditEvent(
        principal: String,
        type: String,
        data: Map<String, Any>
    ) {
        val auditEvent = AuditEvent(Instant.now(), principal, type, data)
        val auditApplicationEvent = AuditApplicationEvent(auditEvent)
        eventPublisher.publishEvent(auditApplicationEvent)
    }
}

审计事件查询

通过 Actuator 端点,你可以查询审计事件:

bash
# 查询所有审计事件
GET /actuator/auditevents

# 根据用户查询
GET /actuator/auditevents?principal=admin

# 根据时间范围查询
GET /actuator/auditevents?after=2023-01-01T00:00:00Z

# 根据事件类型查询
GET /actuator/auditevents?type=AUTHENTICATION_SUCCESS

最佳实践

> **审计数据的生命周期管理**

定期清理旧的审计数据,避免数据库无限增长。可以设置定时任务来删除超过保留期限的审计记录。

kotlin
@Component
class AuditDataCleanup(
    private val auditEventRepository: AuditEventJpaRepository
) {

    /**
     * 每天凌晨清理90天前的审计数据
     */
    @Scheduled(cron = "0 0 2 * * ?")
    fun cleanupOldAuditData() {
        val cutoffDate = Instant.now().minus(90, ChronoUnit.DAYS)
        val deletedCount = auditEventRepository.deleteByTimestampBefore(cutoffDate)
        logger.info("清理了 {} 条过期审计记录", deletedCount)
    }
}

> **敏感信息的处理**

在记录审计事件时,要小心不要记录敏感信息(如密码、信用卡号等)。应该对敏感数据进行脱敏处理。

kotlin
/**
 * 敏感信息脱敏工具
 */
object SensitiveDataMasker {

    fun maskCreditCard(cardNumber: String): String {
        return if (cardNumber.length >= 4) {
            "*".repeat(cardNumber.length - 4) + cardNumber.takeLast(4)
        } else {
            "****"
        }
    }

    fun maskEmail(email: String): String {
        val parts = email.split("@")
        return if (parts.size == 2) {
            val username = parts[0]
            val domain = parts[1]
            val maskedUsername = if (username.length > 2) {
                username.take(2) + "*".repeat(username.length - 2)
            } else {
                "**"
            }
            "$maskedUsername@$domain"
        } else {
            "***@***.com"
        }
    }
}

常见问题

1. 审计事件过多导致性能问题

解决方案

  • 使用异步处理审计事件
  • 实现批量写入机制
  • 对审计表建立合适的索引
kotlin
@Service
class AsyncAuditService(
    private val auditEventRepository: AuditEventRepository
) {

    @Async
    fun recordAuditEventAsync(event: AuditEvent) {
        auditEventRepository.add(event)
    }
}

2. 如何确保审计数据的完整性

解决方案

  • 使用数据库事务
  • 实现审计事件的重试机制
  • 考虑使用消息队列来保证审计事件不丢失
kotlin
@Transactional
fun processBusinessOperationWithAudit(operation: BusinessOperation) {
    try {
        // 执行业务操作
        performBusinessOperation(operation)

        // 记录审计事件
        recordAuditEvent(operation)

    } catch (e: Exception) {
        // 事务回滚会同时回滚业务操作和审计记录
        throw e
    }
}

总结

Spring Boot Actuator 的审计功能为应用提供了强大的安全监控和合规支持能力。通过合理的配置和自定义,我们可以:

  • 🔒 增强安全性:及时发现和响应安全威胁
  • 📊 满足合规要求:为监管机构提供完整的操作记录
  • 🔍 简化故障排查:通过审计日志快速定位问题
  • 📈 优化用户体验:分析用户行为模式,改进产品设计

IMPORTANT

在实施审计功能时,要平衡安全性、性能和存储成本。选择合适的存储策略,定期清理历史数据,并确保审计系统本身的可靠性。