SpringSecurity Active Directory身份验证但抛出异常获取用户数据

时间:2016-12-20 18:36:45

标签: java spring-security spring-ldap

我可以使用提供的凭据创建DirContext。所以我似乎连接到ldap服务器并验证凭据,但稍后我们对这些凭据获取的上下文进行了.search。这是失败的。除了显示我验证凭据是如何工作的代码以及似乎失败的代码之外,我还包括了我的spring安全配置。

spring-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>  

来自ActiveDirectoryLdapAuthenticationProvider

@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();
    } finally {
        LdapUtils.closeContext(ctx);
    }
    }

只要我传入正确的凭据就会返回,只要我发送错误的凭据就会失败,所以我知道我们已经做到了这一点。

问题来自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();
    }

具体而言,我认为以下几行是我遇到问题的地方。当它期望为1时,我们得到大小为0的返回,因此它会抛出错误,整个事情都会失败。

final NamingEnumeration<SearchResult> resultsEnum = ctx.search(searchBaseDn, filter, params, searchControls);

每当我尝试执行resultsEnum.hasMore()时,我们都会捕获一个PartialResultsException。

我想弄清楚为什么会这样。我正在使用Amazon Simple目录服务(由Samba支持的服务而不是MSFT版本)。我是LDAP和Active Directory的新手,所以如果我的问题形成不好,请告诉我需要添加的信息。

2 个答案:

答案 0 :(得分:1)

在方法searchForUser中,该方法称为SpringSecurityLdapTemplate.searchForSingleEntryInternal方法,在该方法中传递了对象数组。数组的第一个对象与username @ domain有关。第二个是用户名本身。因此,当您在ActiveDirectory中搜索(&(objectClass = user)(sAMAccountName = {0}))时,您会将username @ domain作为属性传递给搜索的参数{0}。您只需要通过如下搜索过滤器: (&(objectClass = user)(sAMAccountName = {1}))

编辑:

我假设您已将searchFilter传递给ActiveDirectoryLdapAuthenticationProvider对象。如果没有,则必须这样做。

答案 1 :(得分:0)

一旦我使用Apache Directory Studio尝试运行来自Spring Security Active Directory默认值的ldap查询,问题就非常简单了。他们假设您有一个名为userPrincipalName的属性,它是sAMAccountName和域的组合。

最后,我必须将searchFilter设置为使用sAMAccountName,我必须创建自己的ActiveDirectoryLdapAuthenticationProvider版本,该版本仅查找正在使用的域内的用户,但仅比较sAMAccountName。我只改变了searchForUser,但由于这是一个最后的课程,我必须将其复制一遍。我讨厌必须这样做,但我需要继续前进,这些不是Spring Security 3.2.9中的可配置选项。

package 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);
       }
   }
}