Spring Security реализует аутентификацию по нескольким таблицам SQL

Я относительно новичок в Spring Boot и изучаю его функции аутентификации. Я заметил, что Spring Boot предлагает удобную встроенную аутентификацию при условии, что имена пользователей и роли указаны в таблицах пользователей и пользователей_ролей. Однако я столкнулся с проблемой реализации аутентификации для двух разных типов пользователей, которые должны быть отдельными и храниться в разных таблицах, поскольку они содержат разные типы таблиц SQL данных в моем приложении Spring Boot.

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

CREATE TABLE IF NOT EXISTS passengers (
                            passenger_id INT AUTO_INCREMENT PRIMARY KEY,
                            name VARCHAR(255) NOT NULL,
                            passport_number VARCHAR(15) UNIQUE NOT NULL,
                            nationality VARCHAR(255) NOT NULL,
                            contact_details VARCHAR(255)
);

Другая таблица — таблица сотрудников, и она должна иметь права администратора.

CREATE TABLE IF NOT EXISTS employees (
                           employee_id INT AUTO_INCREMENT PRIMARY KEY,
                           name VARCHAR(255) NOT NULL,
                           role VARCHAR(100) NOT NULL,
                           contact_info VARCHAR(255),
                           airport_id INT,
                           FOREIGN KEY (airport_id) REFERENCES airports(airport_id)
);

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


@Configuration
public class DemoSecurityConfig {

    // add support for JDBC ... no more hardcoded users :-)

    @Bean
    public UserDetailsManager userDetailsManager(DataSource dataSource) {

        JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource);

        // define query to retrieve a user by username
        jdbcUserDetailsManager.setUsersByUsernameQuery(
                "select user_id, pw, active from members where user_id=?");

        // define query to retrieve the authorities/roles by username
        jdbcUserDetailsManager.setAuthoritiesByUsernameQuery(
                "select user_id, role from roles where user_id=?");

        return jdbcUserDetailsManager;
    }


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests(configurer ->
                configurer
                        .requestMatchers(HttpMethod.GET, "/api/employees").hasRole("EMPLOYEE")
                        .requestMatchers(HttpMethod.GET, "/api/employees/**").hasRole("EMPLOYEE")
                        .requestMatchers(HttpMethod.POST, "/api/employees").hasRole("MANAGER")
                        .requestMatchers(HttpMethod.PUT, "/api/employees").hasRole("MANAGER")
                        .requestMatchers(HttpMethod.DELETE, "/api/employees/**").hasRole("ADMIN")
        );

        // use HTTP Basic authentication
        http.httpBasic(Customizer.withDefaults());

        // disable Cross Site Request Forgery (CSRF)
        // in general, not required for stateless REST APIs that use POST, PUT, DELETE and/or PATCH
        http.csrf(csrf -> csrf.disable());

        return http.build();
    }
    
}

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

Мы используем Spring Data JPA и определили объекты для этих таблиц. Итак, у нас есть репозитории, расширяющие JPARepositoryInterface. Может ли кто-нибудь предоставить рекомендации или потенциальное решение для реализации аутентификации, охватывающей несколько таблиц SQL в этом контексте? Будем очень признательны за любые примеры или указатели.

Заранее спасибо!

Ты можешь это сделать. Но скажите мне, как определить, какую аутентификацию использовать? будут ли у вас отдельные маршруты API?

YogendraR 18.04.2024 09:37

@YogendraR Я подумываю о реализации двух UserDetailServices, по одному для каждой таблицы, а затем, возможно, добавлю их в AuthenticationProvider. Логины и пароли я извлеку из таблиц (добавлю в таблицы необходимые поля).

Florian Saqipi 18.04.2024 10:55

Позвольте мне сказать по-другому: пользователь пытается получить доступ к вашему приложению. Как вы определите, какую таблицу вам нужно использовать? Будете ли вы выполнять поиск в обеих таблицах, если этот конкретный пользователь существует? или у вас будут отдельные маршруты API для различий. типы пользователей? Я не спрашиваю вас о технических вопросах, а пытаюсь понять ваши бизнес-требования.

YogendraR 18.04.2024 11:00

@YogendraR У меня будет две отдельные таблицы, поэтому они будут аутентифицироваться отдельно, поэтому для обеих будут разные конечные точки. Таким образом, пассажиру будет показан другой логин, а сотруднику — другой.

Florian Saqipi 18.04.2024 11:16

У вас должна быть одна таблица пользователей с ролями пользователей для аутентификации. Для бизнес-логики, которая зависит от роли пользователя, вы можете использовать таблицы сотрудников и пассажиров (вам просто нужно поле user_id в каждой из них).

Asem 18.04.2024 12:46

