如何使用Spring Security对Active Directory服务器进行身份验证?

32

我正在编写一个Spring Web应用程序,需要用户登录。我的公司有一个Active Directory服务器,我希望利用它来实现这个目的。然而,我在使用Spring Security连接服务器时遇到了问题。

我正在使用Spring 2.5.5和Spring Security 2.0.3,以及Java 1.6。

如果我更改LDAP URL为错误的IP地址,它不会抛出任何异常,因此我想知道它是否正在尝试连接服务器。

虽然Web应用程序启动得很好,但是我输入到登录页面中的任何信息都被拒绝了。我之前使用过InMemoryDaoImpl,这个工作正常,所以我的应用程序的其余部分似乎已经配置正确了。

这是我与安全相关的bean:

  <beans:bean id="ldapAuthProvider" class="org.springframework.security.providers.ldap.LdapAuthenticationProvider">
    <beans:constructor-arg>
      <beans:bean class="org.springframework.security.providers.ldap.authenticator.BindAuthenticator">
        <beans:constructor-arg ref="initialDirContextFactory" />
        <beans:property name="userDnPatterns">
          <beans:list>
            <beans:value>CN={0},OU=SBSUsers,OU=Users,OU=MyBusiness,DC=Acme,DC=com</beans:value>
          </beans:list>
        </beans:property>
      </beans:bean>
    </beans:constructor-arg>
  </beans:bean>

  <beans:bean id="userDetailsService" class="org.springframework.security.userdetails.ldap.LdapUserDetailsManager">
    <beans:constructor-arg ref="initialDirContextFactory" />
  </beans:bean>

  <beans:bean id="initialDirContextFactory" class="org.springframework.security.ldap.DefaultInitialDirContextFactory">
    <beans:constructor-arg value="ldap://192.168.123.456:389/DC=Acme,DC=com" />
  </beans:bean>

这不是一个答案,更像是一个澄清的问题——您是否已经将Spring Security软件包的日志记录级别设置为最高? - Jim Kiley
我已经开启了所有日志记录。但是没有看到任何被记录的消息...我已经更新了我的问题,并附上了我的Log4J配置。 - Michael
请告诉我需要哪些jar文件。 - Aadi
8个回答

37

我曾经也有过你所描述的那种让人抓狂的经历,最终我编写了一个自定义认证提供程序,对Active Directory服务器进行LDAP查询。

因此,我的与安全相关的bean如下:

<beans:bean id="contextSource"
    class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
    <beans:constructor-arg value="ldap://hostname.queso.com:389/" />
</beans:bean>

<beans:bean id="ldapAuthenticationProvider"
    class="org.queso.ad.service.authentication.LdapAuthenticationProvider">
    <beans:property name="authenticator" ref="ldapAuthenticator" />
    <custom-authentication-provider />
</beans:bean>

<beans:bean id="ldapAuthenticator"
    class="org.queso.ad.service.authentication.LdapAuthenticatorImpl">
    <beans:property name="contextFactory" ref="contextSource" />
    <beans:property name="principalPrefix" value="QUESO\" />
</beans:bean>

然后是 LdapAuthenticationProvider 类:
/**
 * Custom Spring Security authentication provider which tries to bind to an LDAP server with
 * the passed-in credentials; of note, when used with the custom {@link LdapAuthenticatorImpl},
 * does <strong>not</strong> require an LDAP username and password for initial binding.
 * 
 * @author Jason
 */
public class LdapAuthenticationProvider implements AuthenticationProvider {

    private LdapAuthenticator authenticator;

    public Authentication authenticate(Authentication auth) throws AuthenticationException {

        // Authenticate, using the passed-in credentials.
        DirContextOperations authAdapter = authenticator.authenticate(auth);

        // Creating an LdapAuthenticationToken (rather than using the existing Authentication
        // object) allows us to add the already-created LDAP context for our app to use later.
        LdapAuthenticationToken ldapAuth = new LdapAuthenticationToken(auth, "ROLE_USER");
        InitialLdapContext ldapContext = (InitialLdapContext) authAdapter
                .getObjectAttribute("ldapContext");
        if (ldapContext != null) {
            ldapAuth.setContext(ldapContext);
        }

        return ldapAuth;
    }

    public boolean supports(Class clazz) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(clazz));
    }

    public LdapAuthenticator getAuthenticator() {
        return authenticator;
    }

    public void setAuthenticator(LdapAuthenticator authenticator) {
        this.authenticator = authenticator;
    }

}

然后是 LdapAuthenticatorImpl 类:
/**
 * Custom Spring Security LDAP authenticator which tries to bind to an LDAP server using the
 * passed-in credentials; does <strong>not</strong> require "master" credentials for an
 * initial bind prior to searching for the passed-in username.
 * 
 * @author Jason
 */
public class LdapAuthenticatorImpl implements LdapAuthenticator {

    private DefaultSpringSecurityContextSource contextFactory;
    private String principalPrefix = "";

