Как реализовать AuditorAware с помощью Spring данных JPA и Spring Безопасность?
Мы используем Hibernate/JPA, Spring, Spring Data и Spring Безопасность в нашем приложении. У меня есть стандартный объект User
, который отображается с использованием JPA. Кроме того, у меня есть UserRepository
public interface UserRepository extends CrudRepository<User, Long> {
List<User> findByUsername(String username);
}
который следует за соглашением Spring данных для методов запроса имен. У меня есть сущность
@Entity
public class Foo extends AbstractAuditable<User, Long> {
private String name;
}
Я хочу использовать Spring поддержку аудита данных. (Как descripe здесь.) Поэтому я создал AuditorService
следующим образом:
@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;
}
}
Когда я создаю метод
@Transactional
public void createFoo() {
Foo bar = new Foo();
fooRepository.save(foo);
}
Где все правильно подключено, а FooRepository
- это Spring Данные CrudRepository
. Затем вызывается a StackOverflowError
, так как вызов findByUsername
, похоже, вызывает спящий режим для сброса данных в базу данных, которая вызывает AuditingEntityListener
, который вызывает AuditorService#getCurrentAuditor
, который снова вызывает флеш и т.д.
Как избежать этой рекурсии? Существует ли "канонический способ" для загрузки объекта User
? Или есть способ предотвратить промывание Hibernate/JPA?
Ответы
Ответ 1
Решение не извлекать запись User
в реализацию AuditorAware
. Это запускает описанный цикл, поскольку запрос выбора запускает флеш (это так, поскольку Hibernate/JPA хочет записать данные в базу данных для фиксации транзакции перед выполнением выбора), который вызывает вызов AuditorAware#getCurrentAuditor
.
Решение состоит в том, чтобы сохранить запись User
в UserDetails
, предоставленную в Spring Безопасность. Поэтому я создал свою собственную реализацию:
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;
}
}
Кроме того, я изменил свой UserDetailsService
, чтобы загрузить User
и создать UserAwareUserDetails
. Теперь можно получить экземпляр User
через SercurityContextHolder
:
@Override
public User getCurrentAuditor() {
return ((UserAwareUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUser();
}
Ответ 2
Похоже, вы используете объект User для двух разных вещей:
Я думаю, что лучше подготовить специальный AuditableUser для целей аудита (он будет иметь идентичное поле имени пользователя как оригинальный User).
Рассмотрим следующий случай: вы хотите удалить некоторых пользователей из базы данных. Если все ваши объекты аудита связаны с пользователем, тогда они будут: a) потерять автора b) также могут быть удалены каскадом (зависит от того, как реализована ссылка). Не уверен, что ты этого хочешь.
Поэтому, используя специальный AuditableUser, вы будете иметь:
- нет рекурсии
- возможность удалить некоторых пользователей из системы и сохранить всю информацию об этом аудита.
Ответ 3
Чтобы быть честным, на самом деле вам не требуется другое лицо.
Например, у меня была аналогичная проблема, и я решил ее следующим образом:
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"));
}
}
}
Где SUser - это класс, который я использую для аудита, а также для безопасности.
У меня может быть другой вариант использования, чем у вас, и мой подход будет удален после, но он может быть разрешен следующим образом.
Ответ 4
У меня такая же проблема, и я только что изменил распространение метода findByUsername(username)
на Propagation.REQUIRES_NEW
, я подозревал, что это проблема с транзакциями, поэтому я изменил использование новой транзакции и хорошо работал у меня. Надеюсь, это поможет.
@Repository
public interface UserRepository extends JpaRepository<User, String> {
@Transactional(propagation = Propagation.REQUIRES_NEW)
List<User> findByUsername(String username);
}