Spring Security LDAP 和记住我

Posted

技术标签:

【中文标题】Spring Security LDAP 和记住我【英文标题】:Spring Security LDAP and Remember Me 【发布时间】:2014-09-04 21:36:41 【问题描述】:

我正在使用 Spring Boot 构建一个与 LDAP 集成的应用程序。我能够成功连接到 LDAP 服务器并验证用户。现在我需要添加记住我的功能。我试图浏览不同的帖子 (this) 但无法找到我的问题的答案。官方 Spring Security document 声明

如果您使用的身份验证提供程序不使用 UserDetailsS​​ervice (例如,LDAP 提供者)那么它将不起作用 除非您的应用程序中还有一个 UserDetailsS​​ervice bean 上下文

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

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<? 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 实现(令牌与持久令牌) 其配置使用 Spring 的 Java 配置

我会一步一步来的。

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

用户已通过身份验证(针对 AD),我们目前知道用户的 ID 和密码 我们构造值 username + expirationTime + password + staticKey 并创建它的 MD5 哈希 我们创建一个包含用户名 + 过期时间 + 计算哈希的 cookie

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

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

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

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

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

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

当用户想要验证我们时:

检查我们是否有带有令牌 ID 的 cookie 可用 验证token 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;
 

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

public class BasicRememberMeUserDetailsService implements UserDetailsService 
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
         return new User(username, "", Collections.<GrantedAuthority>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 一起使用并在 RememberMeAuthenticationToken 中提供加载的数据,这些数据将在 SecurityContextHolder.getContext().getAuthentication() 中可用。它还能够重用您现有的逻辑,将 LDAP 数据解析为用户对象 (CustomUserDetailsServiceImpl)。

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

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

与:

    authManagerBuilder
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
    ;

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

【讨论】:

好答案弗拉基米尔!快速提问。在这行代码中: PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(staticKey, rememberMeUserDetailsS​​ervice, rememberMeTokenRepository);我假设“staticKey”与“internalSecretKey”相同。对吗? 是的,它是同一个密钥 - 它用于验证 RememberMeAuthenticationToken 是在知道此秘密的情况下构建的,并在 RememberMeAuthenticationProvider 中进行了验证。确保您使用的是我的答案的最新版本,因为在上面的当前文本中,它在所有地方都被称为“internalSecretKey”。【参考方案2】:

听起来您缺少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;

【讨论】:

以上是关于Spring Security LDAP 和记住我的主要内容,如果未能解决你的问题,请参考以下文章

Spring Security和LDAP身份验证

如何使用带有 LDAP 的 Spring Security 获取用户信息

使用 Spring Security 3 进行 LDAP 身份验证

如何将 Spring Security 从 ldap 更改为 ldap starttls

使用 LDAP 的 Spring Security - 登录后出错

使用 Spring Security 通过 MySQL-LDAP-Thymeleaf 登录