Java SpringSecurity Active Directory正在进行身份验证,但在获取用户数据时引发异常
我能够使用提供的凭据创建DirContext。因此,似乎我正在连接ldap服务器并验证凭据,但稍后我们会在从这些凭据获得的上下文上进行.search。这就是失败。除了显示如何验证凭据工作的代码和似乎失败的代码之外,我还包括了我的spring安全配置 spring安全配置Java SpringSecurity Active Directory正在进行身份验证,但在获取用户数据时引发异常,java,spring-security,spring-ldap,Java,Spring Security,Spring Ldap,我能够使用提供的凭据创建DirContext。因此,似乎我正在连接ldap服务器并验证凭据,但稍后我们会在从这些凭据获得的上下文上进行.search。这就是失败。除了显示如何验证凭据工作的代码和似乎失败的代码之外,我还包括了我的spring安全配置 spring安全配置 <?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<http pattern="/ui/login" security="none"></http>
<http pattern="/styles" security="none"/>
<http use-expressions="true">
<intercept-url pattern="/views/*" access="isAuthenticated()" />
<intercept-url pattern="/database/upload" access="isAuthenticated()" />
<intercept-url pattern="/database/save" access="isAuthenticated()" />
<intercept-url pattern="/database/list" access="isAuthenticated()" />
<intercept-url pattern="/database/delete" access="isAuthenticated()" />
<intercept-url pattern="/project/*" access="isAuthenticated()" />
<intercept-url pattern="/file/*" access="isAuthenticated()" />
<intercept-url pattern="/amazon/*" access="isAuthenticated()" />
<intercept-url pattern="/python/*" access="isAuthenticated()" />
<intercept-url pattern="/r/*" access="isAuthenticated()" />
<intercept-url pattern="/project/*" access="isAuthenticated()" />
<intercept-url pattern="/image/*" access="isAuthenticated()" />
<intercept-url pattern="/shell/*" access="isAuthenticated()" />
<intercept-url pattern="/register" access="hasRole('ROLE_ADMIN')" />
<intercept-url pattern="/user/save" access="hasRole('ROLE_ADMIN')" />
<intercept-url pattern="/user/userAdministrator" access="hasRole('ROLE_ADMIN')" />
<intercept-url pattern="/user/list" access="isAuthenticated()" />
<intercept-url pattern="/user/archive" access="isAuthenticated()" />
<form-login login-page="/login" default-target-url="/views/main"
authentication-failure-url="/loginfailed"/>
<logout logout-success-url="/login" />
</http>
<beans:bean id="ldapAuthProvider"
class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
<beans:constructor-arg value="simplead.blazingdb.com" />
<beans:constructor-arg value="ldap://simplead.blazingdb.com/" />
</beans:bean>
<authentication-manager alias="authenticationManager" erase-credentials="true">
<authentication-provider ref="ldapAuthProvider">
</authentication-provider>
</authentication-manager>
</beans:beans>
只要我传递了正确的凭据,它就会返回,如果我发送了错误的凭据,它就会失败,因此我知道我们已经做到了这一点
这个问题来自SpringSecurityLdapTemplate
public static DirContextOperations searchForSingleEntryInternal(DirContext ctx, SearchControls searchControls,
String base, String filter, Object[] params) throws NamingException {
final DistinguishedName ctxBaseDn = new DistinguishedName(ctx.getNameInNamespace());
final DistinguishedName searchBaseDn = new DistinguishedName(base);
final NamingEnumeration<SearchResult> resultsEnum = ctx.search(searchBaseDn, filter, params, searchControls);
if (logger.isDebugEnabled()) {
logger.debug("Searching for entry under DN '" + ctxBaseDn
+ "', base = '" + searchBaseDn + "', filter = '" + filter + "'");
}
Set<DirContextOperations> results = new HashSet<DirContextOperations>();
try {
while (resultsEnum.hasMore()) {
SearchResult searchResult = resultsEnum.next();
// Work out the DN of the matched entry
DistinguishedName dn = new DistinguishedName(new CompositeName(searchResult.getName()));
if (base.length() > 0) {
dn.prepend(searchBaseDn);
}
if (logger.isDebugEnabled()) {
logger.debug("Found DN: " + dn);
}
results.add(new DirContextAdapter(searchResult.getAttributes(), dn, ctxBaseDn));
}
} catch (PartialResultException e) {
LdapUtils.closeEnumeration(resultsEnum);
logger.info("Ignoring PartialResultException");
}
if (results.size() == 0) {
throw new IncorrectResultSizeDataAccessException(1, 0);
}
if (results.size() > 1) {
throw new IncorrectResultSizeDataAccessException(1, results.size());
}
return results.iterator().next();
}
public static DirContextOperations searchForSingleEntryInternal(DirContext ctx、SearchControls、SearchControls、,
字符串基、字符串筛选器、对象[]参数)引发NamingException{
final differentiedname ctxBaseDn=新的differentiedname(ctx.getNameInNamespace());
最终区分名称searchBaseDn=新的区分名称(基本);
最终NamingEnumeration resultsEnum=ctx.search(searchBaseDn、筛选器、参数、searchControls);
if(logger.isDebugEnabled()){
logger.debug(“搜索DN下的条目””+ctxBaseDn
+“,base='”+searchBaseDn+”,filter='“+filter+””;
}
Set results=new HashSet();
试一试{
while(resultnum.hasMore()){
SearchResult SearchResult=resultsum.next();
//计算出匹配条目的DN
DifferentizedName dn=新的DifferentizedName(新的CompositeName(searchResult.getName());
if(base.length()>0){
dn.prepend(searchBaseDn);
}
if(logger.isDebugEnabled()){
调试(“发现DN:+DN”);
}
add(新的DirContextAdapter(searchResult.getAttributes(),dn,ctxBaseDn));
}
}捕获(PartialResultException e){
LdapUtils.closeEnumeration(resultsum);
info(“忽略PartialResultException”);
}
如果(results.size()==0){
抛出新的IncorrectResultSizeDataAccessException(1,0);
}
如果(results.size()>1){
抛出新的IncorrectResultSizeDataAccessException(1,results.size());
}
返回results.iterator().next();
}
具体来说,我认为下面这一行是我看到问题的地方。当它期望1时,我们得到大小为0的返回,因此它抛出一个错误,整个过程失败
final NamingEnumeration<SearchResult> resultsEnum = ctx.search(searchBaseDn, filter, params, searchControls);
final namingumeration resultsum=ctx.search(searchBaseDn、filter、params、searchControls);
每当他尝试执行resultsEnum.hasMore()时,我们都会捕获PartialResultsException
我想弄清楚为什么会这样。我使用的是Amazon简单目录服务(由Samba支持的服务,而不是MSFT版本)。我对LDAP和Active Directory非常陌生,因此如果我的问题格式不好,请告诉我需要添加哪些信息。当我使用Apache Directory Studio尝试运行Spring Security Active Directory默认设置中的LDAP查询时,问题非常简单。他们假设您有一个名为userPrincipalName的属性,它是sAMAccountName和域的组合 最后,我必须将searchFilter设置为使用sAMAccountName,并且我必须制作自己的ActiveDirectoryLdapAuthenticationProvider版本,该版本只查找正在使用的域内的用户,但只比较sAMAccountName。我只更改了searchForUser,但因为这是最后一个类,所以我不得不将它复制过来。我讨厌必须这样做,但我需要继续前进,这些是SpringSecurity3.2.9中不可配置的选项 包org.springframework.security.ldap.authentication.ad
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.support.DefaultDirObjectFactory;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryAuthenticationException;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.OperationNotSupportedException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.InitialLdapContext;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class BlazingActiveDirectory extends AbstractLdapAuthenticationProvider {
private static final Pattern SUB_ERROR_CODE = Pattern.compile(".*data\\s([0-9a-f]{3,4}).*");
// Error codes
private static final int USERNAME_NOT_FOUND = 0x525;
private static final int INVALID_PASSWORD = 0x52e;
private static final int NOT_PERMITTED = 0x530;
private static final int PASSWORD_EXPIRED = 0x532;
private static final int ACCOUNT_DISABLED = 0x533;
private static final int ACCOUNT_EXPIRED = 0x701;
private static final int PASSWORD_NEEDS_RESET = 0x773;
private static final int ACCOUNT_LOCKED = 0x775;
private final String domain;
private final String rootDn;
private final String url;
private boolean convertSubErrorCodesToExceptions;
private String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";
// Only used to allow tests to substitute a mock LdapContext
ContextFactory contextFactory = new ContextFactory();
/**
* @param domain the domain name (may be null or empty)
* @param url an LDAP url (or multiple URLs)
*/
public BlazingActiveDirectory(String domain, String url) {
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
this.url = url;
rootDn = this.domain == null ? null : rootDnFromDomain(this.domain);
}
@Override
protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth) {
String username = auth.getName();
String password = (String) auth.getCredentials();
DirContext ctx = bindAsUser(username, password);
try {
return searchForUser(ctx, username);
} catch (NamingException e) {
logger.error("Failed to locate directory entry for authenticated user: " + username, e);
throw badCredentials(e);
} finally {
LdapUtils.closeContext(ctx);
}
}
/**
* Creates the user authority list from the values of the {@code memberOf} attribute obtained from the user's
* Active Directory entry.
*/
@Override
protected Collection<? extends GrantedAuthority> loadUserAuthorities(DirContextOperations userData, String username, String password) {
String[] groups = userData.getStringAttributes("memberOf");
if (groups == null) {
logger.debug("No values for 'memberOf' attribute.");
return AuthorityUtils.NO_AUTHORITIES;
}
if (logger.isDebugEnabled()) {
logger.debug("'memberOf' attribute values: " + Arrays.asList(groups));
}
ArrayList<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(groups.length);
for (String group : groups) {
authorities.add(new SimpleGrantedAuthority(new DistinguishedName(group).removeLast().getValue()));
}
return authorities;
}
private DirContext bindAsUser(String username, String password) {
// TODO. add DNS lookup based on domain
final String bindUrl = url;
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.SECURITY_AUTHENTICATION, "simple");
String bindPrincipal = createBindPrincipal(username);
env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
env.put(Context.PROVIDER_URL, bindUrl);
env.put(Context.SECURITY_CREDENTIALS, password);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.OBJECT_FACTORIES, DefaultDirObjectFactory.class.getName());
try {
return contextFactory.createContext(env);
} catch (NamingException e) {
if ((e instanceof AuthenticationException) || (e instanceof OperationNotSupportedException)) {
handleBindException(bindPrincipal, e);
throw badCredentials(e);
} else {
throw LdapUtils.convertLdapException(e);
}
}
}
private void handleBindException(String bindPrincipal, NamingException exception) {
if (logger.isDebugEnabled()) {
logger.debug("Authentication for " + bindPrincipal + " failed:" + exception);
}
int subErrorCode = parseSubErrorCode(exception.getMessage());
if (subErrorCode <= 0) {
logger.debug("Failed to locate AD-specific sub-error code in message");
return;
}
logger.info("Active Directory authentication failed: " + subCodeToLogMessage(subErrorCode));
if (convertSubErrorCodesToExceptions) {
raiseExceptionForErrorCode(subErrorCode, exception);
}
}
private int parseSubErrorCode(String message) {
Matcher m = SUB_ERROR_CODE.matcher(message);
if (m.matches()) {
return Integer.parseInt(m.group(1), 16);
}
return -1;
}
private void raiseExceptionForErrorCode(int code, NamingException exception) {
String hexString = Integer.toHexString(code);
Throwable cause = new ActiveDirectoryAuthenticationException(hexString, exception.getMessage(), exception);
switch (code) {
case PASSWORD_EXPIRED:
throw new CredentialsExpiredException(messages.getMessage("LdapAuthenticationProvider.credentialsExpired",
"User credentials have expired"), cause);
case ACCOUNT_DISABLED:
throw new DisabledException(messages.getMessage("LdapAuthenticationProvider.disabled",
"User is disabled"), cause);
case ACCOUNT_EXPIRED:
throw new AccountExpiredException(messages.getMessage("LdapAuthenticationProvider.expired",
"User account has expired"), cause);
case ACCOUNT_LOCKED:
throw new LockedException(messages.getMessage("LdapAuthenticationProvider.locked",
"User account is locked"), cause);
default:
throw badCredentials(cause);
}
}
private String subCodeToLogMessage(int code) {
switch (code) {
case USERNAME_NOT_FOUND:
return "User was not found in directory";
case INVALID_PASSWORD:
return "Supplied password was invalid";
case NOT_PERMITTED:
return "User not permitted to logon at this time";
case PASSWORD_EXPIRED:
return "Password has expired";
case ACCOUNT_DISABLED:
return "Account is disabled";
case ACCOUNT_EXPIRED:
return "Account expired";
case PASSWORD_NEEDS_RESET:
return "User must reset password";
case ACCOUNT_LOCKED:
return "Account locked";
}
return "Unknown (error code " + Integer.toHexString(code) +")";
}
private BadCredentialsException badCredentials() {
return new BadCredentialsException(messages.getMessage(
"LdapAuthenticationProvider.badCredentials", "Bad credentials"));
}
private BadCredentialsException badCredentials(Throwable cause) {
return (BadCredentialsException) badCredentials().initCause(cause);
}
private DirContextOperations searchForUser(DirContext context, String username) throws NamingException {
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
String bindPrincipal = createBindPrincipal(username);
String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);
try {
String verifyName = username;
if(username.indexOf("@") != -1){
verifyName = username.substring(0,username.indexOf("@"));
}
return SpringSecurityLdapTemplate.searchForSingleEntryInternal(context, searchControls,
searchRoot, searchFilter, new Object[]{verifyName});
} catch (IncorrectResultSizeDataAccessException incorrectResults) {
// Search should never return multiple results if properly configured - just rethrow
if (incorrectResults.getActualSize() != 0) {
throw incorrectResults;
}
// If we found no results, then the username/password did not match
UsernameNotFoundException userNameNotFoundException = new UsernameNotFoundException("User " + username
+ " not found in directory.", incorrectResults);
throw badCredentials(userNameNotFoundException);
}
}
private String searchRootFromPrincipal(String bindPrincipal) {
int atChar = bindPrincipal.lastIndexOf('@');
if (atChar < 0) {
logger.debug("User principal '" + bindPrincipal + "' does not contain the domain, and no domain has been configured");
throw badCredentials();
}
return rootDnFromDomain(bindPrincipal.substring(atChar + 1, bindPrincipal.length()));
}
private String rootDnFromDomain(String domain) {
String[] tokens = StringUtils.tokenizeToStringArray(domain, ".");
StringBuilder root = new StringBuilder();
for (String token : tokens) {
if (root.length() > 0) {
root.append(',');
}
root.append("dc=").append(token);
}
return root.toString();
}
String createBindPrincipal(String username) {
if (domain == null || username.toLowerCase().endsWith(domain)) {
return username;
}
return username + "@" + domain;
}
/**
* By default, a failed authentication (LDAP error 49) will result in a {@code BadCredentialsException}.
* <p>
* If this property is set to {@code true}, the exception message from a failed bind attempt will be parsed
* for the AD-specific error code and a {@link CredentialsExpiredException}, {@link DisabledException},
* {@link AccountExpiredException} or {@link LockedException} will be thrown for the corresponding codes. All
* other codes will result in the default {@code BadCredentialsException}.
*
* @param convertSubErrorCodesToExceptions {@code true} to raise an exception based on the AD error code.
*/
public void setConvertSubErrorCodesToExceptions(boolean convertSubErrorCodesToExceptions) {
this.convertSubErrorCodesToExceptions = convertSubErrorCodesToExceptions;
}
/**
* The LDAP filter string to search for the user being authenticated.
* Occurrences of {0} are replaced with the {@code username@domain}.
* <p>
* Defaults to: {@code (&(objectClass=user)(userPrincipalName={0}))}
* </p>
*
* @param searchFilter the filter string
*
* @since 3.2.6
*/
public void setSearchFilter(String searchFilter) {
Assert.hasText(searchFilter,"searchFilter must have text");
this.searchFilter = searchFilter;
}
static class ContextFactory {
DirContext createContext(Hashtable<?,?> env) throws NamingException {
return new InitialLdapContext(env, null);
}
}
}
import org.springframework.dao.incorrectresultsizedaataaccessexception;
导入org.springframework.ldap.core.DirContextOperations;
导入org.springframework.ldap.core.differentiedName;
导入org.springframework.ldap.core.support.DefaultDirObjectFactory;
导入org.springframework.ldap.support.LdapUtils;
导入org.springframework.security.authentication.AccountExpiredException;
导入org.springframework.security.authentication.BadCredentialsException;
导入org.springframework.security.authentication.CredentialsExpiredException;
导入org.springframework.security.authentication.DisabledException;
导入org.springframework.security.authentication.LockedException;
导入org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
导入org.springframework.security.core.GrantedAuthority;
导入org.springframework.security.core.authority.AuthorityUtils;
导入org.springframework.security.core.authority.SimpleGrantedAuthority;
导入org.springframework.security.core.userdetails.UsernameNotFoundException;
导入org.springframework.security.ldap.SpringSecurityLdapTemplate;
导入org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
导入org.springframework.security.ldap.authentication.ad.ActiveDirectoryAuthenticationException;
导入org.springframework.util.Assert;
导入org.springframework.util.StringUtils;
导入javax.naming.AuthenticationException;
导入javax.naming.Context;
导入javax.naming.NamingException;
导入javax.naming.OperationNotSupportedException;
导入javax.naming.directory.DirContext;
导入javax.naming.directory.SearchControls;
导入javax.naming.ldap.InitialLdapContext;
导入java.util.*;
导入java.util.regex.Matcher;
导入java.util.regex.Pattern;
公共最终类BlazingActiveDirectory扩展了AbstractLdapAuthenticationProvider{
私有静态最终模式SUB_ERROR_CODE=Pattern.compile(“.*data\\s([0-9a-f]{3,4})。*”;
//错误代码
私有静态final int USERNAME_NOT_FOUND=0x525;
私有静态final int INVALID_PASSWORD=0x52e;
私人静态决赛
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.support.DefaultDirObjectFactory;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryAuthenticationException;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.OperationNotSupportedException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.InitialLdapContext;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class BlazingActiveDirectory extends AbstractLdapAuthenticationProvider {
private static final Pattern SUB_ERROR_CODE = Pattern.compile(".*data\\s([0-9a-f]{3,4}).*");
// Error codes
private static final int USERNAME_NOT_FOUND = 0x525;
private static final int INVALID_PASSWORD = 0x52e;
private static final int NOT_PERMITTED = 0x530;
private static final int PASSWORD_EXPIRED = 0x532;
private static final int ACCOUNT_DISABLED = 0x533;
private static final int ACCOUNT_EXPIRED = 0x701;
private static final int PASSWORD_NEEDS_RESET = 0x773;
private static final int ACCOUNT_LOCKED = 0x775;
private final String domain;
private final String rootDn;
private final String url;
private boolean convertSubErrorCodesToExceptions;
private String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";
// Only used to allow tests to substitute a mock LdapContext
ContextFactory contextFactory = new ContextFactory();
/**
* @param domain the domain name (may be null or empty)
* @param url an LDAP url (or multiple URLs)
*/
public BlazingActiveDirectory(String domain, String url) {
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
this.url = url;
rootDn = this.domain == null ? null : rootDnFromDomain(this.domain);
}
@Override
protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth) {
String username = auth.getName();
String password = (String) auth.getCredentials();
DirContext ctx = bindAsUser(username, password);
try {
return searchForUser(ctx, username);
} catch (NamingException e) {
logger.error("Failed to locate directory entry for authenticated user: " + username, e);
throw badCredentials(e);
} finally {
LdapUtils.closeContext(ctx);
}
}
/**
* Creates the user authority list from the values of the {@code memberOf} attribute obtained from the user's
* Active Directory entry.
*/
@Override
protected Collection<? extends GrantedAuthority> loadUserAuthorities(DirContextOperations userData, String username, String password) {
String[] groups = userData.getStringAttributes("memberOf");
if (groups == null) {
logger.debug("No values for 'memberOf' attribute.");
return AuthorityUtils.NO_AUTHORITIES;
}
if (logger.isDebugEnabled()) {
logger.debug("'memberOf' attribute values: " + Arrays.asList(groups));
}
ArrayList<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(groups.length);
for (String group : groups) {
authorities.add(new SimpleGrantedAuthority(new DistinguishedName(group).removeLast().getValue()));
}
return authorities;
}
private DirContext bindAsUser(String username, String password) {
// TODO. add DNS lookup based on domain
final String bindUrl = url;
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.SECURITY_AUTHENTICATION, "simple");
String bindPrincipal = createBindPrincipal(username);
env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
env.put(Context.PROVIDER_URL, bindUrl);
env.put(Context.SECURITY_CREDENTIALS, password);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.OBJECT_FACTORIES, DefaultDirObjectFactory.class.getName());
try {
return contextFactory.createContext(env);
} catch (NamingException e) {
if ((e instanceof AuthenticationException) || (e instanceof OperationNotSupportedException)) {
handleBindException(bindPrincipal, e);
throw badCredentials(e);
} else {
throw LdapUtils.convertLdapException(e);
}
}
}
private void handleBindException(String bindPrincipal, NamingException exception) {
if (logger.isDebugEnabled()) {
logger.debug("Authentication for " + bindPrincipal + " failed:" + exception);
}
int subErrorCode = parseSubErrorCode(exception.getMessage());
if (subErrorCode <= 0) {
logger.debug("Failed to locate AD-specific sub-error code in message");
return;
}
logger.info("Active Directory authentication failed: " + subCodeToLogMessage(subErrorCode));
if (convertSubErrorCodesToExceptions) {
raiseExceptionForErrorCode(subErrorCode, exception);
}
}
private int parseSubErrorCode(String message) {
Matcher m = SUB_ERROR_CODE.matcher(message);
if (m.matches()) {
return Integer.parseInt(m.group(1), 16);
}
return -1;
}
private void raiseExceptionForErrorCode(int code, NamingException exception) {
String hexString = Integer.toHexString(code);
Throwable cause = new ActiveDirectoryAuthenticationException(hexString, exception.getMessage(), exception);
switch (code) {
case PASSWORD_EXPIRED:
throw new CredentialsExpiredException(messages.getMessage("LdapAuthenticationProvider.credentialsExpired",
"User credentials have expired"), cause);
case ACCOUNT_DISABLED:
throw new DisabledException(messages.getMessage("LdapAuthenticationProvider.disabled",
"User is disabled"), cause);
case ACCOUNT_EXPIRED:
throw new AccountExpiredException(messages.getMessage("LdapAuthenticationProvider.expired",
"User account has expired"), cause);
case ACCOUNT_LOCKED:
throw new LockedException(messages.getMessage("LdapAuthenticationProvider.locked",
"User account is locked"), cause);
default:
throw badCredentials(cause);
}
}
private String subCodeToLogMessage(int code) {
switch (code) {
case USERNAME_NOT_FOUND:
return "User was not found in directory";
case INVALID_PASSWORD:
return "Supplied password was invalid";
case NOT_PERMITTED:
return "User not permitted to logon at this time";
case PASSWORD_EXPIRED:
return "Password has expired";
case ACCOUNT_DISABLED:
return "Account is disabled";
case ACCOUNT_EXPIRED:
return "Account expired";
case PASSWORD_NEEDS_RESET:
return "User must reset password";
case ACCOUNT_LOCKED:
return "Account locked";
}
return "Unknown (error code " + Integer.toHexString(code) +")";
}
private BadCredentialsException badCredentials() {
return new BadCredentialsException(messages.getMessage(
"LdapAuthenticationProvider.badCredentials", "Bad credentials"));
}
private BadCredentialsException badCredentials(Throwable cause) {
return (BadCredentialsException) badCredentials().initCause(cause);
}
private DirContextOperations searchForUser(DirContext context, String username) throws NamingException {
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
String bindPrincipal = createBindPrincipal(username);
String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);
try {
String verifyName = username;
if(username.indexOf("@") != -1){
verifyName = username.substring(0,username.indexOf("@"));
}
return SpringSecurityLdapTemplate.searchForSingleEntryInternal(context, searchControls,
searchRoot, searchFilter, new Object[]{verifyName});
} catch (IncorrectResultSizeDataAccessException incorrectResults) {
// Search should never return multiple results if properly configured - just rethrow
if (incorrectResults.getActualSize() != 0) {
throw incorrectResults;
}
// If we found no results, then the username/password did not match
UsernameNotFoundException userNameNotFoundException = new UsernameNotFoundException("User " + username
+ " not found in directory.", incorrectResults);
throw badCredentials(userNameNotFoundException);
}
}
private String searchRootFromPrincipal(String bindPrincipal) {
int atChar = bindPrincipal.lastIndexOf('@');
if (atChar < 0) {
logger.debug("User principal '" + bindPrincipal + "' does not contain the domain, and no domain has been configured");
throw badCredentials();
}
return rootDnFromDomain(bindPrincipal.substring(atChar + 1, bindPrincipal.length()));
}
private String rootDnFromDomain(String domain) {
String[] tokens = StringUtils.tokenizeToStringArray(domain, ".");
StringBuilder root = new StringBuilder();
for (String token : tokens) {
if (root.length() > 0) {
root.append(',');
}
root.append("dc=").append(token);
}
return root.toString();
}
String createBindPrincipal(String username) {
if (domain == null || username.toLowerCase().endsWith(domain)) {
return username;
}
return username + "@" + domain;
}
/**
* By default, a failed authentication (LDAP error 49) will result in a {@code BadCredentialsException}.
* <p>
* If this property is set to {@code true}, the exception message from a failed bind attempt will be parsed
* for the AD-specific error code and a {@link CredentialsExpiredException}, {@link DisabledException},
* {@link AccountExpiredException} or {@link LockedException} will be thrown for the corresponding codes. All
* other codes will result in the default {@code BadCredentialsException}.
*
* @param convertSubErrorCodesToExceptions {@code true} to raise an exception based on the AD error code.
*/
public void setConvertSubErrorCodesToExceptions(boolean convertSubErrorCodesToExceptions) {
this.convertSubErrorCodesToExceptions = convertSubErrorCodesToExceptions;
}
/**
* The LDAP filter string to search for the user being authenticated.
* Occurrences of {0} are replaced with the {@code username@domain}.
* <p>
* Defaults to: {@code (&(objectClass=user)(userPrincipalName={0}))}
* </p>
*
* @param searchFilter the filter string
*
* @since 3.2.6
*/
public void setSearchFilter(String searchFilter) {
Assert.hasText(searchFilter,"searchFilter must have text");
this.searchFilter = searchFilter;
}
static class ContextFactory {
DirContext createContext(Hashtable<?,?> env) throws NamingException {
return new InitialLdapContext(env, null);
}
}
}