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

Spring Безопасность LDAP и Запомнить меня

Я создаю приложение с Spring загрузкой с интеграцией с LDAP. Я смог успешно подключиться к серверу LDAP и аутентифицировать пользователя. Теперь у меня есть требование добавить функцию mem-me. Я попытался просмотреть различные сообщения (this), но не смог найти ответ на мою проблему. Официальный Spring Безопасность document утверждает, что

Если вы используете поставщик проверки подлинности, который не использует UserDetailsService (например, поставщик LDAP), тогда он не будет работать если у вас также нет UserDetailsService bean в вашем приложении контекст

Вот мой рабочий код с некоторыми первоначальными мыслями, чтобы добавить функцию mem-me:

WebSecurityConfig

import com.ui.security.CustomUserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.event.LoggerListener;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    String DOMAIN = "ldap-server.com";
    String URL = "ldap://ds.ldap-server.com:389";


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/ui/**").authenticated()
                .antMatchers("/", "/home", "/UIDL/**", "/ui/**").permitAll()
                .anyRequest().authenticated()
        ;
        http
                .formLogin()
                .loginPage("/login").failureUrl("/login?error=true").permitAll()
                .and().logout().permitAll()
        ;

        // Not sure how to implement this
        http.rememberMe().rememberMeServices(rememberMeServices()).key("password");

    }

    @Override
    protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {

        authManagerBuilder
                .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
                .userDetailsService(userDetailsService())
        ;
    }

    @Bean
    public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {

        ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(DOMAIN, URL);
        provider.setConvertSubErrorCodesToExceptions(true);
        provider.setUseAuthenticationRequestCredentials(true);
        provider.setUserDetailsContextMapper(userDetailsContextMapper());
        return provider;
    }

    @Bean
    public UserDetailsContextMapper userDetailsContextMapper() {
        UserDetailsContextMapper contextMapper = new CustomUserDetailsServiceImpl();
        return contextMapper;
    }

    /**
     * Impl of remember me service
     * @return
     */
    @Bean
    public RememberMeServices rememberMeServices() {
//        TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", userService);
//        rememberMeServices.setCookieName("cookieName");
//        rememberMeServices.setParameter("rememberMe");
        return rememberMeServices;
    }

    @Bean
    public LoggerListener loggerListener() {
        return new LoggerListener();
    }
}

CustomUserDetailsServiceImpl

public class CustomUserDetailsServiceImpl implements UserDetailsContextMapper {

    @Autowired
    SecurityHelper securityHelper;
    Log ___log = LogFactory.getLog(this.getClass());

    @Override
    public LoggedInUserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> grantedAuthorities) {

        LoggedInUserDetails userDetails = null;
        try {
            userDetails = securityHelper.authenticateUser(ctx, username, grantedAuthorities);
        } catch (NamingException e) {
            e.printStackTrace();
        }

        return userDetails;
    }

    @Override
    public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {

    }
}

Я знаю, что мне нужно каким-то образом реализовать UserService, но не знаю, как это можно добиться.

4b9b3361

Ответ 1

Есть две проблемы с настройкой функций RememberMe с LDAP:

  • выбор правильной реализации RememberMe (токены против PersistentTokens)
  • его конфигурация с помощью Spring Конфигурация Java

Я сделаю это шаг за шагом.

Функция аутентификации на основе токена (TokenBasedRememberMeServices) работает во время аутентификации следующим образом:

  • пользователь получает аутентификацию (agaisnt AD), и мы в настоящее время знаем идентификатор пользователя и пароль.
  • мы строим значение username + expirationTime + password + staticKey и создаем хэш MD5 из него
  • мы создаем файл cookie, который содержит имя пользователя + истечение + вычисленный хэш

Когда пользователь хочет вернуться к службе и пройти аутентификацию с помощью функции "запомнить меня", мы:

  • проверить, существует ли файл cookie и не истекает.
  • введите идентификатор пользователя из файла cookie и вызовите предоставленный UserDetailsService, который, как ожидается, вернет информацию, относящуюся к идентификатору пользователя, , включая пароль
  • Затем мы вычисляем хэш из возвращаемых данных и проверяем, что хэш в файле cookie совпадает со значением, которое мы вычислили
  • если он соответствует, мы возвращаем объект аутентификации пользователя

Процесс проверки хэша требуется, чтобы убедиться, что никто не может создать "фальшивый" файл cookie, который позволит им олицетворять другого пользователя. Проблема заключается в том, что этот процесс зависит от возможности загрузки пароля из нашего репозитория - но это невозможно в Active Directory - мы не можем загружать пароль открытого текста на основе имени пользователя.

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

