Search code examples
javahibernatejpamappingaudit-logging

Spring Boot Auditing Hostname and HostIp


I have an auditing entity which defined like this:

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.io.Serializable;
import java.time.Instant;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;

/**
 * Base abstract class for entities which will hold definitions for created, last modified, created by,
 * last modified by attributes.
 */
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractAuditingEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    @CreatedBy
    @Column(name = "created_by", nullable = false, length = 50, updatable = false)
    @JsonIgnore
    private String createdBy;

    @CreatedDate
    @Column(name = "created_date", updatable = false)
    @JsonIgnore
    private Instant createdDate = Instant.now();

    @LastModifiedBy
    @Column(name = "last_modified_by", length = 50)
    @JsonIgnore
    private String lastModifiedBy;

    @LastModifiedDate
    @Column(name = "last_modified_date")
    @JsonIgnore
    private Instant lastModifiedDate = Instant.now();

    public String getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(String createdBy) {
        this.createdBy = createdBy;
    }

    public Instant getCreatedDate() {
        return createdDate;
    }

    public void setCreatedDate(Instant createdDate) {
        this.createdDate = createdDate;
    }

    public String getLastModifiedBy() {
        return lastModifiedBy;
    }

    public void setLastModifiedBy(String lastModifiedBy) {
        this.lastModifiedBy = lastModifiedBy;
    }

    public Instant getLastModifiedDate() {
        return lastModifiedDate;
    }

    public void setLastModifiedDate(Instant lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }
 }

And this is the implementation:

import com.app.derin.uaa.config.Constants;

import java.util.Optional;

import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;

/**
 * Implementation of {@link AuditorAware} based on Spring Security.
 */
@Component
public class SpringSecurityAuditorAware implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.of(SecurityUtils.getCurrentUserName().orElse(Constants.SYSTEM_ACCOUNT));
    }    
}

But I also need to store Hostname of request ip ( if it can be acquired from browser/dns if not it can be null) and request ip.

I found some examples and i changed my class to this:

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.hibernate.annotations.ColumnDefault;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.io.Serializable;
import java.time.Instant;


@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractAuditingEntity<T> implements Serializable {

    @CreatedDate
    @JsonIgnore
    @Column(name = "created_date", updatable = false)
    private Instant createdDate = Instant.now();

    @CreatedBy
    @JsonIgnore
    @Column(name = "created_by",  updatable = false)
    @Embedded
    private T createdBy;

    @LastModifiedDate
    @Column(name = "modified_date")
    @JsonIgnore
    private Instant modifiedDate = Instant.now();;

    @LastModifiedBy
    @Column(name = "modified_by")
    @JsonIgnore
    @Embedded
    private T modifiedBy;

    public Instant getCreatedDate() {
        return createdDate;
    }

    public void setCreatedDate(Instant createdDate) {
        this.createdDate = createdDate;
    }

    public T getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(T createdBy) {
        this.createdBy = createdBy;
    }

    public Instant getModifiedDate() {
        return modifiedDate;
    }

    public void setModifiedDate(Instant modifiedDate) {
        this.modifiedDate = modifiedDate;
    }

    public T getModifiedBy() {
        return modifiedBy;
    }

    public void setModifiedBy(T modifiedBy) {
        this.modifiedBy = modifiedBy;
    }
}

And my implementation to this:

import com.app.derin.configuration.config.Constants;

import java.util.Optional;

import com.app.derin.configuration.ext.AuditorDetails;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;


/**
 * Implementation of {@link AuditorAware} based on Spring Security.
 */
@Component
public class SpringSecurityAuditorAware implements AuditorAware<AuditorDetails> {

    @Override
    public Optional<AuditorDetails> getCurrentAuditor() {


        AuditorDetails currentAuditor = new AuditorDetails();
        currentAuditor.setLoggedUser(SecurityUtils.getCurrentUserLogin().orElse(Constants.SYSTEM_ACCOUNT));
        currentAuditor.setHostIp("ip");
        return Optional.of(currentAuditor);
    }


}

But if I am right hibernate doesn't support generic types. I am getting this error:

