Spring Security LDAP и помните меня

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

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

вот мой рабочий код с некоторыми начальными мыслями, чтобы добавить функциональность remember-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, но не уверен, как это может быть достигнуто.

2 ответов


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

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

Я буду делать это шаг за шагом.

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

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

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

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

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

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

другая реализация remember me основана на постоянных токенах (PersistentTokenBasedRememberMeServices) и он работает следующим образом (немного упрощенным способом):

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

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

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

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

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

@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 отвечает за загрузку дополнительных данных для пользователя, идентифицированного идентификатором пользователя, загруженным из remember me печенье. Самая простая реализация может выглядеть так:

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

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

@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("user@"+DOMAIN);
    source.setPassword("password");
    source.setUrl(URL);
    return source;
}

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

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

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

С:

    authManagerBuilder
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
    ;

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


похоже, вам не хватает экземпляра 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;
}