Другое помните, что реализация на основе постоянных токенов (PersistentTokenBasedRememberMeServices) и работает как это (немного упрощенным способом):

  • Когда пользователь аутентифицируется, мы генерируем случайный токен
  • мы сохраняем токен в хранилище вместе с информацией об ассоциированном с ним идентификаторе пользователя
  • мы создаем файл cookie, который включает идентификатор маркера

Когда пользователь хочет аутентифицировать нас:

  • проверьте, есть ли у нас файл cookie с идентификатором токена.
  • проверить, существует ли идентификатор маркера в базе данных
  • Загрузка пользовательских данных на основе информации в базе данных

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

И это приводит нас к части конфигурации. Основная конфигурация для запоминающего устройства, основанного на постоянном токене, выглядит следующим образом:

@Override
protected void configure(HttpSecurity http) throws Exception {           
    ....
    String internalSecretKey = "internalSecretKey";
    http.rememberMe().rememberMeServices(rememberMeServices(internalSecretKey)).key(internalSecretKey);
}

 @Bean
 public RememberMeServices rememberMeServices(String internalSecretKey) {
     BasicRememberMeUserDetailsService rememberMeUserDetailsService = new BasicRememberMeUserDetailsService();
     InMemoryTokenRepositoryImpl rememberMeTokenRepository = new InMemoryTokenRepositoryImpl();
     PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(staticKey, rememberMeUserDetailsService, rememberMeTokenRepository);
     services.setAlwaysRemember(true);
     return services;
 }

В этой реализации будет использоваться хранилище токенов в памяти, которое должно быть заменено на JdbcTokenRepositoryImpl для производства. Предоставленный UserDetailsService отвечает за загрузку дополнительных данных для пользователя, идентифицированного идентификатором пользователя, загруженным из файла cookie "помнить меня". Простейшая реализация может выглядеть так:

public class BasicRememberMeUserDetailsService implements UserDetailsService {
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         return new User(username, "", Collections.<GrantedAuthority>emptyList());
     }
}

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

@Bean
public RememberMeServices rememberMeServices(String internalSecretKey) {
    LdapContextSource ldapContext = getLdapContext();

    String searchBase = "OU=Users,DC=test,DC=company,DC=com";
    String searchFilter = "(&(objectClass=user)(sAMAccountName={0}))";
    FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch(searchBase, searchFilter, ldapContext);
    search.setSearchSubtree(true);

    LdapUserDetailsService rememberMeUserDetailsService = new LdapUserDetailsService(search);
    rememberMeUserDetailsService.setUserDetailsMapper(new CustomUserDetailsServiceImpl());

    InMemoryTokenRepositoryImpl rememberMeTokenRepository = new InMemoryTokenRepositoryImpl();

    PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(internalSecretKey, rememberMeUserDetailsService, rememberMeTokenRepository);
    services.setAlwaysRemember(true);
    return services;
}

@Bean
public LdapContextSource getLdapContext() {
    LdapContextSource source = new LdapContextSource();
    source.setUserDn("[email protected]"+DOMAIN);
    source.setPassword("password");
    source.setUrl(URL);
    return source;
}

Это поможет вам вспомнить функциональность, которая работает с LDAP и предоставляет загруженные данные внутри RememberMeAuthenticationToken, которые будут доступны в SecurityContextHolder.getContext().getAuthentication(). Он также сможет повторно использовать существующую логику для анализа данных LDAP в объекте User (CustomUserDetailsServiceImpl).

Как отдельный вопрос, есть также одна проблема с кодом, размещенным в вопросе, вы должны заменить:

    authManagerBuilder
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
            .userDetailsService(userDetailsService())
    ;

с:

    authManagerBuilder
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
    ;

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

Ответ 2

Похоже, вам не хватает экземпляра UserService, которому нужна ссылка RememberMeService. Поскольку вы используете LDAP, вам понадобится версия LDAP UserService. Я знаком с реализацией JDBC/JPA, но выглядит так: org.springframework.security.ldap.userdetails.LdapUserDetailsManager - это то, что вы ищете. Тогда ваш конфиг будет выглядеть примерно так:

@Bean
public UserDetailsService getUserDetailsService() {
    return new LdapUserDetailsManager(); // TODO give it whatever constructor params it needs
}

@Bean
public RememberMeServices rememberMeServices() {
    TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", getUserDetailsService());
    rememberMeServices.setCookieName("cookieName");
    rememberMeServices.setParameter("rememberMe");
    return rememberMeServices;
}