Je crée une application avec Spring Boot qui a une intégration avec LDAP. J'ai pu me connecter avec succès au serveur LDAP et authentifier l'utilisateur. Maintenant, je dois ajouter une fonctionnalité de souvenir de moi. J'ai essayé de parcourir différents messages ( this ) mais je n'ai pas pu trouver de réponse à mon problème. Sécurité officielle du printemps document déclare que
Si vous utilisez un fournisseur d'authentification qui n'utilise pas un UserDetailsService (par exemple, le fournisseur LDAP), cela ne fonctionnera que si vous disposez également d'un bean UserDetailsService dans votre contexte d'application.
Voici mon code de travail avec quelques réflexions initiales pour ajouter la fonctionnalité de souvenir de moi:
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 je dois implémenter UserService d'une manière ou d'une autre, mais je ne sais pas comment cela peut être réalisé.
La configuration des fonctionnalités RememberMe avec LDAP pose deux problèmes:
Je vais les prendre étape par étape.
La fonction Se souvenir de moi basée sur les jetons (TokenBasedRememberMeServices
) fonctionne de la manière suivante lors de l'authentification:
Lorsque l'utilisateur souhaite revenir au service et être authentifié à l'aide de la fonctionnalité Se souvenir de moi, nous:
Le processus de vérification du hachage est requis afin de s'assurer que personne ne peut créer un "faux" cookie de souvenir de moi, qui leur permettrait de se faire passer pour un autre utilisateur. Le problème est que ce processus repose sur la possibilité de charger le mot de passe à partir de notre référentiel - mais cela 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 jetons impropre à l'utilisation avec AD (sauf si nous commençons à créer un magasin d'utilisateurs local qui contient le mot de passe ou d'autres informations d'identification basées sur l'utilisateur secrètes et je ne suggère pas cette approche car je ne connais pas d'autres détails de votre candidature, même si cela peut être une bonne façon de procéder).
L'autre implémentation de Remember me est basée sur des jetons persistants (PersistentTokenBasedRememberMeServices
) et fonctionne comme ceci (de manière un peu simplifiée):
Lorsque l'utilisateur souhaite s'authentifier, nous:
Comme vous pouvez le voir, le mot de passe n'est plus requis, bien que nous ayons maintenant besoin d'un stockage de jetons (généralement une base de données, nous pouvons utiliser en mémoire pour les tests) qui est utilisé à la place de la vérification du mot de passe.
Et cela nous amène à la partie configuration. La configuration de base pour le rappel persistant basé sur des jetons 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 un stockage de jetons en mémoire qui devrait être remplacé par JdbcTokenRepositoryImpl
pour la production. Le UserDetailsService
fourni est responsable du chargement des données supplémentaires pour l'utilisateur identifié par l'ID utilisateur chargé à partir du cookie Remember me. L'implémentation 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 une autre implémentation UserDetailsService
qui charge des attributs supplémentaires ou des appartenances à des groupes à partir de votre base de données AD ou interne, selon vos besoins. Cela 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;
}
Cela vous permettra de vous souvenir de la fonctionnalité qui fonctionne avec LDAP et fournit les données chargées dans RememberMeAuthenticationToken
qui seront disponibles dans la SecurityContextHolder.getContext().getAuthentication()
. Il pourra également réutiliser votre logique existante pour l'analyse des données LDAP dans un objet utilisateur (CustomUserDetailsServiceImpl
).
Comme sujet séparé, il y a aussi un problème avec le code affiché dans la question, vous devez remplacer le:
authManagerBuilder
.authenticationProvider(activeDirectoryLdapAuthenticationProvider())
.userDetailsService(userDetailsService())
;
avec:
authManagerBuilder
.authenticationProvider(activeDirectoryLdapAuthenticationProvider())
;
L'appel à userDetailsService ne doit être effectué que pour ajouter une authentification basée sur DAO (par exemple contre une base de données) et doit être appelé avec une implémentation réelle du service de détails utilisateur. Votre configuration actuelle peut conduire à des boucles infinies.
Il semble qu'il vous manque une instance de UserService
à laquelle votre RememberMeService
a besoin d'une référence. Puisque vous utilisez LDAP, vous aurez besoin d'une version LDAP de UserService
. Je ne connais que les implémentations JDBC/JPA, mais ressemble à org.springframework.security.ldap.userdetails.LdapUserDetailsManager
est ce que vous recherchez. Votre configuration ressemblerait alors à 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;
}