Spring Security LDAP and Remember Me

前端 未结 2 1505
攒了一身酷
攒了一身酷 2020-12-23 21:01

I\'m building an app with Spring Boot that has integration with LDAP. I was able to connect successfully to LDAP server and authenticate user. Now I have a requirement to ad

相关标签:
2条回答
  • 2020-12-23 21:39

    It sounds like you are missing an instance of UserService that your RememberMeService needs a reference to. Since you are using LDAP, you'd need an LDAP version of UserService. I'm only familiar with JDBC/JPA implementations, but looks like org.springframework.security.ldap.userdetails.LdapUserDetailsManager is what you are looking for. Then your config would look something like this:

    @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 讨论(0)
  • 2020-12-23 21:52

    There are two issues to configuration of the RememberMe features with LDAP:

    • selection of the correct RememberMe implementation (Tokens vs. PersistentTokens)
    • its configuration using Spring's Java Configuration

    I'll take these step by step.

    The Token-based remember me feature (TokenBasedRememberMeServices) works in the following way during authentication:

    • user gets authenticated (agaisnt AD) and we currently know user's ID and password
    • we construct value username + expirationTime + password + staticKey and create an MD5 hash of it
    • we create a cookie which contains username + expiration + the calculated hash

    When user wants to come back to the service and be authenticated using the remember me functionality we:

    • check whether the cookie exists and isn't expired
    • populate the user ID from the cookie and call the provided UserDetailsService which is expected to return information related to the user's ID, including the password
    • we then calculate the hash from the returned data and verify that the hash in the cookie matches with the value we calculated
    • if it matches we return the user's Authentication object

    The hash checking process is required in order to make sure that nobody can create a "fake" remember me cookie, which would let them impersonate another user. The problem is that this process relies on possibility of loading password from our repository - but this is impossible with Active Directory - we cannot load plaintext password based on username.

    This makes the Token-based implementation unsuitable for usage with AD (unless we start creating some local user store which contains the password or some other secret user-based credential and I'm not suggesting this approach as I don't know other details of your application, although it might be a good way to go).

    The other remember me implementation is based on persistent tokens (PersistentTokenBasedRememberMeServices) and it works like this (in a bit simplified way):

    • when user authenticates we generate a random token
    • we store the token in storage together with information about user's ID associated with it
    • we create a cookie which includes the token ID

    When user wants to authenticate we:

    • check whether we have the cookie with token ID available
    • verify whether the token ID exists in database
    • load user's data based on information in the database

    As you can see, the password is no longer required, although we now need a token storage (typically database, we can use in-memory for testing) which is used instead of the password verification.

    And that gets us to the configuration part. The basic configuration for persistent-token-based remember me looks like this:

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

    This implementation will use in-memory token storage which should be replaced with JdbcTokenRepositoryImpl for production. The provided UserDetailsService is responsible for loading of additional data for the user identified by the user ID loaded from the remember me cookie. The simpliest implementation can look like this:

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

    You could also supply another UserDetailsService implementation which loads additional attributes or group memberships from your AD or internal database, depending on your needs. It could look like this:

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

    This will get you remember me functionality which works with LDAP and provides the loaded data inside RememberMeAuthenticationToken which will be available in the SecurityContextHolder.getContext().getAuthentication(). It will also be able to re-use your existing logic for parsing of LDAP data into an User object (CustomUserDetailsServiceImpl).

    As a separate subject, there's also one problem with the code posted in the question, you should replace the:

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

    with:

        authManagerBuilder
                .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
        ;
    

    The call to userDetailsService should only be made in order to add DAO-based authentication (e.g. against database) and should be called with a real implementation of the user details service. Your current configuration can lead to infinite loops.

    0 讨论(0)
提交回复
热议问题