第 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.3java
// 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 个月起 |
| 个人信息 | 最小化,删除权 |
| 跨境 | 安全评估 |
动手练习
- 审计日志:用 AOP 记录用户删除操作,独立存储
- 数据删除:实现用户注销,关联数据匿名化
- 数据导出:实现用户数据导出 JSON,符合 GDPR
- 保留策略:为不同数据配置保留期,定时清理
下一章:第 35 章:架构演进 →