Spring Security LDAP和Meember Me

我正在构建一个与LDAP集成的Spring Boot应用程序。我能够成功连接到LDAP服务器并对用户进行身份验证。现在,我需要添加“记住我”功能。我试图查看不同的帖子(这个),但无法找到我问题的答案。官方Spring Security文件指出,

如果您使用的是不使用 UserDetailsService 的身份验证提供程序(例如,LDAP 提供程序),那么除非您在应用程序上下文中也有 UserDetailsService Bean,否则它将不起作用

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

网络安全配置

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

自定义用户详细信息服务含义

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,但不确定如何实现。


答案 1

使用 LDAP 配置 RememberMe 功能时存在两个问题:

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

我将一步一步地进行这些操作。

基于令牌的“记住我”功能 () 在身份验证期间按以下方式工作:TokenBasedRememberMeServices

  • 用户获得身份验证(agaisnt AD),我们目前知道用户的ID和密码
  • 我们构造值用户名 + 过期时间 + 密码 + 静态密钥,并创建它的 MD5 哈希
  • 我们创建一个包含用户名+到期+计算哈希值的cookie

当用户想要返回服务并使用“记住我”功能进行身份验证时,我们:

  • 检查 Cookie 是否存在且未过期
  • 从 Cookie 填充用户 ID 并调用提供的用户详细信息服务,该服务应返回与用户 ID 相关的信息,包括密码
  • 然后,我们从返回的数据中计算哈希值,并验证 cookie 中的哈希值是否与我们计算的值匹配
  • 如果它匹配,我们将返回用户的身份验证对象

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

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

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

  • 当用户进行身份验证时,我们会生成一个随机令牌
  • 我们将令牌与与其关联的用户ID的信息一起存储在存储中
  • 我们创建一个包含令牌ID的cookie

当用户想要进行身份验证时,我们:

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

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

这让我们进入了配置部分。基于持久令牌的“记住我”的基本配置如下所示:

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

此实现将使用内存中令牌存储,该存储应替换为用于生产。提供的负责为从“记住我”cookie 加载的用户 ID 所标识的用户加载其他数据。最简单的实现可以如下所示:JdbcTokenRepositoryImplUserDetailsService

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

您还可以根据需要提供另一个实现,该实现从 AD 或内部数据库加载其他属性或组成员身份。它可能看起来像这样: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一起使用的功能,并提供其中的加载数据,这些数据将在.它还将能够重用现有逻辑将 LDAP 数据解析为 User 对象 ()。RememberMeAuthenticationTokenSecurityContextHolder.getContext().getAuthentication()CustomUserDetailsServiceImpl

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

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

跟:

    authManagerBuilder
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
    ;

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


答案 2

听起来你错过了一个你需要参考的实例。由于您使用的是 LDAP,因此您需要 LDAP 版本的 .我只熟悉 JDBC/JPA 实现,但看起来正是您正在寻找的。然后你的配置将如下所示:UserServiceRememberMeServiceUserServiceorg.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;
}

推荐