    public DirContextOperations authenticate(Authentication authentication) {

        // Grab the username and password out of the authentication object.
        String principal = principalPrefix + authentication.getName();
        String password = "";
        if (authentication.getCredentials() != null) {
            password = authentication.getCredentials().toString();
        }

        // If we have a valid username and password, try to authenticate.
        if (!("".equals(principal.trim())) && !("".equals(password.trim()))) {
            InitialLdapContext ldapContext = (InitialLdapContext) contextFactory
                    .getReadWriteContext(principal, password);

            // We need to pass the context back out, so that the auth provider can add it to the
            // Authentication object.
            DirContextOperations authAdapter = new DirContextAdapter();
            authAdapter.addAttributeValue("ldapContext", ldapContext);

            return authAdapter;
        } else {
            throw new BadCredentialsException("Blank username and/or password!");
        }
    }

    /**
     * Since the InitialLdapContext that's stored as a property of an LdapAuthenticationToken is
     * transient (because it isn't Serializable), we need some way to recreate the
     * InitialLdapContext if it's null (e.g., if the LdapAuthenticationToken has been serialized
     * and deserialized). This is that mechanism.
     * 
     * @param authenticator
     *          the LdapAuthenticator instance from your application's context
     * @param auth
     *          the LdapAuthenticationToken in which to recreate the InitialLdapContext
     * @return
     */
    static public InitialLdapContext recreateLdapContext(LdapAuthenticator authenticator,
            LdapAuthenticationToken auth) {
        DirContextOperations authAdapter = authenticator.authenticate(auth);
        InitialLdapContext context = (InitialLdapContext) authAdapter
                .getObjectAttribute("ldapContext");
        auth.setContext(context);
        return context;
    }

    public DefaultSpringSecurityContextSource getContextFactory() {
        return contextFactory;
    }

    /**
     * Set the context factory to use for generating a new LDAP context.
     * 
     * @param contextFactory
     */
    public void setContextFactory(DefaultSpringSecurityContextSource contextFactory) {
        this.contextFactory = contextFactory;
    }

    public String getPrincipalPrefix() {
        return principalPrefix;
    }

    /**
     * Set the string to be prepended to all principal names prior to attempting authentication
     * against the LDAP server.  (For example, if the Active Directory wants the domain-name-plus
     * backslash prepended, use this.)
     * 
     * @param principalPrefix
     */
    public void setPrincipalPrefix(String principalPrefix) {
        if (principalPrefix != null) {
            this.principalPrefix = principalPrefix;
        } else {
            this.principalPrefix = "";
        }
    }

}

最后,是LdapAuthenticationToken类:
/**
 * <p>
 * Authentication token to use when an app needs further access to the LDAP context used to
 * authenticate the user.
 * </p>
 * 
 * <p>
 * When this is the Authentication object stored in the Spring Security context, an application
 * can retrieve the current LDAP context thusly:
 * </p>
 * 
 * <pre>
 * LdapAuthenticationToken ldapAuth = (LdapAuthenticationToken) SecurityContextHolder
 *      .getContext().getAuthentication();
 * InitialLdapContext ldapContext = ldapAuth.getContext();
 * </pre>
 * 
 * @author Jason
 * 
 */
public class LdapAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = -5040340622950665401L;

    private Authentication auth;
    transient private InitialLdapContext context;
    private List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();

    /**
     * Construct a new LdapAuthenticationToken, using an existing Authentication object and
     * granting all users a default authority.
     * 
     * @param auth
     * @param defaultAuthority
     */
    public LdapAuthenticationToken(Authentication auth, GrantedAuthority defaultAuthority) {
        this.auth = auth;
        if (auth.getAuthorities() != null) {
            this.authorities.addAll(Arrays.asList(auth.getAuthorities()));
        }
        if (defaultAuthority != null) {
            this.authorities.add(defaultAuthority);
        }
        super.setAuthenticated(true);
    }

    /**
     * Construct a new LdapAuthenticationToken, using an existing Authentication object and
     * granting all users a default authority.
     * 
     * @param auth
     * @param defaultAuthority
     */
    public LdapAuthenticationToken(Authentication auth, String defaultAuthority) {
        this(auth, new GrantedAuthorityImpl(defaultAuthority));
    }

    public GrantedAuthority[] getAuthorities() {
        GrantedAuthority[] authoritiesArray = this.authorities.toArray(new GrantedAuthority[0]);
        return authoritiesArray;
    }

    public void addAuthority(GrantedAuthority authority) {
        this.authorities.add(authority);
    }

    public Object getCredentials() {
        return auth.getCredentials();
    }

    public Object getPrincipal() {
        return auth.getPrincipal();
    }

    /**
     * Retrieve the LDAP context attached to this user's authentication object.
     * 
     * @return the LDAP context
     */
    public InitialLdapContext getContext() {
        return context;
    }

    /**
     * Attach an LDAP context to this user's authentication object.
     * 
     * @param context
     *          the LDAP context
     */
    public void setContext(InitialLdapContext context) {
        this.context = context;
    }

}

