使用 Spring Boot 的 LDAP Active Directory 身份验证问题

问题描述 投票:0回答:1

我已经使用 Spring Boot 2.7.0 成功实现了针对 forsumsys.com LDAP 测试服务器的身份验证。但是当我尝试在我公司的 AD 服务器上运行相同的实现时,我得到了这个异常:

2022-06-01 14:54:15.505 DEBUG 6968 --- [nio-9080-exec-8] o.s.s.l.a.BindAuthenticator              : Failed to bind with any user DNs [sAMAccountName=my-username]
2022-06-01 14:54:15.576 DEBUG 6968 --- [nio-9080-exec-8] o.s.s.ldap.SpringSecurityLdapTemplate    : Found DN: CN=My Full Name,OU=Users,OU=Group4,OU=Group3,OU=Group2,OU=Group1,DC=subdomain,DC=domain,DC=com
2022-06-01 14:54:15.739 DEBUG 6968 --- [nio-9080-exec-8] o.s.s.l.s.FilterBasedLdapUserSearch      : Found user 'my-username', with FilterBasedLdapUserSearch [searchFilter=(&(objectClass=user)(sAMAccountName={0})); searchBase=DC=subdomain,DC=domain,DC=com; scope=subtree; searchTimeLimit=0; derefLinkFlag=false ]
2022-06-01 14:54:15.739 DEBUG 6968 --- [nio-9080-exec-8] o.s.s.l.u.LdapUserDetailsMapper          : Mapping user details from context with DN CN=My Full Name,OU=Users,OU=Group4,OU=Group3,OU=Group2,OU=Group1,DC=subdomain,DC=domain,DC=com
...
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:289) ~[spring-security-crypto-5.7.1.jar:5.7.1]
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:237) ~[spring-security-crypto-5.7.1.jar:5.7.1]
    at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$LazyPasswordEncoder.matches(WebSecurityConfigurerAdapter.java:599) ~[spring-security-config-5.7.1.jar:5.7.1]
    at org.springframework.security.authentication.dao.DaoAuthenticationProvider.additionalAuthenticationChecks(DaoAuthenticationProvider.java:76) ~[spring-security-core-5.7.1.jar:5.7.1]
    at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:147) ~[spring-security-core-5.7.1.jar:5.7.1]

这就是我的 application.yml 中的内容

ldap:
  urls: ldap://ldaphost:389/
  base:
    dn: DC=subdomain,DC=domain,DC=com
  username: ldap_bind_username
  password: ldap_bind_password
  user:
    dn:
      #pattern: sAMAccountName={0},OU=Users,OU=Group4,OU=Group3,OU=Group2,OU=Group1
      pattern: sAMAccountName={0}
  search:
    filter: (&(objectClass=user)(sAMAccountName={0})) 

这是我的 LdapConfig 类的相关部分

/**
 * 
 */
package xyz;

import static org.springframework.security.config.Customizer.withDefaults;

import java.util.Arrays;
import java.util.List;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.security.ldap.userdetails.LdapUserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

...

/**
 * Configures the HTTP Security of the application, and the LDAP
 * authentication manager.
 * 
 * @author  myself
 *
 */
@SuppressWarnings("deprecation")
@EnableWebSecurity
@Configuration
public class LdapSecurityConfig extends WebSecurityConfigurerAdapter {

    @Value("${ldap.urls}")
    private String ldapUrls;

    @Value("${ldap.base.dn}")
    private String ldapBaseDn;

    @Value("${ldap.search.filter}")
    private String ldapSearchFilter;

    @Value("${ldap.username}")
    private String ldapSecurityPrincipal;

    @Value("${ldap.password}")
    private String ldapPrincipalPassword;

    @Value("${ldap.user.dn.pattern}")
    private String ldapUserDnPattern;
    
    @Value("${cors.allowedOrigins}")
    private List<String> corsAllowedOrigins;
    
