Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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?

like image 268
gregor Avatar asked Jan 08 '13 20:01

gregor


People also ask

Can we use spring data JPA with spring MVC?

In this tutorial, we will use a Java-based spring configuration to configure Spring MVC 5, Spring Data JPA, Hibernate 5 and MySQL, etc. Spring Data JPA provides CRUD API, so you don't have to write boilerplate code. You just need to create a repository interface and spring will provide implementation automatically.

Can we use spring data JPA and Hibernate together?

You cant actually use both of them in the same application.

Can we use JPA with R2DBC?

JPA cannot deal with reactive repositories such as provided by Spring Data R2DBC. This means you will have to do more things manually when using R2DBC. There are other reactive drivers around such as for example Quarkus Reactive Postgres client (which uses Vert.

What is AuditorAware?

Interface AuditorAware<T> T - the type of the auditing instance. public interface AuditorAware<T> Interface for components that are aware of the application's current auditor. This will be some kind of user mostly.


2 Answers

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(); } 
like image 134
gregor Avatar answered Sep 23 '22 10:09

gregor


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); } 
like image 44
Anand Shah Avatar answered Sep 24 '22 10:09

Anand Shah