у вас есть два способа сделать это: либо вы реализуете интерфейс UserDetailsService, попытаетесь получить пользователя из первой базы данных, если это не удастся, перейдите ко второй. Другой вариант — иметь две отдельные конечные точки, что означает два отдельных входа в систему, что, в свою очередь, означает две отдельные цепочки фильтров и две отдельные конфигурации безопасности. Я бы категорически не стал реализовывать собственный AuthenticationProvider, поскольку стандартный поставщик включает в себя дополнительные функции безопасности, такие как предотвращение атак с фиксацией сеанса и т. д. Не создавайте свой собственный, если вы действительно не уверены, что знаете, как работает безопасность.

Toerktumlare 18.04.2024 12:47

или вы можете оптимизировать все до одного запроса и проверять пользователя в обеих таблицах, используя один запрос и оператор UNION.

Toerktumlare 18.04.2024 12:50

@Asem, спасибо за это предложение, это действительно хорошая идея реализации. Я это очень ценю.

Florian Saqipi 18.04.2024 20:05

@Toerktumlare Спасибо за предоставление множества идей для этих решений, эти идеи очень осуществимы. Я очень благодарен.

Florian Saqipi 18.04.2024 20:06
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
9
115
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

В реальном мире вполне возможно, что разные бизнес-пользователи должны проходить аутентификацию из разных источников. Давайте рассмотрим решение:

Для каждой точки входа можно зарегистрировать два разных SecurityFilterChain. Предлагаю вам разобраться в Spring Security Architecture. FilterChainProxy решит, какой SecurityFilterChain будет использоваться на основе securityMatcher.

Аналогичным образом вы можете создать два разных UserDetailsService для каждого источника данных.

Конфигурация безопасности

@Configuration
public class SecurityConfiguration {

private final PassengerUserDetailsService passengerUserDetailsService;
private final EmployeeUserDetailsService employeeUserDetailsService;
private final PasswordEncoder passwordEncoder;

public SecurityConfiguration(PassengerUserDetailsService passengerUserDetailsService,
                             EmployeeUserDetailsService employeeUserDetailsService,
                             PasswordEncoder passwordEncoder) {
    this.passengerUserDetailsService = passengerUserDetailsService;
    this.employeeUserDetailsService = employeeUserDetailsService;
    this.passwordEncoder = passwordEncoder;
}


@Bean
@Order(0)
public SecurityFilterChain filterChain0(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            .securityMatcher("/passenger/**") // this will decide which securityFilterChain will be used
            .authorizeHttpRequests(
                    auth -> auth.
                            requestMatchers(HttpMethod.GET, "/passenger/**").hasRole("PASSENGER")
            )
            .httpBasic(Customizer.withDefaults())
            .csrf(CsrfConfigurer::disable)
            .userDetailsService(passengerUserDetailsService);
    return httpSecurity.build();
}

@Bean
@Order(1)
public SecurityFilterChain filterChain1(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            .securityMatcher("/employee/**")
            .authorizeHttpRequests(
                    auth -> auth.
                            requestMatchers(HttpMethod.GET, "/employee/**").hasRole("EMPLOYEE")
            )
            .httpBasic(Customizer.withDefaults())
            .csrf(CsrfConfigurer::disable)
            .userDetailsService(employeeUserDetailsService);
    return httpSecurity.build();
 }
}

ПассажирПользовательПодробностиСервис

@Service
public class PassengerUserDetailsService implements UserDetailsService  {

private final PasswordEncoder passwordEncoder;

PassengerUserDetailsService(PasswordEncoder passwordEncoder) {
    this.passwordEncoder = passwordEncoder;
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User.UserBuilder builder = User.builder();
    builder.username(username);
    builder.password(passwordEncoder.encode(username));

    switch (username) {
        case "passenger":
            builder.roles("PASSENGER");
            break;
        default:
            throw new UsernameNotFoundException("User not found.");
    }
    return builder.build();
 }
}

Сведения о пользователе сотрудникаСервис

@Service
public class EmployeeUserDetailsService implements UserDetailsService {

private final PasswordEncoder passwordEncoder;

EmployeeUserDetailsService(PasswordEncoder passwordEncoder) {
    this.passwordEncoder = passwordEncoder;
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User.UserBuilder builder = User.builder();
    builder.username(username);
    builder.password(passwordEncoder.encode(username));

    switch (username) {
        case "employee":
            builder.roles("EMPLOYEE");
            break;
        default:
            throw new UsernameNotFoundException("User not found.");
    }
    return builder.build();
 }
}

В каждом UserDetailsService я высмеивал пользователей, которых вы можете получить из соответствующего источника данных. Вы можете обратиться к классу org.springframework.security.authentication.dao.DaoAuthenticationProvider для справки.

Вам не нужно следовать стандартному ddl Spring для определения вашей схемы. Вы всегда можете использовать существующую схему и использовать собственные запросы, используя UserDetailsService. Они предназначены для быстрого запуска вашего проекта без ручной настройки.

Я рад, что мой ответ оказался полезным. Еще одна вещь: вам не нужно следовать сценариям Spring по умолчанию для определения вашей схемы. Вы всегда можете использовать существующую схему и использовать собственные запросы, используя UserDetailsService

YogendraR 19.04.2024 10:44

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