Sécurité de printemps LDAP et se souvenir de moi

je construis une application avec botte de printemps qui a une intégration avec LDAP. J'ai pu me connecter avec succès au serveur LDAP et authentifier l'utilisateur. Maintenant, j'ai une exigence d'ajouter souvenir de moi fonctionnalité. J'ai essayé de regarder à travers différents messages ( ce ) mais n'a pas été en mesure de trouver une réponse à mon problème. Sécurité officielle du ressort document indique que

Si vous utilisez un fournisseur d'authentification ce qui ne veut pas utiliser un UserDetailsService (par exemple, le fournisseur LDAP) alors il ne fonctionnera pas sauf si vous avez également un haricot UserDetailsService dans votre demande contexte

Voici mon code de travail avec quelques réflexions initiales pour ajouter la fonctionnalité me souvenir:

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) {

    }
}

je sais que j'ai besoin de mettre en œuvre UserService d'une façon ou d'une autre, mais pas sûr comment cela peut être réalisé.

24
demandé sur Community 2014-07-15 00:36:11

2 réponses

il y a deux problèmes de configuration des fonctionnalités RememberMe avec LDAP:

  • sélection de la bonne RememberMe de mise en œuvre (Jetons contre PersistentTokens)
  • sa configuration utilisant la Configuration Java de Spring

je vais les prendre pas à pas.

la fonctionnalité "remember me" ( TokenBasedRememberMeServices ) basée sur un jeton fonctionne de la manière suivante lors de l'authentification:

  • l'utilisateur est authentifié (agaisnt AD) et nous connaissons actuellement l'identifiant et le mot de passe de l'utilisateur
  • nous construisons valeur username + expirationTime + password + staticKey et en créons un hash MD5
  • nous créons un cookie qui contient nom d'utilisateur + expiration + le hash calculé

lorsque l'utilisateur veut revenir au service et être authentifié en utilisant le me fonctionnalité nous:

  • vérifier si le témoin existe et s'il n'est pas expiré
  • afficher l'identifiant de l'utilisateur à partir du cookie et appeler le service de messagerie fourni par L'utilisateur qui est censé retourner les informations relatives à l'identifiant de l'utilisateur, y compris le mot de passe
  • nous calculons ensuite le hash à partir des données retournées et vérifions que le hash dans le cookie correspond à la valeur que nous avons calculée.
  • si elle correspond, nous retournons l'objet D'authentification de l'utilisateur

le processus de vérification du hachage est requis afin de s'assurer que personne ne peut créer un "faux" cookie remember me, qui les laisserait usurper l'identité d'un autre utilisateur. Le problème est que ce processus repose sur la possibilité de charger le mot de passe à partir de notre dépôt - mais c'est impossible avec Active Directory - nous ne pouvons pas charger le mot de passe en clair basé sur le nom d'utilisateur.

cela rend l'implémentation basée sur les tokens Impropre à l'utilisation avec la publicité (à moins que nous ne commencions à créer un magasin utilisateur local qui contient le mot de passe ou un autre justificatif d'identité secret basé sur l'utilisateur et je ne suggère pas cette approche car je ne connais pas d'autres détails de votre application, bien que ce soit une bonne façon de procéder).

l'autre implémentation remember me est basée sur des tokens persistants ( PersistentTokenBasedRememberMeServices ) et fonctionne comme ceci (d'une manière un peu simplifiée)):

  • lorsque l'utilisateur s'authentifie, nous générons un jeton aléatoire
  • nous stockons le jeton dans le stockage avec les informations sur l'ID de l'utilisateur associé à lui
  • nous créons un cookie qui inclut le token ID

lorsque l'utilisateur veut nous authentifier:

  • vérifiez si nous avons le cookie avec token ID disponible
  • vérifier si l'ID du token existe dans la base de données
  • charger les données de l'utilisateur à partir des informations de la base de données

comme vous pouvez le voir, le mot de passe n'est plus nécessaire, bien que nous ayons maintenant besoin d'un stockage token (typiquement base de données, nous pouvons utiliser en mémoire pour tester) qui est utilisé à la place de la vérification de mot de passe.

et cela nous amène à la partie configuration. La configuration de base pour les tokens persistants souvenez-vous de moi ressemble à ceci:

@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;
 }

cette implémentation utilisera le stockage en mémoire token qui doit être remplacé par JdbcTokenRepositoryImpl pour la production. Le UserDetailsService fourni est responsable du chargement de données supplémentaires pour l'utilisateur identifié par L'identifiant de l'utilisateur chargé à partir du cookie remember me. La mise en œuvre la plus simple peut ressembler à ceci:

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

vous pouvez également fournir un autre UserDetailsService mise en œuvre qui charge attributs supplémentaires ou adhésions de groupe à partir de votre annonce ou base de données interne, selon vos besoins. Il pourrait ressembler à ceci:

@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;
}

cette fonctionnalité vous permettra de vous souvenir de moi qui fonctionne avec LDAP et fournit les données chargées à l'intérieur de RememberMeAuthenticationToken qui seront disponibles dans le SecurityContextHolder.getContext().getAuthentication() . Il pourra également RÉUTILISER votre logique existante pour l'analyse des données LDAP dans un objet utilisateur ( CustomUserDetailsServiceImpl ).

en tant qu'élément distinct sujet, il y a aussi un problème avec le code affiché dans la question, vous devriez remplacer le:

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

avec:

    authManagerBuilder
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
    ;

l'appel à userDetailsService ne doit être fait que pour ajouter une authentification basée sur DAO (par exemple contre une base de données) et doit être appelé avec une réelle implémentation du service de détails utilisateur. Votre configuration actuelle peut conduire à des boucles infinies.

27
répondu Vladimír Schäfer 2014-07-20 20:57:09

on dirait que vous manquez une instance de UserService à laquelle votre RememberMeService a besoin d'une référence. Puisque vous utilisez LDAP, vous avez besoin d'une version LDAP de UserService . Je ne connais que les implémentations JDBC/JPA, mais on dirait que org.springframework.security.ldap.userdetails.LdapUserDetailsManager est ce que vous recherchez. Alors votre configuration ressemblerait à quelque chose comme ceci:

@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;
}
0
répondu SergeyB 2014-07-17 20:47:11