Skip to content
第 34 章 架构 ⏱ 11 分钟阅读

第 34 章:审计与合规 ​

学习目标 ​

  • 掌握审计日志的设计与实现
  • 学会等保 / GDPR / 个人信息保护法合规要点
  • 配置数据保留与删除
  • 避免审计缺失、合规失败的坑

一、审计日志 ​

text
审计日志 = 关键操作记录
├── 谁(UserId)
├── 何时(Timestamp)
├── 做了什么(Action)
├── 对象(Resource)
├── 结果(Success/Failure)
└── 上下文(IP、UA)
java
@Entity
@Table(name = "audit_log")
public class AuditLog {
    @Id @GeneratedValue
    private Long id;

    private String userId;
    private String action;        // "DELETE_USER"
    private String resource;      // "user:100"
    private String result;        // "SUCCESS" / "FAILURE"
    private String ip;
    private String userAgent;
    private String detail;        // JSON
    private LocalDateTime timestamp;
}
java
// AOP 记录审计日志
@Aspect
@Component
public class AuditLogAspect {
    @Resource private AuditLogRepository repository;

    @Around("@annotation(AuditLog)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        AuditLog annotation = signature.getMethod().getAnnotation(AuditLog.class);

        AuditLog log = new AuditLog();
        log.setAction(annotation.action());
        log.setUserId(SecurityContext.getCurrentUserId());
        log.setTimestamp(LocalDateTime.now());

        try {
            Object result = pjp.proceed();
            log.setResult("SUCCESS");
            repository.save(log);
            return result;
        } catch (Throwable e) {
            log.setResult("FAILURE");
            log.setDetail(e.getMessage());
            repository.save(log);
            throw e;
        }
    }
}
java
// 使用
@AuditLog(action = "DELETE_USER")
public void deleteUser(Long id) {
    userRepository.deleteById(id);
}

⚠️ 坑 1:审计日志写到普通表,和数据混在一起,误删数据 = 审计丢失。独立存储 + 不可改写。

二、审计日志安全 ​

java
// 1. 不可篡改(Append-only)
CREATE TABLE audit_log (
    id BIGINT PRIMARY KEY,
    user_id VARCHAR(50),
    action VARCHAR(50),
    result VARCHAR(20),
    timestamp DATETIME,
    pre_hash VARCHAR(64),   // 前一条 hash
    row_hash VARCHAR(64)    // 当前 hash = sha256(本条 + pre_hash)
);
// 形成 hash chain,任一条改动,链断开

// 2. 独立存储
// 写入单独的 audit_log 数据库 / ES,不归业务
java
// 3. 定时备份
@Scheduled(cron = "0 0 1 * * ?")
public void backupAudit() {
    // 每日 1:00 备份到 OSS
    ossService.upload("audit/2026-01-15.json", exportToJson());
}

三、查询与追溯 ​

java
// 1. 单用户操作历史
public List<AuditLog> findByUser(String userId, LocalDateTime start, LocalDateTime end) {
    return auditLogRepository.findByUserIdAndTimestampBetween(userId, start, end);
}

// 2. 资源操作历史
public List<AuditLog> findByResource(String resource) {
    return auditLogRepository.findByResourceOrderByTimestampDesc(resource);
}

// 3. 异常操作告警
@EventListener
public void onAuditLog(AuditLog log) {
    if (isSensitive(log.getAction()) && log.getResult().equals("FAILURE")) {
        // 敏感操作失败 3 次 → 告警
        alertService.send("敏感操作失败:" + log.getAction());
    }
}

四、等保 2.0 合规 ​

