How to implement AuditorAware with Spring Data JPA and Spring Security?

23,550

Solution 1

The solution is not to fetch the User record in the AuditorAware implementation. This triggers the described loop, since a select query triggers a flush (this is the case since Hibernate/JPA wants to write the data to the database to commit the transaction before executing the select), which triggers a call to AuditorAware#getCurrentAuditor.

The solution is to store the User record in the UserDetails provided to Spring Security. Hence I created my own implementation:

public class UserAwareUserDetails implements UserDetails {

    private final User user;
    private final Collection<? extends GrantedAuthority> grantedAuthorities;

    public UserAwareUserDetails(User user) {
        this(user, new ArrayList<GrantedAuthority>());
    }

    public UserAwareUserDetails(User user, Collection<? extends GrantedAuthority> grantedAuthorities) {
        this.user = user;
        this.grantedAuthorities = grantedAuthorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return grantedAuthorities;
    }

    @Override
    public String getPassword() {
        return user.getSaltedPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    public User getUser() {
        return user;
    }
}

Further, I changed my UserDetailsService to load the User and create UserAwareUserDetails. Now it is possible to access the User instance through the SercurityContextHolder:

@Override
public User getCurrentAuditor() {
    return ((UserAwareUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUser();
}

Solution 2

I got the same issue and what I did was just change the propagation on the findByUsername(username) method to Propagation.REQUIRES_NEW, I suspected that was a problem with the transactions, so I changed to use a new transaction and that worked well for me. I hope this can help.

@Repository
public interface UserRepository extends JpaRepository<User, String> {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    List<User> findByUsername(String username);
}

Solution 3

It looks like you use a User entity for two different things:

  • authentication
  • audit

I think it will be better to prepare a special AuditableUser for audit purpose (it will have identical username field as original User). Consider following case: you want to delete some User from database. If all your audit objects are linked to User then they will a) loose author b) may be deleted by cascade too (depends on how the link is implemented). Not sure that you want it. So by using special AuditableUser you will have:

  • no recursion
  • ability to delete some User from the system and preserve all audit info about it

Solution 4

To be honest, You do not actually require one another entity. For example, I had similar problem and I resolved it in following way:

public class SpringSecurityAuditorAware implements AuditorAware<SUser>, ApplicationListener<ContextRefreshedEvent> {
    private static final Logger LOGGER = getLogger(SpringSecurityAuditorAware.class);
    @Autowired
    SUserRepository repository;
    private SUser systemUser;

    @Override
    public SUser getCurrentAuditor() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        SUser principal;
        if (authentication == null || !authentication.isAuthenticated()) {
            principal = systemUser;
        } else {
            principal = (SUser) authentication.getPrincipal();
        }
        LOGGER.info(String.format("Current auditor is >>> %s", principal));
        return principal;
    }

    @Override
    public void onApplicationEvent(final ContextRefreshedEvent event) {
        if (this.systemUser == null) {
            LOGGER.info("%s >>> loading system user");
            systemUser = this.repository.findOne(QSUser.sUser.credentials.login.eq("SYSTEM"));
        }
    }
}

Where SUser is both the class which I use for auditing as well as for the security. I had maybe different use case than Yours and my approach will be deleted after, but it can be resolved like this.

Share:
23,550
gregor
Author by

gregor

Updated on August 25, 2022

Comments

  • gregor
    gregor over 1 year

    We use Hibernate/JPA, Spring, Spring Data and Spring Security in our application. I have a standard User entity which is mapped using JPA. Further, I have a UserRepository

    public interface UserRepository extends CrudRepository<User, Long> {
        List<User> findByUsername(String username);
    }
    

    which follows the Spring Data convention for naming query methods. I have an entity

    @Entity
    public class Foo extends AbstractAuditable<User, Long> {
        private String name;
    }
    

    I want to use Spring Data auditing support. (As descripe here.) Hence I created a AuditorService as follows:

    @Service
    public class AuditorService implements AuditorAware<User> {
    
        private UserRepository userRepository;
    
        @Override
        public User getCurrentAuditor() {
            String username = SecurityContextHolder.getContext().getAuthentication().getName();
            List<User> users = userRepository.findByUsername(username);
            if (users.size() > 0) {
                return users.get(0);
            } else {
                throw new IllegalArgumentException();
            }
        }
    
        @Autowired
        public void setUserService(UserService userService) {
            this.userService = userService;
        }
    }
    

    When I create a method

    @Transactional
    public void createFoo() {
        Foo bar = new Foo(); 
        fooRepository.save(foo);
    }
    

    Where everything is correctly wired and FooRepository is a Spring Data CrudRepository. Then a StackOverflowError is thrown since the the call to findByUsername seems to trigger hibernate to flush the data to the database which triggers AuditingEntityListener who calls AuditorService#getCurrentAuditor which again triggers a flush and so on.

    How to avoid this recursion? Is there a "canonical way" to load the User entity? Or is there a way to prevent Hibernate/JPA from flushing?