Подтвердить что ты не робот

Как реализовать 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?

4b9b3361

Ответ 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);
}