text
等保三级要求
├── 身份鉴别:双因素认证
├── 访问控制:RBAC 审计
├── 安全审计:日志保留 6 个月
├── 入侵防范:WAF、漏洞修复
├── 数据完整:加密传输
├── 数据保密:加密存储
├── 备份恢复:异地备份
└── 个人信息保护:最小化原则
java
// 1. 审计日志保留 6 个月
@Scheduled(fixedDelay = 86400000)  // 每天执行
public void cleanAuditLog() {
    LocalDateTime expire = LocalDateTime.now().minusMonths(6);
    auditLogRepository.deleteByTimestampBefore(expire);
}
java
// 2. 登录失败 5 次锁账户
@EventListener
public void onLoginFailure(LoginFailureEvent event) {
    int count = loginFailureRepository.countByUserIdAndTimestampAfter(
        event.getUserId(), LocalDateTime.now().minusMinutes(15)
    );
    if (count >= 5) {
        userService.lock(event.getUserId(), Duration.ofMinutes(30));
        alertService.send("账户锁定:" + event.getUserId());
    }
}

五、GDPR 合规 ​

text
GDPR 七大原则
├── 合法、公正、透明
├── 目的限定
├── 数据最小化
├── 准确性
├── 存储限制
├── 完整与保密
└── 问责
java
// 1. 用户删除权(被遗忘权)
@PostMapping("/api/user/delete")
public void deleteAccount(@RequestHeader("X-User-Id") String userId) {
    // 1. 主数据删除
    userRepository.deleteById(userId);

    // 2. 关联数据删除
    orderRepository.anonymizeByUserId(userId);     // 订单匿名化
    addressRepository.deleteByUserId(userId);
    paymentRepository.deleteByUserId(userId);

    // 3. 审计日志保留(可识别信息脱敏)
    auditLogRepository.anonymizeByUserId(userId);

    // 4. 异步清理缓存
    cacheService.evictByUserId(userId);
}
java
// 2. 数据导出权
@GetMapping("/api/user/export")
public Resource exportData(@RequestHeader("X-User-Id") String userId) {
    Map<String, Object> data = new HashMap<>();
    data.put("user", userRepository.findById(userId).orElseThrow());
    data.put("orders", orderRepository.findByUserId(userId));
    data.put("addresses", addressRepository.findByUserId(userId));
    return new ByteArrayResource(JsonUtil.toJson(data).getBytes());
}

六、个人信息保护法 ​

text
中国《个人信息保护法》
├── 告知同意:采集前告知用途
├── 最小必要:不采集与业务无关的信息
├── 公开透明:隐私政策
├── 个人权利:查询、更正、删除
└── 数据出境:安全评估
java
// 1. 隐私政策版本管理
@Entity
public class PrivacyPolicy {
    private Long id;
    private String version;
    private String content;
    private LocalDateTime effectiveAt;
}

@Entity
public class UserAgreement {
    private Long userId;
    private String policyVersion;
    private LocalDateTime agreedAt;
}

// 2. 采集前明确告知
public void register(UserDto dto) {
    if (!dto.isPrivacyAgreed()) {
        throw new PrivacyNotAgreedException();
    }
    userRepository.save(dto);
    agreementRepository.save(new UserAgreement(dto.getId(), PRIVACY_POLICY_VERSION, Instant.now()));
}
java
// 3. 敏感信息单独同意
public void bindBankCard(Long userId, String cardNo) {
    // 采集银行卡号需用户单独同意
    if (!agreementRepository.hasAgreed(userId, "BANK_CARD_COLLECTION")) {
        throw new SensitiveDataNotAgreedException();
    }
    bankCardRepository.save(userId, cardNo);
}

⚠️ 坑 2:GDPR 罚款最低 1000 万欧元或年营收 2%,某公司因违规被罚 7 亿欧元。敏感数据处理必须谨慎。

七、PCI DSS 支付合规 ​

java
// 1. 卡号不存储,只存哈希
public class CardToken {
    private String token;          // 透明存储由支付网关
    private String lastFour;       // 后四位
    private String brand;          // Visa / Master
}