    @Autowired
    private JwtFilter jwtFilter;
    
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
        .cors(withDefaults())   // Activates CORS support via CORS configuration (see below)
        .csrf()                 // Disable CSRF protection, since we implement a stateless 
            .disable()          // session management
        .authorizeRequests()
            .antMatchers("/authenticate").permitAll()   // Allow unauthenticated requests to /authenticate
            .anyRequest().authenticated()               // Forbid unauthenticated requests everywhere else
        .and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()  // Place the JWT Filter before user/pw authentication filter, to ensure it gets called before any endpoint
        .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class).exceptionHandling()
        .authenticationEntryPoint(
            (request, response, ex) -> {
                response.sendError(
                    HttpServletResponse.SC_UNAUTHORIZED,
                    ex.getMessage()
                );
            }
        );
    }
    
    /**
     * Configures the LDAP authentication manager, and set it in the AuthenticationManagerBuilder.
     * 
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        
        auth
            .ldapAuthentication()
                .contextSource()
                .url(ldapUrls + ldapBaseDn)
                .managerDn(ldapSecurityPrincipal)
                .managerPassword(ldapPrincipalPassword)
            .and()
                .userDnPatterns(ldapUserDnPattern)
            .and()
                .userDetailsService(ldapUserDetailsService())
        ;
    }

    
    /**
     * CORS configuration.
     * 
     * @return
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        
        CorsConfiguration configuration = new CorsConfiguration();
        
        // The URLs for which cross-origin requests are allowed
        configuration.setAllowedOrigins(corsAllowedOrigins);
        
        // List of allowed HTTP verbs
        configuration.setAllowedMethods(Arrays.asList("GET","POST"));
        
        // Allowing all headers for now - ideally this should be narrowed 
        // down to the strictly necessary
        configuration.setAllowedHeaders(Arrays.asList("*"));
        
        // Sets the CORS configuration to all url mappings
        UrlBasedCorsConfigurationSource source = new
        UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration);
        
        return source; 
    }
    
    @Bean
    AppAuthenticationSuccessHandler appAuthenticationSuccessHandler() {
        return new AppAuthenticationSuccessHandler();
    }
    
    @Bean
    AppAuthenticationFailureHandler appAuthenticationFailureHandler() {
        return new AppAuthenticationFailureHandler();
    }
    
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        
        return super.authenticationManagerBean();
    }
    
    /**
     * Declares and configures an LdapUserDetailsService bean, which will be
     * used to e.g. retrieve the user details by username from the LDAP tree. 
     * 
     * @return
     */
    @Bean
    public LdapUserDetailsService ldapUserDetailsService() {
        
        LdapContextSource ldapContextSource = new LdapContextSource();
        ldapContextSource.setUrl(ldapUrls);
        ldapContextSource.setUserDn(ldapSecurityPrincipal);
        ldapContextSource.setPassword(ldapPrincipalPassword);
        ldapContextSource.setReferral("follow");
        ldapContextSource.afterPropertiesSet();
        
        return new LdapUserDetailsService(
                    new FilterBasedLdapUserSearch(ldapBaseDn,
                                                  ldapSearchFilter, 
                                                  ldapContextSource)
                    );
    }
}

这就是我发现的:

这是 DAOAuthenticationProvider 中的方法,用于检查输入的密码是否与 AD 中的用户密码匹配

@Override
    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
        String presentedPassword = authentication.getCredentials().toString();
        if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { // PROBLEM HERE!!!
            this.logger.debug("Failed to authenticate since password does not match stored value");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }

问题是 userDetails.getPassword() 为空。因此,无法找到有助于找到合适的密码编码器的编码密码前缀(应该从 AD 返回?),这导致了错误。

我注意到的一件事是,当我尝试针对 forumsys.com ldap 测试服务器进行身份验证时,我没有任何问题,并且 userDetails.getPassword() 调用确实检索到一个值,而不是 null。

我可能做错了什么?我花了更多的时间来解决这个问题,然后我想承认:-)非常感谢一些帮助。谢谢!

spring-boot active-directory ldap
1个回答
0
投票

spring 文档说“Active Directory 支持自己的非标准身份验证选项,正常使用模式与标准 LdapAuthenticationProvider 不太吻合”,并继续推荐使用 ActiveDirectoryLdapAuthenticationProvider,我看到它已经在您的导入中。

medium.com 上有一个 示例 显示了如何使用它。

如果将方法 protected void configure() 替换为以下内容,它应该可以工作。

    @Bean
    public ActiveDirectoryLdapAuthenticationProvider activeDirectoryAuthenticationManager() {
        return new ActiveDirectoryLdapAuthenticationProvider(myDomain, ldapUrl);
    }
© www.soinside.com 2019 - 2024. All rights reserved.