Что мне делать, чтобы избежать чрезмерных SELECT для ролей при работе с Spring Security?

Посмотрите, что делает Hibernate

Hibernate: select a1_0.user_id,a1_1.id,a1_1.role from user_role a1_0 join roles a1_1 on a1_1.id=a1_0.role_id where a1_0.user_id=?
Hibernate: select a1_0.user_id,a1_1.id,a1_1.role from user_role a1_0 join roles a1_1 on a1_1.id=a1_0.role_id where a1_0.user_id=?
Hibernate: select a1_0.user_id,a1_1.id,a1_1.role from user_role a1_0 join roles a1_1 on a1_1.id=a1_0.role_id where a1_0.user_id=?
Hibernate: select a1_0.user_id,a1_1.id,a1_1.role from user_role a1_0 join roles a1_1 on a1_1.id=a1_0.role_id where a1_0.user_id=?
Hibernate: select a1_0.user_id,a1_1.id,a1_1.role from user_role a1_0 join roles a1_1 on a1_1.id=a1_0.role_id where a1_0.user_id=?

Он делает отдельный запрос для каждого пользователя, чтобы получить его роли. Я хотел бы избежать этого, поскольку это похоже на убийцу производительности. Я считаю, что есть способ заставить Hibernate выполнять только один запрос для всех пользователей, чтобы получить свои роли за один раз.

Насколько я понимаю, проблема связана с тем, как настроена моя Spring Security.

@Configuration
@Import(WebMvcConfig.class)
@EnableWebSecurity
public class WebSecurityConfig {
    private final DataSource dataSource;

    private static final String USERS_BY_USERNAME_QUERY = """
            SELECT username, password, enabled
            FROM users where username = ?
            """;

    private static final String AUTHORITIES_BY_USERNAME_QUERY = """
            SELECT users.username, roles.role
            FROM user_role
            JOIN users ON user_role.user_id = users.id
            JOIN roles ON user_role.role_id = roles.id
            WHERE users.username = ?
            """;

    public WebSecurityConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .requestMatchers("/user/**").hasAuthority("USER")
                .requestMatchers("/admin/**").hasAuthority("ADMIN")
                .anyRequest().authenticated()
                .and().formLogin()
                .successHandler(successHandler())
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/login");
        return http.build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager(dataSource);
        userDetailsManager.setUsersByUsernameQuery(USERS_BY_USERNAME_QUERY);
        userDetailsManager.setAuthoritiesByUsernameQuery(AUTHORITIES_BY_USERNAME_QUERY);

        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsManager);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(10);
    }

    @Bean
    public AuthenticationSuccessHandler successHandler() {
        return (httpServletRequest, httpServletResponse, authentication) ->
                httpServletResponse.sendRedirect("/");
    }
}

Что мне делать, чтобы избежать чрезмерных SELECT для ролей при работе с Spring Security?

Мои сущности:

@Entity
@Table(name = "users")
@Getter
@Setter
@EqualsAndHashCode
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    @Column(nullable = false, unique = true)
    private String username;
    @Column(nullable = false)
    private String password;
    @Column(nullable = false)
    private String name;
    @Column(name = "last_name", nullable = false)
    private String lastName;
    @Column(nullable = false)
    private String department;
    @Column
    private int salary;
    @Column(nullable = false)
    private byte age;
    @Column
    private String email;
    @Column(name = "enabled", nullable = false)
    private byte enabledByte;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "user_role",
            joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
    @EqualsAndHashCode.Exclude
    private Set<Role> authorities;
@Entity
@Table(name = "roles")
@Getter
@Setter
@EqualsAndHashCode
public class Role implements GrantedAuthority {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    @Column(name = "role", nullable = false, unique = true)
    private String authority;
    @ManyToMany(mappedBy = "authorities")
    @EqualsAndHashCode.Exclude
    private Set<User> userList;

Если хотите, можете использовать мои файлы schema.sql и data.sql для быстрой настройки. Он создаст таблицы и заполнит их образцами строк. Я думаю, что было бы лучше не размещать скрипты прямо здесь, чтобы избежать ненужного беспорядка.

Что я пробовал:

