1、概览 {#1概览}
就 ORM 而言,数据库审计指的是跟踪和记录与实体相关的事件,或者简单地说是实体版本管理。受 SQL 触发器的启发,这些事件是对实体的插入、更新和删除操作。数据库审计的好处类似于源代码版本控制。
本文将带你了解在应用中使用审计的三种方法。首先介绍来自于 JPA 标准的审计实现、然后再介绍由 Hibernate 和 Spring Data 分别提供的审计的扩展实现。
下面是本文中使用的相关实体 Bar
和 Foo
示例:
2、JPA 审计 {#2jpa-审计}
JPA 并没有明确包含审计 API,但我们可以通过使用实体生命周期事件来实现这一功能。
2.1、@PrePersist、@PreUpdate 和 @PreRemove {#21prepersistpreupdate-和-preremove}
在 JPA 实体类中,我们可以使用 @PrePersist
、@PreUpdate
和 @PreRemove
注解指定一个方法作为回调,以在相应的 DML 操作前执行它。
@Entity
public class Bar {
@PrePersist
public void onPrePersist() { ... }
@PreUpdate
public void onPreUpdate() { ... }
@PreRemove
public void onPreRemove() { ... }
}
实体内部的回调方法应是非 static
的,返回类型为 void
,且不带参数。访问权限任意。
注意,JPA 中的
@Version
注解与本文的主题没有关系,它是一种乐观锁,与审计数据无关。
2.2、实现回调方法 {#22实现回调方法}
不过,这种方法有一个很大的限制。正如 JPA 2 规范(JSR 317)所述:
一般来说,可移植应用的生命周期方法不应在同一持久化上下文(Persistence Context)中调用
EntityManager
或Query
操作、访问其他实体实例或修改关系。生命周期回调方法可修改被调用实体的非关联状态。
在没有审计框架的情况下,我们必须手动维护数据库 Schema 和 Domain Model。
在本例中,我们为实体添加两个新属性(只能管理 "实体的非关联状态")。
operation
属性用于存储执行操作的名称。timestamp
属性用于存储执行操作的时间戳。
@Entity
public class Bar {
//...
@Column(name = "operation")
private String operation;
@Column(name = "timestamp")
private long timestamp;
//...
// get /set 方法省略
//...
@PrePersist
public void onPrePersist() {
audit("INSERT");
}
@PreUpdate
public void onPreUpdate() {
audit("UPDATE");
}
@PreRemove
public void onPreRemove() {
audit("DELETE");
}
private void audit(String operation) {
setOperation(operation);
setTimestamp((new Date()).getTime());
}
}
如果我们需要在多个类中添加此类审计,可以使用 @EntityListeners
来集中管理:
// 指定监听回调类
@EntityListeners(AuditListener.class)
@Entity
public class Bar { ... }
public class AuditListener {
// 监听回调类中的事件方法
@PrePersist
@PreUpdate
@PreRemove
private void beforeAnyOperation(Object object) { ... }
}
3、Hibernate Envers {#3hibernate-envers}
在 Hibernate 中,我们可以利用拦截器(Interceptor
)和事件监听器(EventListener
)以及数据库触发器来完成审计。
然而,ORM 框架提供了 Envers 模块,它可以实现实体类的审计和版本控制。
3.1、Envers 入门 {#31envers-入门}
要使用 Envers,需要在项目中添加 hibernate-envers
依赖:
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-envers</artifactId>
<version>6.4.4.Final</version>
</dependency>
然后,在 @Entity
上(审核整个实体)或在特定的 @Column
上(只审计特定属性)添加 @Audited
注解:
@Entity
@Audited
public class Bar { ... }
注意,Bar
与 Foo
是一对多的关系。在这种情况下,我们要么在 Foo
上添加 @Audited
来审计 Foo
,要么在 Bar
的关系属性上设置 @NotAudited
:
@OneToMany(mappedBy = "bar")
@NotAudited
private Set<Foo> fooSet;
3.2、创建审计日志表 {#32创建审计日志表}
创建审计表有几种方法:
- 将
hibernate.hbm2ddl.auto
设置为create
、create-drop
或update
,以便 Envers 自动创建它们。 - 使用
org.hibernate.tool.EnversSchemaGenerator
以编程式导出完整的数据库 Schema。 - 设置 Ant task 以生成适当的 DDL 语句。
- 使用 Maven 插件从映射(如 Juplo)生成数据库 Schema,以导出 Envers Schema(适用于 Hibernate 4 及更高版本)
这里采用第一种方法,因为这是最直接的方法,但要注意,在生产中使用 hibernate.hbm2ddl.auto
并不安全。
在本例中,bar_AUD
和 foo_AUD
(如果将 Foo
也设置为 @Audited
)表会自动生成。审计表会从实体表中复制所有需要审计的字段,并包含两个额外的字段 REVTYPE
(值为 0
表示添加,1
表示更新,2
表示更新)和 REV
。
除此之外,默认情况下还会额外生成一个名为 REVINFO
的表。它包括两个重要字段:REV
和 REVTSTMP
,并记录每次修订的时间戳。我们其实可以猜到,bar_AUD.REV
和 foo_AUD.REV
实际上是 REVINFO.REV
的外键。
3.3、配置 Envers {#33配置-envers}
可以像配置其他 Hibernate 属性一样配置 Envers 属性。
例如,通过 org.hibernate.envers.audit_table_suffix
属性把审计表的后缀(默认为 _AUD
)改为 _AUDIT_LOG
:
Properties hibernateProperties = new Properties();
hibernateProperties.setProperty(
"org.hibernate.envers.audit_table_suffix", "_AUDIT_LOG");
sessionFactory.setHibernateProperties(hibernateProperties);
完整可用的属性列表,请参阅 Envers 文档。
3.4、查询实体历史的记录 {#34查询实体历史的记录}
我们可以通过类似于使用 Hibernate Criteria
API 查询数据的方式来查询历史数据。
使用 AuditReader
接口访问实体的审计历史记录,通过 AuditReaderFactory
从打开的 EntityManager
或 Session
中获取该接口:
AuditReader reader = AuditReaderFactory.get(session);
Envers 提供了 AuditQueryCreator
(由 AuditReader.createQuery()
返回),以便创建特定于审计的查询。下面的一行代码会返回在修订 #2
时修改的所有 Bar
实例(其中 bar_AUDIT_LOG.REV = 2
):
AuditQuery query = reader.createQuery()
.forEntitiesAtRevision(Bar.class, 2)
下面是查询 Bar
修订情况的方法。这将返回一个包含所有受审计的 Bar
实例及其所有状态的列表:
AuditQuery query = reader.createQuery()
.forRevisionsOfEntity(Bar.class, true, true);
如果第二个参数为 false
,结果将与 REVINFO
表连接。否则,只返回实体实例。最后一个参数指定是否返回已删除的 Bar
实例。
然后,就可以使用 AuditEntity
工厂类来指定约束条件:
query.addOrder(AuditEntity.revisionNumber().desc());
4、Spring Data JPA {#4spring-data-jpa}
Spring Data JPA 是一个 JPA 的扩展框架,它在 JPA Provider 之上添加了一个额外的抽象层。该层支持通过继承 Spring JPA Repository 接口来创建 JPA Repository。
我们可以继承 CrudRepository<T, ID extends Serializable>
,这是用于通用 CRUD 操作的接口。只要我们创建了 Repository 并将其注入另一个组件,Spring Data 就会自动提供实现,我们就可以添加审计功能了。
4.1、启用 JPA 审计 {#41启用-jpa-审计}
首先,需要通过注解配置启用审计。在 @Configuration
类中添加 @EnableJpaAuditing
:
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories
@EnableJpaAuditing // 启用 JPA 审计
public class PersistenceConfig { ... }
4.2、 添加 Spring 实体回调监听器 {#42-添加-spring-实体回调监听器}
如上所述,JPA 提供了 @EntityListeners
注解来指定回调监听器类。
Spring Data 提供了自己的 JPA 实体监听器类 AuditingEntityListener
。
为 Bar
实体指定 Spring 的实体回调监听器:
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar { ... }
现在,我们可以通过监听器在持久化和更新 Bar
实体时捕获审计信息。
4.3、跟踪创建日期和最后修改日期 {#43跟踪创建日期和最后修改日期}
接下来,为 Bar
实体添加两个新属性,用于存储创建日期和最后修改日期。并相添加应的 @CreatedDate
和 @LastModifiedDate
注解,其值会被自动设置:
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar {
//...
// 创建时间
@Column(name = "created_date", nullable = false, updatable = false)
@CreatedDate
private long createdDate;
// 修改时间
@Column(name = "modified_date")
@LastModifiedDate
private long modifiedDate;
//...
}
一般来说,应该将这些审计属性移至一个 Base
类(用 @MappedSuperClass
进行注解),所有需要审计的实体直接继承该 Base 类。在本例中,为了简单起见,直接将它们添加到了 Bar
实体中。
4.4、使用 Spring Security 审计操作人 {#44使用-spring-security-审计操作人}
如果我们的应用使用了 Spring Security,我们就可以跟踪创建人和最后修改人:
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar {
//...
// 创建人
@Column(name = "created_by")
@CreatedBy
private String createdBy;
// 修改人
@Column(name = "modified_by")
@LastModifiedBy
private String modifiedBy;
//...
}
使用 @CreatedBy
和 @LastModifiedBy
注解的列将填充为创建或最后修改实体的用户的名称。
该信息来自 SecurityContext
的 Authentication
实例。如果想要自定义设置注解字段的值,那么可以实现 AuditorAware<t>
接口:
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public String getCurrentAuditor() {
// 自定义逻辑
}
}
要配置应用使用 AuditorAwareImpl
查找当前 Principal(认证用户),需要声明一个 AuditorAware
类型的 Bean,并用 AuditorAwareImpl
实例进行初始化,然后将该 Bean 的名称指定为 @EnableJpaAuditing
的 auditorAwareRef
参数的值:
@EnableJpaAuditing(auditorAwareRef="auditorProvider")
public class PersistenceConfig {
//...
@Bean
AuditorAware<String> auditorProvider() {
return new AuditorAwareImpl();
}
//...
}
5、总结 {#5总结}
本文介绍了三种实现审计功能的方法:
- 纯粹的 JPA 方法是最基本的方法,它包括使用生命周期回调。然而,我们只允许修改实体的非关联状态。这使得
@PreRemove
回调对我们的目的无用,因为方法中进行的任何设置都将随实体一起被删除。 - Envers 是 Hibernate 提供的成熟的审计模块。它具有高度可配置性,并且不会出现纯 JPA 实现的缺陷。因此,它允许我们对删除操作进行审计,因为它会将日志记录到实体表以外的其他表中。
- Spring Data JPA 方法对 JPA 回调进行了抽象,并为审计属性提供了方便的注解。它还可以与 Spring Security 集成。缺点是它继承了 JPA 方法的相同缺陷,因此无法对删除操作进行审计。
你可以根据你的需求,选择合适的方法。
Ref:https://www.baeldung.com/database-auditing-jpa