public void saveCard(Long userId, String cardNo) {
    String token = paymentClient.tokenize(cardNo);  // 支付网管代币化
    CardToken cardToken = new CardToken(token, cardNo.substring(12, 16), detectBrand(cardNo));
    cardRepository.save(userId, cardToken);
    // 不存储完整卡号
}
java
// 2. 传输加密
ssl:
  enabled: true
  tls:
    protocols: TLSv1.2,TLSv1.3
java
// 3. 访问审计
@AuditLog(action = "VIEW_CARD")
public CardToken viewCard(Long userId) {
    // 任何查看卡号的行为都记录
    return cardRepository.findByUserId(userId);
}

八、数据保留与删除 ​

java
// 1. 数据分类
public enum DataCategory {
    TRANSACTION(7),      // 交易数据保留 7 年(税务要求)
    AUDIT_LOG(6),        // 审计日志 6 个月
    SESSION(30),         // 会话 30 天
    LOG(90),             // 日志 90 天
    PII(0);              // 个人信息,删除即清

    private final int retentionMonths;
}

// 2. 自动清理
@Scheduled(cron = "0 0 3 * * ?")
public void cleanExpiredData() {
    for (DataCategory category : DataCategory.values()) {
        if (category.getRetentionMonths() == 0) continue;
        LocalDateTime expire = LocalDateTime.now().minusMonths(category.getRetentionMonths());
        cleanService.cleanByCategory(category, expire);
    }
}
java
// 3. 软删除 + 硬删除
@Entity
public class User {
    @Column
    private LocalDateTime deletedAt;
}

// 软删除
public void softDelete(Long id) {
    User user = userRepository.findById(id).orElseThrow();
    user.setDeletedAt(LocalDateTime.now());
    userRepository.save(user);
}

// 硬删除(30 天后)
@Scheduled(cron = "0 0 4 * * ?")
public void hardDeleteExpired() {
    LocalDateTime deadline = LocalDateTime.now().minusDays(30);
    userRepository.deleteByDeletedAtBefore(deadline);
}

九、合规检查清单 ​

text
身份认证
├── 双因素(密码 + 短信 / 令牌)
├── 密码策略(长度、复杂度、过期)
└── 登录失败锁定

访问控制
├── RBAC 模型
├── 最小权限
└── 定期审查

数据保护
├── 传输加密(HTTPS)
├── 存储加密(字段级)
├── 密钥管理(KMS)
└── 脱敏展示

审计
├── 关键操作审计
├── 日志保留 6 个月
├── 不可篡改
└── 异地备份

事件响应
├── 安全事件预案
├── 应急响应流程
└── 上报机制

十、审计与监控联动 ​

java
// 审计日志 + 告警
@EventListener
public void onAuditLog(AuditLog log) {
    // 1. 异常登录
    if ("LOGIN_FAILURE".equals(log.getAction())) {
        if (failureCount(log.getUserId()) > 10) {
            alertService.send("异常登录:userId=" + log.getUserId());
        }
    }

    // 2. 敏感操作
    if (SENSITIVE_ACTIONS.contains(log.getAction())) {
        auditLogMonitor.record(log);
    }

    // 3. 大批量删除
    if (log.getAction().startsWith("DELETE_")) {
        if (deleteCount(log.getUserId()) > 100) {
            alertService.send("批量删除:userId=" + log.getUserId());
        }
    }
}

⚠️ 坑 3:审计日志只记录不告警,出问题才发现。审计事件 + 实时告警。

本章小结 ​

场景合规要求
通用等保三级
跨境GDPR
国内个人信息保护法
支付PCI DSS
关键点建议
审计日志独立存储,不可篡改
保留期6 个月起
个人信息最小化,删除权
跨境安全评估

动手练习 ​

  1. 审计日志:用 AOP 记录用户删除操作,独立存储
  2. 数据删除:实现用户注销,关联数据匿名化
  3. 数据导出:实现用户数据导出 JSON,符合 GDPR
  4. 保留策略:为不同数据配置保留期,定时清理

下一章:第 35 章:架构演进 →

本站基于 VitePress 构建 · 由 StackHub 团队维护