[ERROR] Error setting up or running Liquibase:
[ERROR] org.hibernate.AnnotationException: Property com.app.derin.configuration.ext.extendedFields.AbstractAuditingEntity.createdBy has an unbound type and no explicit target entity. Resolve this Generic usage issue or set an explicit targe
t attribute (eg @OneToMany(target=) or use an explicit @Type

Is there any way to achieve this?


Solution

  • I found solution and here is how did:

    After a lot of googling i found that my problem is with hibernate annotation. I am trying to send Datatype that hibernate doesn't know about. So I changed my class to custom type for hibernate.

    More info you can check this link. It helped me.

    My AuditorDetails class:

    import java.io.Serializable;
    import java.util.Objects;
    
    public final class AuditorDetails implements Serializable {
        private final String loggedUser;
    
        private final String hostName;
    
        private final String hostIp;
    
        public AuditorDetails(String loggedUser, String hostName, String hostIp) {
            this.loggedUser = loggedUser;
            this.hostName = hostName;
            this.hostIp = hostIp;
        }
    
        public String getLoggedUser() {
            return loggedUser;
        }
    
        public String getHostName() {
            return hostName;
        }
    
        public String getHostIp() {
            return hostIp;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            AuditorDetails that = (AuditorDetails) o;
            return Objects.equals(loggedUser, that.loggedUser) &&
                Objects.equals(hostName, that.hostName) &&
                Objects.equals(hostIp, that.hostIp);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(loggedUser, hostName, hostIp);
        }
    }
    

    AuditorDetailsType class:

    import org.hibernate.HibernateException;
    import org.hibernate.engine.spi.SharedSessionContractImplementor;
    import org.hibernate.usertype.UserType;
    
    import java.io.Serializable;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.sql.Types;
    import java.util.Objects;
    
    public class AuditorDetailsType implements UserType {
    
        @Override
        public int[] sqlTypes() {
            return new int[]{Types.VARCHAR, Types.VARCHAR, Types.VARCHAR};
        }
    
        @Override
        public Class returnedClass() {
            return AuditorDetails.class;
        }
    
        @Override
        public boolean equals(Object o, Object o1) throws HibernateException {
            if(o == o1)
                return true;
            if (Objects.isNull(o) || Objects.isNull(o1))
                return false;
    
            return o.equals(o1);
        }
    
        @Override
        public int hashCode(Object o) throws HibernateException {
            return o.hashCode();
        }
    
        @Override
        public Object nullSafeGet(ResultSet resultSet, String[] strings, SharedSessionContractImplementor sharedSessionContractImplementor, Object o) throws HibernateException, SQLException {
            String loggedUser = resultSet.getString(strings[0]);
    
            if(resultSet.wasNull())
                return null;
    
            String hostName = resultSet.getString(strings[1]);
    
            String hostIp = resultSet.getString(strings[2]);
    
            AuditorDetails currentAuditor = new AuditorDetails(loggedUser, hostName, hostIp);
            return currentAuditor;
        }
    
        @Override
        public void nullSafeSet(PreparedStatement preparedStatement, Object o, int i, SharedSessionContractImplementor sharedSessionContractImplementor) throws HibernateException, SQLException {
            if (Objects.isNull(o)){
                preparedStatement.setNull(i,Types.VARCHAR);
            }
            else {
                AuditorDetails currentAuditor = (AuditorDetails) o;
                preparedStatement.setString(i,currentAuditor.getLoggedUser());
                preparedStatement.setString(i+1,currentAuditor.getHostName());
                preparedStatement.setString(i+2,currentAuditor.getHostIp());
            }
        }
    
        @Override
        public Object deepCopy(Object o) throws HibernateException {
            if (Objects.isNull(o))
                return null;
    
            AuditorDetails currentAuditor = (AuditorDetails) o;
    
            return new AuditorDetails(currentAuditor.getLoggedUser(), currentAuditor.getHostName(), currentAuditor.getHostIp());
        }
    
        @Override
        public boolean isMutable() {
            return false;
        }
    
        @Override
        public Serializable disassemble(Object o) throws HibernateException {
            return (Serializable) o;
        }
    
        @Override
        public Object assemble(Serializable serializable, Object o) throws HibernateException {
            return serializable;
        }
    
        @Override
        public Object replace(Object o, Object o1, Object o2) throws HibernateException {
            return o;
        }
    }
    

    AbstractAuditingEntity:

    import com.app.derin.uaa.ext.AuditorDetails;
    import com.app.derin.uaa.ext.AuditorDetailsType;
    import com.fasterxml.jackson.annotation.JsonIgnore;
    import org.hibernate.annotations.ColumnDefault;
    import org.hibernate.annotations.Columns;
    import org.hibernate.annotations.TypeDef;
    import org.springframework.data.annotation.CreatedBy;
    import org.springframework.data.annotation.CreatedDate;
    import org.springframework.data.annotation.LastModifiedBy;
    import org.springframework.data.annotation.LastModifiedDate;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    
    import javax.persistence.Column;
    import javax.persistence.EntityListeners;
    import javax.persistence.MappedSuperclass;
    import java.time.Instant;
    
    @TypeDef(name = "AuditorDetails",
        typeClass = AuditorDetailsType.class,
        defaultForType = AuditorDetails.class)
    @MappedSuperclass
    @EntityListeners(AuditingEntityListener.class)
    public abstract class AbstractAuditingEntity{
    
        @CreatedDate
        @JsonIgnore
        @Column(name = "created_date", updatable = false)
        private Instant createdDate = Instant.now();
    
        @CreatedBy
        @Columns(columns = {@Column(name = "created_by", updatable = false),
        @Column(name = "created_host_name", updatable = false),
        @Column(name = "created_host_ip", updatable = false)})
        private AuditorDetails createdBy;
    
        @LastModifiedDate
        @Column(name = "modified_date")
        @JsonIgnore
        private Instant modifiedDate = Instant.now();
    
        @LastModifiedBy
        @Columns(columns = {@Column(name = "modified_by"),
            @Column(name = "modified_host_name"),
            @Column(name = "modified_host_ip")})
        private AuditorDetails modifiedBy;
    
        @Column(name = "row_status")
        @JsonIgnore
        @ColumnDefault("1")
        private Integer rowStatus = 1;
    
        protected AbstractAuditingEntity() {
        }
        public AuditorDetails getModifiedBy() {
            return modifiedBy;
        }
    
        public Instant getCreatedDate() {
            return createdDate;
        }
    
        public void setCreatedDate(Instant createdDate) {
            this.createdDate = createdDate;
        }
    
        public AuditorDetails getCreatedBy() {
            return createdBy;
        }
    
        public void setCreatedBy(AuditorDetails createdBy) {
            this.createdBy = createdBy;
        }
    
        public Instant getModifiedDate() {
            return modifiedDate;
        }
    
        public void setModifiedDate(Instant modifiedDate) {
            this.modifiedDate = modifiedDate;
        }
    
        public Integer getRowStatus() {
            return rowStatus;
        }
    
        public void setRowStatus(Integer rowStatus) {
            this.rowStatus = rowStatus;
        }
    }
    

    SpringSecurityAuditorAware:

    import com.app.derin.uaa.config.Constants;
    
    import java.util.Optional;
    
    import com.app.derin.uaa.ext.AuditorDetails;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.data.domain.AuditorAware;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    
    
    /**
     * Implementation of {@link AuditorAware} based on Spring Security.
     */
    @Component
    public class SpringSecurityAuditorAware implements AuditorAware<AuditorDetails> {
    
    
        private final Logger log = LoggerFactory.getLogger(SpringSecurityAuditorAware.class);
    
        @Override
        public Optional<AuditorDetails> getCurrentAuditor() {
            ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = sra.getRequest();
            if(request != null) {
                String hostIp = getClientIpAddress(request);
                String hostName = "";
                AuditorDetails currentAuditor = new AuditorDetails(SecurityUtils.getCurrentUserName().orElse(Constants.SYSTEM_ACCOUNT),
                    hostName, hostIp);
                return Optional.of(currentAuditor);
            }
    
            return Optional.of(currentAuditor);
        }
    
        private static final String[] IP_HEADER_CANDIDATES = {
            "X-Forwarded-For",
            "Proxy-Client-IP",
            "WL-Proxy-Client-IP",
            "HTTP_X_FORWARDED_FOR",
            "HTTP_X_FORWARDED",
            "HTTP_X_CLUSTER_CLIENT_IP",
            "HTTP_CLIENT_IP",
            "HTTP_FORWARDED_FOR",
            "HTTP_FORWARDED",
            "HTTP_VIA",
            "REMOTE_ADDR" };
    
        public String getClientIpAddress(HttpServletRequest request) {
            for (String header : IP_HEADER_CANDIDATES) {
                String ip = request.getHeader(header);
                log.info("ip : {}", ip);
                if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
                    return ip;
                }
            }
            return request.getRemoteAddr();
        }
    
    }