Spring Security LDAP和Remember Me

我正在使用Spring Boot构建一个与LDAP集成的应用程序。 我能够成功连接到LDAP服务器并validation用户身份。 现在我需要添加remember-mefunction。 我试图浏览不同的post( 这个 ),但无法找到我的问题的答案。 Spring Spring官方文件指出

如果您使用的身份validation提供程序不使用UserDetailsS​​ervice(例如,LDAP提供程序),那么除非您的应用程序上下文中还有UserDetailsS​​ervice bean,否则它将无效

这里是我的工作代码,其中包含一些初步想法,以添加记住我的function:

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(); } } 

CustomUserDetailsS​​erviceImpl

 public class CustomUserDetailsServiceImpl implements UserDetailsContextMapper { @Autowired SecurityHelper securityHelper; Log ___log = LogFactory.getLog(this.getClass()); @Override public LoggedInUserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection 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,但不确定如何实现。

使用LDAP配置RememberMefunction有两个问题:

  • 选择正确的RememberMe实现(Tokens vs. PersistentTokens)
  • 使用Spring的Java配置进行配置

我将逐步采取这些措施。

基于令牌的记住我function( TokenBasedRememberMeServices )在身份validation期间以下列方式工作:

  • 用户获得身份validation(agaisnt AD),我们目前知道用户的ID和密码
  • 我们构造值username + expirationTime + password + staticKey并创建它的MD5哈希
  • 我们创建一个cookie,其中包含用户名+到期+计算的哈希值

当用户想要回到服务并使用记住我的function进行身份validation时,我们:

  • 检查cookie是否存在且未过期
  • 从cookie中填充用户ID并调用提供的UserDetailsS​​ervice,该服务有望返回与用户ID相关的信息, 包括密码
  • 然后,我们根据返回的数据计算哈希值,并validationcookie中的哈希值是否与我们计算的值匹配
  • 如果匹配,我们返回用户的Authentication对象

哈希检查过程是必需的,以确保没有人可以创建一个“假”记住我的cookie,这将让他们模仿另一个用户。 问题是这个过程依赖于从我们的存储库加载密码的可能性 – 但是使用Active Directory这是不可能的 – 我们无法根据用户名加载明文密码。

这使得基于令牌的实现不适合与AD一起使用(除非我们开始创建包含密码或其他一些基于用户的秘密凭证的本地用户存储,并且我不建议使用此方法,因为我不知道其他详细信息你的应用程序,虽然它可能是一个很好的方式去)。

另一个记住我实现是基于持久性令牌( PersistentTokenBasedRememberMeServices ),它的工作原理如下(以简化的方式):

  • 当用户validation时,我们生成一个随机令牌
  • 我们将令牌存储在存储器中,并附带有关与其关联的用户ID的信息
  • 我们创建一个包含令牌ID的cookie

当用户想要validation我们时:

  • 检查我们是否有可用令牌ID的cookie
  • validation数据库中是否存在令牌ID
  • 根据数据库中的信息加载用户的数据

正如您所看到的,不再需要密码,尽管我们现在需要一个令牌存储(通常是数据库,我们可以使用内存进行测试),而不是使用密码validation。

这让我们进入了配置部分。 基于持久令牌的基本配置记住我看起来像这样:

 @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加载的用户ID标识的用户加载附加数据。 最简单的实现可能如下所示:

 public class BasicRememberMeUserDetailsService implements UserDetailsService { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return new User(username, "", Collections.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("user@"+DOMAIN); source.setPassword("password"); source.setUrl(URL); return source; } 

这将让你记住我使用LDAP的function,并在RememberMeAuthenticationToken提供加载的数据,这些数据将在SecurityContextHolder.getContext().getAuthentication() 。 它还可以重用现有逻辑将LDAP数据解析为User对象( CustomUserDetailsServiceImpl )。

作为一个单独的主题,问题中发布的代码也存在一个问题,您应该替换:

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

有:

  authManagerBuilder .authenticationProvider(activeDirectoryLdapAuthenticationProvider()) ; 

只应该为了添加基于DAO的身份validation(例如针对数据库)而调用userDetailsS​​ervice,并且应该使用用户详细信息服务的实际实现来调用。 您当前的配置可能导致无限循环。

听起来您缺少一个您的RememberMeService需要引用的UserService实例。 由于您使用的是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; }