我已经使用 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 文档说“Active Directory 支持自己的非标准身份验证选项,正常使用模式与标准 LdapAuthenticationProvider 不太吻合”,并继续推荐使用 ActiveDirectoryLdapAuthenticationProvider,我看到它已经在您的导入中。
medium.com 上有一个 示例 显示了如何使用它。
如果将方法 protected void configure() 替换为以下内容,它应该可以工作。
@Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryAuthenticationManager() {
return new ActiveDirectoryLdapAuthenticationProvider(myDomain, ldapUrl);
}