Audit (Spring Boot)
Introduction
Three approaches to introducing auditing into an application:
[1/3] Auditing with JPA (@PrePersist, @PreUpdate and @PreRemove)
[2/3] Hibernate Envers (easy auditing / versioning)
[3/3] Spring Data JPA (@CreatedDate, @CreatedBy, @LastModifiedDate, @LastMofifiedBy)
References
Auditing with JPA, Hibernate, and Spring Data JPA
https://www.baeldung.com/database-auditing-jpa
Hibernate Envers – Extend the standard revision
https://thorben-janssen.com/hibernate-envers-extend-standard-revision/
Hibernate Envers – Query data from your audit log
https://thorben-janssen.com/hibernate-envers-query-data-audit-log/
Ways to pass additional data to Custom RevisionEntity in Hibernate Envers?
It might be used, eg, for storing who hard-deleted a record.
JPA Entity Lifecycle Events
https://www.baeldung.com/jpa-entity-lifecycle-events
Hibernate Envers - Easy Entity Auditing
https://docs.jboss.org/envers/docs/
Envers generated tables and their content
https://docs.jboss.org/envers/docs/#tables
Maintain the data versioning info with Spring Data — Envers (medium)
Audit entities with Hibernate Envers
https://adamzareba.github.io/Audit-entities-with-Hibernate-Envers/
Versioning and Auditing with Hibernate Envers
https://bytefish.de/blog/hibernate_envers_versioning_and_auditing/
The best way to implement an audit log using Hibernate Envers
https://vladmihalcea.com/the-best-way-to-implement-an-audit-log-using-hibernate-envers/
Entity technical fields
How these fields are exactly filled will depend on which of the auditing strategies is used.
BaseEntity.java
package edu.cou.myapp.audit;
/**
* Technical fields common to all entities.
*/
@NoArgsConstructor
@Data
@MappedSuperclass
public class BaseEntity {
/**
* JPA optimistic locking.
* Technical field only nullable in object (in table: not null default 0).
*/
@Version
@Column(name = "version", nullable = false)
@Nullable
private Long version;
/**
* Created instant.
* In table: datetime not null default current_timestamp
*/
@CreationTimestamp
@Column(name = "created_instant", nullable = false, updatable = false)
private Instant createdInstant;
/**
* Created by (id).
* Set on @PrePersist. In table: bigint nullable
*/
@Column(name = "created_by", nullable = true, updatable = false)
@Nullable
private Long createdBy;
/**
* Modified instant. In table: datetime not null default current_timestamp
* Set on @PrePersist, @PreUpdate.
*/
@UpdateTimestamp
@Column(name = "modified_instant", nullable = false)
private Instant modifiedInstant;
/**
* Modified by (id). In table: bigint nullable
* Set on @PrePersist, @PreUpdate.
*/
@Column(name = "modified_by", nullable = true)
@Nullable
private Long modifiedBy;
/**
* Inform current user, if known
*/
@PrePersist
public void preInsert() {
this.createdBy = SecurityHelper.getCurrentId().orElse(null);
this.modifiedBy = this.createdBy;
}
/**
* Inform current user, if known
*/
@PreUpdate
public void preUpdate() {
this.modifiedBy = SecurityHelper.getCurrentId().orElse(null);
}
}
Whatever.java
package edu.cou.myapp;
@NoArgsConstructor
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Data
@Table(name = "whatever")
@Entity
public class Whatever extends BaseEntity {
...
}
[1/3] Auditing with JPA (@PrePersist, @PreUpdate and @PreRemove)
Eg: https://www.baeldung.com/database-auditing-jpa#auditing
[2/3] Hibernate Envers (easy auditing / versioning)
Hibernate sequences (liquibase)
Note: With Liquibase >= 4.0.0 use lowercase "_aud" suffix instead of "_AUD" (because of case sensitivity with PostgreSQL).
If necessary, application.properties could be set (not recommended):
# spring.jpa.properties.org.hibernate.envers.audit_table_suffix=_AUD
Remark: When an Envers application using Hibernate <6 is migrated for Hibernate >=6, it fails because it doesn't find the sequence 'revinfo_seq'. It can be fixed reverting the Hibernate naming strategy in application.properties:
#DOC {standard (>=6 default), legacy (>=5.3, <6), single (<5.3), custom class} MYAPP uses legacy cause it was created before hibernate 6
spring.jpa.properties.hibernate.id.db_structure_naming_strategy=legacy
[1/2] For Hibernate >=6 (revinfo_rev_seq)
Eg: H2 - Hibernate 6.2.5 - Spring Boot 3.1.1 (as automatically generated by Hibernate):
#Only table is created
CREATE MEMORY TABLE "PUBLIC"."REVINFO"(
"REV" INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1 RESTART WITH 3) DEFAULT ON NULL NOT NULL,
"REVTSTMP" BIGINT
);
ALTER TABLE "PUBLIC"."REVINFO" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_6C" PRIMARY KEY("REV");
Eg: PosgreSQL - Hibernate 6.2.5 - Spring Boot 3.1.1 (as automatically generated by Hibernate):
#Both table and sequence are created
CREATE TABLE revinfo (
rev int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY( INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1 NO CYCLE),
revtstmp int8 NULL,
CONSTRAINT revinfo_pkey PRIMARY KEY (rev)
);
CREATE SEQUENCE public.revinfo_rev_seq
INCREMENT BY 1
MINVALUE 1
MAXVALUE 9223372036854775807
START 1
CACHE 1
NO CYCLE;
Envers seems to be able to use either field level "revinfo_rev_seq" or table level "revinfo_seq":
# hibernate_sequence needed by Hibernate Envers - Easy Entity Auditing
# H2 sequence data type is BIGINT [https://www.h2database.com/html/commands.html#create_sequence]
databaseChangeLog:
- changeSet:
id: "002"
author: thatsme
failOnError: true
comment: "Create sequence for 'revinfo' pk, needed by envers audit"
changes:
- createSequence:
sequenceName: revinfo_rev_seq
incrementBy: 1
cycle: false
cacheSize: 1
#minValue: (int)-2147483648 (long)-9223372036854775808
minValue: 1
#maxValue: (int)2147483647 (long)9223372036854775807
maxValue: 9223372036854775807
startValue: 1
[2/2] For Hibernate <6 (hibernate_sequence)
By default, Hibernate <6 uses the sequence "hibernate_sequence" for the "revinfo" table primary key.
Liquibase script for creating this sequence can be found at page "Hibernate ORM (JPA)":
https://sites.google.com/site/pawneecity/java-development/hibernate-java#h.9fenronhfrt9
revinfo (liquibase)
Envers uses a revision info table to store the revision number, the timestamp and (if extended) any another information , eg: who inserted, updated or deleted a record.
WARNING: It's highly recommended to always use a 'CustomRevisionEntity' that extends 'DefaultRevisionEntity' to avoid later changes to field names (see CustomRevisionEntity).
Extending DefaultRevisionEntity (as of hibernate-envers-6.2.6.Final)
databaseChangeLog:
- changeSet:
id: "003"
author: thatsme
failOnError: true
comment: "Creation of table revinfo (extending DefaultRevisionEntity)"
changes:
- createTable:
tableName: revinfo
columns:
- column:
name: id
type: INTEGER
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: timestamp
type: bigint
- column:
name: user_id
type: bigint
remarks: Who inserted, updated or deleted a record (custom)
Without extending DefaultRevisionEntity (as of hibernate-envers-6.2.6.Final)
databaseChangeLog:
- changeSet:
id: "003"
author: thatsme
failOnError: true
comment: "Creation of table revinfo (not extending DefaultRevisionEntity)"
changes:
- createTable:
tableName: revinfo
columns:
- column:
name: rev
type: bigint
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: revtstmp
type: bigint
- column:
name: user_id
type: bigint
remarks: Who inserted, updated or deleted a record (custom)
pom.xml (envers)
Tested w/ Spring Boot 3.2.0
<dependencies>
<!-- Audit: Envers queries available in repositories for Spring Data JPA -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-envers</artifactId>
</dependency>
</dependencies>
Sample table and its corresponding _aud (liquibase)
Let's create a sample table "app_request" and its corresponding auditing one "app_request_aud".
The primary key of the _aud table is the combination of the original primary key plus 'rev'.
Note: The 'rev' field must be of the same type than in table 'revinfo' (probable a "bigint").
Auditing type 'revtype' values are:
0=ADD - Indicates that the entity was added (persisted) at that revision.
1=MOD - Indicates that the entity was modified (one or more of its fields) at that revision.
2=DEL - Indicates that the entity was deleted (removed) at that revision.
databaseChangeLog:
- changeSet:
id: "029"
author: thatsme
failOnError: true
comment: "Creation of table app_request and its auditing"
changes:
- createTable:
tableName: app_request
columns:
- column:
name: uuid
type: uuid
constraints:
nullable: false
primaryKey: true
remarks: "Request identifier"
- column:
name: create_instant
type: timestamp
defaultValueComputed: current_timestamp
constraints:
nullable: false
remarks: "Create instant"
- column:
name: update_idp
type: bigint
constraints:
nullable: true
remarks: "Last update idp"
- column:
name: version
type: bigint
constraints:
nullable: false
defaultValue: 0
remarks: "JPA version"
- column:
name: update_instant
type: timestamp
defaultValueComputed: current_timestamp
constraints:
nullable: false
remarks: "Last update instant"
- createTable:
tableName: app_request_aud
columns:
- column:
name: uuid
type: uuid
constraints:
nullable: false
primaryKey: true
primaryKeyName: PK_APP_REQUEST_aud
remarks: "Request identifier"
- column:
name: rev
type: bigint
constraints:
nullable: false
primaryKey: true
primaryKeyName: PK_APP_REQUEST_aud
remarks: "Auditing revision"
- column:
name: revtype
type: tinyint
remarks: "Auditing type. 0=ADD, 1=MOD, 2=DEL"
- column:
name: create_instant
type: timestamp
- column:
name: update_idp
type: bigint
- column:
name: version
type: bigint
- column:
name: update_instant
type: timestamp
- addForeignKeyConstraint:
baseColumnNames: rev
baseTableName: app_request_aud
constraintName: fk_app_request_aud_revinfo
referencedColumnNames: rev
referencedTableName: revinfo
App (Spring Boot entry point)
Notice the @EnableEnversRepositories annotation (before Spring Boot 2.5 use instead @EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class)).
package edu.cou.myapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
* Myapp app.<br>
*/
@Configuration
@ComponentScan(basePackages = { "edu.cou.myapp" })
@EnableEnversRepositories
@SpringBootApplication
public class App {
/**
* Main entry point.
*
* @param args -
*/
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
app_request (JPA Entity)
Notice the @Audited annotation
@Component
@SuppressFBWarnings
@Table(name = "app_request")
@NoArgsConstructor
@Data
@Audited
@Entity
public class AppRequest implements Serializable {
/**
*
*/
private static final long serialVersionUID = 2045704706981241836L;
/**
* Do never set this value, managed by JPA.
*/
@Column(name = "version", nullable = false)
@Version
private Long version;
/**
* Do never set this value, implicitly assigned.
*/
@Column(name = "update_instant", nullable = false)
@UpdateTimestamp
private Instant updateInstant;
/**
* Do always set this value (idp of the current user).
*/
@Column(name = "update_idp", nullable = false)
private Long updateIdp;
/**
*
*/
@Id
@Column(name = "uuid")
@Getter(AccessLevel.PUBLIC)
@Setter(AccessLevel.PUBLIC)
private UUID uuid;
@Column(name = "create_instant")
@Getter(AccessLevel.PUBLIC)
@Setter(AccessLevel.PUBLIC)
@UpdateTimestamp
private Instant createInstant;
/**
* Constructor.
*
* @param updateIdp
* @param uuid
*/
public AppRequest(final @Nullable Long updateIdp, final @NotNull UUID uuid) {
this();
this.updateIdp = updateIdp;
this.uuid = uuid;
}
}
app_request (JPA Repository)
Notice that it must extend 'RevisionRepository', which adds the following 4 methods:
findLastChangeRevision(ID id) — Lastest revision of the given entity.
findRevisions(ID id) — All Revisions of an entity with the given id.
findRevisions(ID id, Pageable pageable) — Page of revisions for the entity with the given id. It’s not guaranteed support for sorting by all properties.
findRevision(ID id, N revisionNumber) — The entity with the given ID in the given revision number.
@Repository
public interface AppRequestRepo extends JpaRepository<AppRequest, UUID>,
RevisionRepository<AppRequest, UUID, Long> {
/**
* It removes records which 'created_instant' is older than specified instant.<br>
* <br>
*
* @param instant -
* @return -
*/
long deleteByCreateInstantBefore(final Date instant);
/**
* @param uuid
* @return -
*/
@Transactional
Long deleteByUuid(UUID uuid);
/**
* @param uuid
* @return -
*/
Optional<AppRequest> findByUuid(UUID uuid);
/**
* @return -
*/
@Query(nativeQuery = true, value = "SELECT COUNT(*) FROM app_request")
Long getAppRequestRows();
}
Controller operation for retrieving revisions of a given entity
/**
* Sample Envers query. <br>
*
* @param rId
* @return All revisions for given entity identifier.
*/
@Operation(summary = "Revisions for given entity identifier", security = { @SecurityRequirement(
name = "bearer-key") }, parameters = { @Parameter(name = "Authorization",
in = ParameterIn.HEADER, required = true) })
@PreAuthorize("hasAuthority('SUPER')")
@GetMapping(value = "/revisions")
public ResponseEntity<Revisions<Integer, AppRequest>> getCalendarTfGlobalConfigs(//
@Parameter(description = "Request unique identifier", required = true,
example = "3fa85f64-5717-4562-b3fc-2c963f66afa6") @PathVariable("rId") UUID rId //
) {
Revisions<Integer, AppRequest> ret = this.appRequestRepository.findRevisions(rId);
//
return new ResponseEntity<Revisions<Integer, AppRequest>>(ret, HttpStatus.OK);
}
Warning: Instances of class Revisions don't usually serialize fine in JSON. Error thrown, in debug mode, is:
Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Index -1 out of bounds for length 0]
An alternative w/out metadata is shown in the following example:
@Operation(summary = "[SUPER] Revisions for given entity identifier", security = {
@SecurityRequirement(name = "bearer-key") })
@PreAuthorize("hasAuthority('SUPER')")
@GetMapping(value = "/{id}/revisions")
public ResponseEntity<?> getRevisions(//
@Parameter(description = "Id", required = true,
example = "color_red") @PathVariable("id") @NotBlank String mid
) {
Revisions<Long, Translation> revisions = this.translationService.getTranslationRevisions(id);
List<Revision<Long, Translation>> revisionsList = revisions.getContent();
// Returning only records, w/out metadata
List<Translation> ret = new ArrayList<>();
for (Revision<Long, Translation> x : revisionsList) {
ret.add(x.getEntity());
}
return new ResponseEntity<>(ret, HttpStatus.OK);
}
Who inserted, updated or deleted a record?
By extending Envers table 'revinfo' w/ custom field 'user_id', we'll also be able to know who deleted a record
AuditConfig.java
package edu.cou.myapp;
@EnableEnversRepositories
@Configuration
public class AuditConfig {
}
CustomRevisionListener.java
package edu.cou.myapp.audit;
public class CustomRevisionListener implements RevisionListener {
@Override
public void newRevision(Object revisionEntity) {
CustomRevisionEntity rev = (CustomRevisionEntity) revisionEntity;
/*- DOC. If you use spring security, you could use SpringSecurityContextHolder:
* final UserContext userContext = UserContextHolder.getUserContext()
*/
/*- Populate the id of the current user */
Optional<Long> userIdOpt =SecurityHelper.getCurrentId();
rev.setUserId(userIdOpt.orElse(null));
}
}
CustomRevisionEntity.java (extending DefaultRevisionEntity
package edu.cou.myapp.audit;
/**
* Envers custom 'revinfo' table.
*
* Add userId to the Envers revision table. It will, also, allow to know who has deleted a record.
*
* <pre>
* WARNING: Extending DefaultRevisionEntity changes the default Envers field names.
* https://stackoverflow.com/questions/12736811/why-does-overriding-change-column-names
* https://hibernate.atlassian.net/browser/HHH-11325
*
* "rev" -> "id"
* "revtstmp" -> "timestamp"
* </pre>
*/
@Data
@EqualsAndHashCode(callSuper=true)
@RevisionEntity(CustomRevisionListener.class)
@Table(name = "revinfo")
@Entity
public class CustomRevisionEntity extends DefaultRevisionEntity{
private static final long serialVersionUID = -8490066597393478856L;
/*- Table field 'user_id' */
@Nullable
private Long userId;
}
CustomRevisionEntity.java (workaround w/out extending DefaultRevisionEntity)
package edu.cou.myapp.audit;
/**
* Envers custom 'revinfo' table.
*
* Add userId to the Envers revision table. It will, also, allow to know who has deleted a record.
*
* <pre>
* WARNING: Extending DefaultRevisionEntity changes the default Envers field names.
* https://stackoverflow.com/questions/12736811/why-does-overriding-change-column-names
* https://hibernate.atlassian.net/browser/HHH-11325
*
* "rev" -> "id"
* "revtstmp" -> "timestamp"
* </pre>
*/
@Data
@EqualsAndHashCode(callSuper=true)
@RevisionEntity(CustomRevisionListener.class)
@Table(name = "revinfo")
@Entity
public class CustomRevisionEntity /*extends DefaultRevisionEntity*/ implements Serializable {
private static final long serialVersionUID = 5012215631880447514L;
/*- Workaround, don't do this in new projects
* https://stackoverflow.com/questions/12736811/why-does-overriding-change-column-names
*/
@EqualsAndHashCode.Include
@Id
@GeneratedValue
@RevisionNumber
private long rev;
/*- Workaround, don't do this in new projects
* https://stackoverflow.com/questions/12736811/why-does-overriding-change-column-names
*/
@EqualsAndHashCode.Include
@RevisionTimestamp
private long revtstmp;
/*- Table field 'user_id' */
@Nullable
private Long userId;
@Override
public String toString() {
return "CustomRevisionEntity(revId = " + this.rev + ", revDate = " + DateTimeFormatter.INSTANCE
.format(new Date(this.revtstmp)) + ")";
}
}
Car.java (entity for testing purposes)
package edu.cou.myapp.entity;
@NoArgsConstructor
@RequiredArgsConstructor
@Data
@EqualsAndHashCode(callSuper=true)
@Audited
@Entity
@Table(name = "car")
public class Car extends BaseEntity{
/**
* Record identifier (Vehicle Identification Number).
*/
@Id
@Column(name = "vin", nullable = false)
@NonNull
private Long vin;
@Column(name = "plate", nullable = true)
@Nullable
private String plate;
}
CarRepo.java
package edu.cou.myapp.repo;
public interface CarRepo extends JpaRepository<Car, Long>, RevisionRepository<Car, Long, Long> {
}
SecurityHelper.java
package edu.cou.myapp.security;
@UtilityClass
public class SecurityHelper {
/** Fake user id form testing purposes only */
public static final Long USER_ID_FAKE = 2727L;
/** Simulate, for testing purposes only, getting the user id from the Spring Security Context */
public static Optional<Long> getCurrentId() {
return Optional.ofNullable(USER_ID_FAKE);
}
}
application.properties (main)
spring.jpa.open-in-view=false
spring.datasource.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=LEGACY
spring.datasource.username=sa
CasrTest.java (unit test)
package edu.cou.myapp.entity;
@Slf4j
@DataJpaTest
class CarTest {
@Inject
private PlatformTransactionManager platformTransactionManager;
@Inject
private ApplicationContext applicationContext;
@Inject
private CarRepo carRepo;
/**
* Delete an entity and ensure Envers audit stores the current userId.
*
* @throws InterruptedException
*/
@WithMockUser(username = "johndoe", authorities = { "SUPER", "MANAGER" })
@Test
void test() throws InterruptedException {
TransactionTemplate tx = new TransactionTemplate(this.platformTransactionManager);
tx.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
Car e = new Car(556699L);
Car eSaved = tx.execute(status -> this.carRepo.save(e));
log.info(">>> Created: " + eSaved.getCreatedInstant());
log.info(">>> Created: " + eSaved.getCreatedBy());
log.info(">>> Modified: " + eSaved.getModifiedInstant());
log.info(">>> Modified: " + eSaved.getModifiedBy());
// Delete the record
tx.executeWithoutResult(status -> this.carRepo.delete(eSaved));
JpaTransactionManager tm = this.applicationContext.getBean(JpaTransactionManager.class);
EntityManagerFactory emf = tm.getEntityManagerFactory();
tx.executeWithoutResult(status -> {
EntityManager em = EntityManagerFactoryUtils.getTransactionalEntityManager(emf);
// Ensure that the userId that deleted the record has been saved by Envers audit
AuditReader auditReader = AuditReaderFactory.get(em);
AuditQuery q = auditReader.createQuery().forRevisionsOfEntity(Car.class, false, true);
// q.addProjection(AuditEntity.revisionNumber());
q.add(AuditEntity.revisionProperty("userId").eq(SecurityHelper.USER_ID_FAKE));
List<?> revs = q.getResultList();
// Encure there are exactly 2 revisions (insert + delete)
Assertions.assertEquals(2, revs.size());
// The 2nd revision is the DELETE
Object[] rev2 = (Object[]) revs.get(1);
// The 2nd element of the array is the CustomRevisionEntity
CustomRevisionEntity cre2 = (CustomRevisionEntity) rev2[1];
log.info(cre2.toString());
// Ensure the userId has been saved in the revision of the DELETE
Assertions.assertEquals(SecurityHelper.USER_ID_FAKE, cre2.getUserId());
});
}
}
Issues (envers)
"revisionType": "UNKNOWN"
Issue is fixed and verified to work out as expected on Spring Boot 2.3.1.RELEASE
See:
https://github.com/spring-projects/spring-data-envers/issues/215
[3/3] Spring Data JPA (@CreatedDate, @CreatedBy, @LastModifiedDate, @LastModifiedBy)
I never was able to make it work out together with Hibernate Envers.
Eg: https://www.baeldung.com/database-auditing-jpa#spring
In other projects w/ out Envers, eg ssz, it worked out fine:
MyEntity.java
package edu.cou.myapp.entity;
@EntityListeners(AuditingEntityListener.class)
@Entity
public class MyEntity {
@Column(name = "modified_by")
@LastModifiedBy
private String modifiedBy;
}
AuditConfig.java
package edu.cou.myapp;
import edu.cou.myapp.audit.AuditorAwareImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.envers.repository.config.EnableEnversRepositories;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
/**
* Audit config.
*/
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
//@EnableEnversRepositories
@Configuration
public class AuditConfig {
/*- Used by JpaAuditing */
@Bean
AuditorAware<Long> auditorProvider() {
return new AuditorAwareImpl();
}
}
AuditorAwareImpl.java
package edu.cou.myapp.audit;
import edu.cou.myapp.security.CustomUserDetails;
import java.util.Optional;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* Used by JpaAuditing
*/
public class AuditorAwareImpl implements AuditorAware<Long> {
@Override
public Optional<Long> getCurrentAuditor() {
var authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.empty();
}
return Optional.of(((CustomUserDetails) authentication.getPrincipal()).getId());
}
}