您会注意到其中有一些可能不需要的部分。
例如,我的应用程序需要保留成功登录的LDAP上下文以供用户在登录后进一步使用 -- 应用程序的目的是让用户通过他们的AD凭据登录,然后执行更多的与AD相关的功能。因此,我有一个自定义的身份验证令牌LdapAuthenticationToken,在其中传递(而不是Spring的默认身份验证令牌),这使我能够附加LDAP上下文。在LdapAuthenticationProvider.authenticate()中,我创建该令牌并将其传回;在LdapAuthenticatorImpl.authenticate()中,我将已登录的上下文附加到返回对象中,以便可以将其添加到用户的Spring身份验证对象中。
同样,在LdapAuthenticationProvider.authenticate()中,我分配所有已登录用户ROLE_USER角色 -- 这就是让我在我的intercept-url元素中测试该角色的原因。您将希望将其与您要测试的任何角色匹配,甚至基于Active Directory组或其他内容分配角色。
最后,与此相关的是,我实现的LdapAuthenticationProvider.authenticate()方式为所有具有有效AD帐户的用户分配了相同的ROLE_USER角色。显然,在该方法中,您可以对用户执行进一步的测试(即,用户是否在特定的AD组中?)并以这种方式分配角色,甚至在授予用户访问权限之前测试某些条件。

请告诉我需要哪些jar文件。谢谢。 - Aadi
我遇到了同样的问题。但是随着Spring新版本中许多这些方法被弃用,我不能再像以前那样完成同样的工作了。我在这里提出了我的问题http://stackoverflow.com/questions/32070142/spring-security-configuratiion-to-authenticate-ldap-user。如果@delfuego能帮助我,我会非常感激。 - moha

21

2
+1 这应该是最佳答案。从经验来看,如果您使用LdapAuthenticationProvider对AD进行身份验证,则会使自己的生活变得非常困难。在现代版本的Spring中,您可以用不到5行代码实现您想要的功能。请参见Luke提供的链接。我在下面的答案中也提供了详细信息。 - Cookalino

2
只是为了将其更新到最新状态。 Spring Security 3.0有一个完整的包,其中包含默认实现专门用于ldap-bind以及查询和比较认证。

1

正如Luke在上面的回答中所述:

Spring Security 3.1有一个专门针对Active Directory的身份验证提供程序。

以下是使用ActiveDirectoryLdapAuthenticationProvider轻松完成此操作的详细信息。

在resources.groovy中:

ldapAuthProvider1(ActiveDirectoryLdapAuthenticationProvider,
        "mydomain.com",
        "ldap://mydomain.com/"
)

在Config.groovy文件中:
grails.plugin.springsecurity.providerNames = ['ldapAuthProvider1']

这是您需要的全部代码。您可以在Config.groovy中删除几乎所有其他 grails.plugin.springsecurity.ldap.* 设置,因为它们不适用于此AD设置。
有关文档,请参见: http://docs.spring.io/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#ldap-active-directory

我有类似的问题。我需要更改视图以使安全控制器自动查找主体吗? - Bmoe

1
如果您正在使用Spring安全4,也可以使用给定的类来实现相同的功能。
  • SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfig.class);

@Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
              .antMatchers("/").permitAll()
              .anyRequest().authenticated();
            .and()
              .formLogin()
            .and()
              .logout();
}

@Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
    ActiveDirectoryLdapAuthenticationProvider authenticationProvider = 
        new ActiveDirectoryLdapAuthenticationProvider("<domain>", "<url>");

    authenticationProvider.setConvertSubErrorCodesToExceptions(true);
    authenticationProvider.setUseAuthenticationRequestCredentials(true);

    return authenticationProvider;
}
}

1

0

根据Luke的回答:

参考资料,Spring Security 3.1有一个身份验证提供程序[专门用于Active Directory][1]。

[1]: http://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#ldap-active-directory

我尝试了使用Spring Security 3.1.1:与ldap相比有一些细微的更改 - 用户所属的活动目录组以原始大小写形式显示。

以前,在ldap下,组名大写并以“ROLE_”为前缀,这使得在项目中进行文本搜索时很容易找到它们,但显然可能会在unix组中引起问题,如果由于某种奇怪的原因有2个仅由大小写区分的不同组(即accounts和Accounts)。

此外,语法需要手动指定域控制器名称和端口,这使得它对冗余性有点可怕。肯定有一种方法可以在Java中查找域的SRV DNS记录,即等效于(来自Samba 4 howto):

$ host -t SRV _ldap._tcp.samdom.example.com.
_ldap._tcp.samdom.example.com has SRV record 0 100 389 samba.samdom.example.com.

跟随常规的A记录查询:

$ host -t A samba.samdom.example.com.
samba.samdom.example.com has address 10.0.0.1

(实际上可能还需要查找_kerberos SRV记录...)

以上是使用Samba4.0rc1的情况,我们正在逐步从Samba 3.x LDAP环境升级到Samba AD环境。


0

没有使用SSL的LDAP身份验证是不安全的,任何人都可以在传输到LDAP服务器时查看用户凭据。我建议使用LDAPS:协议进行身份验证。这不需要在Spring端进行任何主要更改,但您可能会遇到与证书相关的一些问题。有关更多详细信息,请参见{{link1:Spring中带有SSL的LDAP Active Directory身份验证}}。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接