  1. Установка типа выборки на EAGER
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "user_role",
            joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
    @EqualsAndHashCode.Exclude
    private Set<Role> authorities;

SELECT все еще присутствовали

  1. Использование аннотации @Query в моем репозитории и передача собственного запроса, который также возвращает роли
public interface UserDao extends JpaRepository<User, Long> {
    @Query(value = """
SELECT users.id, users.username, users.password, users.name,
users.last_name, users.department, users.salary, users.age,
users.email, users.enabled, roles.role
FROM user_role
JOIN users ON user_role.user_id = users.id
JOIN roles ON user_role.role_id = roles.id
WHERE username != :username
""", nativeQuery = true)
    List<User> findAllExceptLoggedUser(String username);

Результат:

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: pp.spring_bootstrap.models.User.authorities: could not initialize proxy - no Session

Хочу подчеркнуть, что @ManyToMany(fetch = FetchType.EAGER) все еще учился в моем User классе. Если я изменю его на LAZY, тот же эффект

вы можете использовать JdbcTemplate, который вообще не использует Hibernate/Jpa. Тем не менее, попытка получить все записи имеет смысл только для очень маленьких таблиц.

dsp_user 20.04.2023 19:24

Обратите внимание, что вопрос выделен жирным шрифтом. Я упоминаю LazyInitializatonException только для того, чтобы рассказать вам, к чему привела одна из моих попыток решить проблему. Я никогда не спрашивал, как мне избежать этого исключения (по крайней мере, здесь). Вот почему я считаю, что это не делает его расфокусированным

Sergey 21.04.2023 13:00
Освоение архитектуры микросервисов с Laravel: Лучшие практики, преимущества и советы для
Освоение архитектуры микросервисов с Laravel: Лучшие практики, преимущества и советы для
В последние годы архитектура микросервисов приобрела популярность как способ построения масштабируемых и гибких приложений. Laravel , популярный PHP...
Как построить CRUD-приложение в Laravel
Как построить CRUD-приложение в Laravel
Laravel - это популярный PHP-фреймворк, который позволяет быстро и легко создавать веб-приложения. Одной из наиболее распространенных задач в...
Освоение PHP и управление базами данных: Создание собственной СУБД - часть II
Освоение PHP и управление базами данных: Создание собственной СУБД - часть II
В предыдущем посте мы создали функциональность вставки и чтения для нашей динамической СУБД. В этом посте мы собираемся реализовать функции обновления...
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
Роли и разрешения пользователей без пакета Laravel 9
Роли и разрешения пользователей без пакета Laravel 9
Этот пост изначально был опубликован на techsolutionstuff.com .
Как установить LAMP Stack - Security 5/5 на виртуальную машину Azure Linux VM
Как установить LAMP Stack - Security 5/5 на виртуальную машину Azure Linux VM
В предыдущей статье мы завершили установку базы данных, для тех, кто не знает.
2
2
77
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

JOIN FETCHes JSQL помогает в такой ситуации

@Configuration
@EnableWebSecurityConfig
public class WebSecurityConfig {
// ...
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> {
            User user = userDao.findByUsername(username);
            if (user == null) {
                String msg = String.format("No user with username %s is found in the database", username);
                throw new UsernameNotFoundException(msg);
            }
            return user;
        };
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
public interface UserDao extends JpaRepository<User, Long> {
    @Query(value = """
            SELECT u
            FROM User u LEFT JOIN FETCH u.authorities
            WHERE u.username != :username
            """)
    List<User> findAllExceptLoggedUser(String username);

    @Query(value = """
            SELECT u
            FROM User u LEFT JOIN FETCH u.authorities
            WHERE u.username = :username
            """)
    User findByUsername(String username);

В консоли вы можете увидеть что-то вроде

Hibernate: select u1_0.id,u1_0.age,a1_0.user_id,a1_1.id,a1_1.role,u1_0.department,u1_0.email,u1_0.enabled,u1_0.last_name,u1_0.name,u1_0.password,u1_0.salary,u1_0.username from users u1_0 left join (user_role a1_0 join roles a1_1 on a1_1.id=a1_0.role_id) on u1_0.id=a1_0.user_id where u1_0.username=?

Другие вопросы по теме