【问题标题】:SpringSecurity Active Directory authenticating but throwing exception getting user dataSpringSecurity Active Directory 进行身份验证但抛出异常获取用户数据
【发布时间】:2019-02-17 12:00:51
【问题描述】:

我可以使用提供的凭据创建 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 非常陌生,所以如果我的问题格式不正确,请告诉我需要添加哪些信息。

【问题讨论】:

    标签: java spring-security spring-ldap


    【解决方案1】:

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

    编辑:

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

    【讨论】:

      【解决方案2】:

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

      最后,我必须将 searchFilter 设置为使用 sAMAccountName,并且我必须制作自己的 ActiveDirectoryLdapAuthenticationProvider 版本,它只查找正在使用的域内的用户,但只比较 sAMAccountName。我只更改了 searchForUser 但由于这是最后一堂课,我不得不将其复制过来。我讨厌这样做,但我需要继续前进,这些在 Spring Security 3.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);
             }
         }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-05-12
        • 2018-11-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多