30. LDAP 认证

30.1 概述

LDAP 通常被组织用作用户信息的中央存储库和身份验证服务。它还可以用于存储应用程序用户的角色信息。

对于如何配置 LDAP 服务器,存在许多不同的场景,因此 Spring Security 的 LDAP 提供程序是完全可配置的。它使用单独的策略接口进行身份验证和角色检索,并提供默认实现,这些实现可以被配置为处理各种各样的情况。

在尝试使用 Spring Security 之前,你应该熟悉 LDAP。以下链接很好地介绍了所涉及的概念以及使用免费 LDAP 服务器 OpenLDAP 建立目录的指南:http://www.zytrax.com/books/ldap/。熟悉从 Java 访问 LDAP 的 JNDI API 也可能是有用的。在 LDAP 提供程序中,我们不使用任何第三方 LDAP 库(Mozilla、JLDAP 等),但是 Spring LDAP 得到了广泛的使用,所以如果你打算添加自己的定制,那么熟悉该项目可能会很有用。

在使用 LDAP 认证时,确保正确配置 LDAP 连接池是很重要的。如果不熟悉如何做到这一点,可以参考 Java LDAP 文档。

30.2 使用 LDAP 与 Spring Security

LDAP 认证在 Spring Security 中大致可分为以下几个阶段。

  • 从登录名中获取唯一的 LDAP "Distinguished Name" 或 DN。这通常意味着在目录中执行搜索,除非预先知道用户名到 DNS 的精确映射。因此,用户在登录时可以输入名称 "joe",但是用于向 LDAP 进行身份验证的实际名称将是完整的 DN,例如 uid=joe,ou=users,dc=spring,dc=io。
  • 通过用户作为该用户的 "binding" 或通过对 DN 的目录条目中的密码属性执行用户的密码的远程比较操作对用户进行身份验证。
  • 加载用户的权限列表。

例外情况是 LDAP 目录刚刚被用于检索用户信息并在本地对其进行身份验证。这可能是不可能的,因为目录通常设置有有限的读取访问属性,如用户密码。

我们将在下面查看一些配置方案。有关可用配置选项的完整信息,请参阅安全命名空间模式(XML 编辑器中应该提供安全命名空间模式的信息)。

30.3 配置一个 LDAP 服务器

你需要做的第一件事是配置应该进行身份验证的服务器。这是使用安全命名空间中的 <ldap-server> 元素完成的。可以使用 url 属性将其配置为指向外部 LDAP 服务器:

<ldap-server url="ldap://springframework.org:389/dc=springframework,dc=org" />

30.3.1 使用一个嵌入式测试服务器

也可以使用 <ldap-server> 元素来创建嵌入式服务器,这对于测试和演示非常有用。在这种情况下,不用 url 属性就可以使用它:

<ldap-server root="dc=springframework,dc=org"/>

这里我们指定目录的根目录应该是 "dc=springframework,dc=org",这是默认的。使用这种方式,命名空间解析器将创建一个嵌入式 Apache 目录服务器,并扫描类路径以查找任何 LDIF 文件,它将尝试将其加载到服务器中。可以使用 ldif 属性来自定义此行为,该属性定义要加载的 LDIF 资源:

<ldap-server ldif="classpath:users.ldif" />

这使得用 LDAP 启动和运行更加容易,因为与外部服务器一起工作会很不方便。它还将用户隔离为连接 Apache 目录服务器所需的复杂 bean 配置。使用普通的 bean,配置会更加混乱。你必须具有可供应用程序使用的必要 Apache 目录依赖项 jar。这些可以从 LDAP 示例应用程序中获得。

30.3.2 使用绑定认证

这是最常见的 LDAP 认证方案。

<ldap-authentication-provider user-dn-pattern="uid={0},ou=people"/>

这个简单的示例将通过在所提供的模式中替换用户登录名并尝试使用登录密码绑定作为该用户,从而获得用户的DN。如果所有用户都存储在目录中的单个节点之下,这是可以的。如果你希望配置 LDAP 搜索过滤器来定位用户,则可以使用以下内容:

<ldap-authentication-provider user-search-filter="(uid={0})"
	user-search-base="ou=people"/>

如果与上面的服务器定义一起使用,则这将使用 user-search-filter 属性的值作为过滤器在 DN ou=people,dc=springframework,dc=org 下执行搜索。同样,用户登录名替换过滤器名称中的参数,因此它将搜索一个具有 uid 属性等于用户名的条目。如果不提供 user-search-base,则从根执行搜索。

30.3.3 加载权限

如何从 LDAP 目录中加载组的权限是由以下属性控制的。

  • group-search-base。定义目录树的一部分,在该目录树下进行组搜索。
  • group-role-attribute。该属性包含由组项定义的权限的名称。默认为 cn
  • group-search-filter。用于搜索组成员资格的过滤器。默认值为 uniqueMember={0},对应于 groupOfUniqueNames LDAP 类 [19]。在这种情况下,被替换的参数是用户的全名。如果要在登录名上过滤,则可以使用参数 {1}。

因此,如果我们使用以下配置

<ldap-authentication-provider user-dn-pattern="uid={0},ou=people"
		group-search-base="ou=groups" />

并且被成功认证为用户 "ben",随后的权限加载将在目录条目 ou=groups,dc=springframework,dc=org 下执行搜索,查找包含属性 uniqueMember 的条目,该属性具有值 uid=ben,ou=people,dc=springframework,dc=org。默认情况下,权限名称将具有前缀 ROLE_。可以使用 role-prefix 属性来更改此项。如果不需要任何前缀,请使用 role-prefix="none"。有关加载权限的更多详细信息,请参阅 DefaultLdapAuthoritiesPopulator 类的 Javadoc。

30.4 实现类

上面我们使用的名称空间配置选项使用起来很简单,并且比显式地使用 Spring bean 更简洁。有些情况下,你可能需要知道如何在应用程序上下文中直接配置 Spring Security LDAP。例如,你可能希望自定义某些类的行为。如果你很高兴使用命名空间配置,那么你可以跳过这一部分和下一个部分。

主要的 LDAP 提供程序类,LdapAuthenticationProvider,本身实际上没有做很多工作,但是将工作委托给另外两个 bean,即 LdapAuthenticator 和 LdapAuthoritiesPopulator,这两个 bean 负责分别对用户进行身份验证并检索用户的 GrantedAuthority 设置。

30.4.1 LdapAuthenticator 实现

认证器还负责检索所需的用户属性。这是因为属性的权限可能取决于所使用的身份验证的类型。例如,如果绑定为用户,则可能需要使用用户自己的权限来读取它们。

目前有两种提供 Spring Security 的认证策略:

  • 直接向 LDAP 服务器("bind" 认证)进行身份验证。
  • 密码比较,其中由用户提供的密码与存储在存储库中的密码进行比较。这可以通过检索密码属性的值并在本地检查它来实现,也可以通过执行 LDAP 比较操作来实现,其中提供的密码被传递到服务器以进行比较,并且永远不会检索真正的密码值。

常见功能

在能够(通过任一策略)对用户进行认证之前,必须从提供给应用程序的登录名获得区分名称(DN)。这可以通过简单的模式匹配(通过设置 setUserDnPatterns 数组属性)或通过设置 userSearch 属性来实现。对于 DN 模式匹配方法,使用标准的 Java 模式格式,并且登录名将被替换为参数 {0}。模式应该与配置的 SpringSecurityContextSource 将绑定的 DN 相关(有关此的更多详细信息,请参阅关于 连接到 LDAP 服务器 连接到LDAP服务器的部分)。例如,如果你使用的 LDAP 服务器具有 URL ldap://monkeymachine.co.uk/dc=springframework,dc=org,并且具有模式 uid={0},ou=greatapes,那么登录名 "gorilla"将映射到 DN uid=gorilla,ou=greatapes,dc=springframework,dc=org。每个配置的 DN 模式将被轮流尝试,直到找到匹配为止。有关使用搜索的信息,请参阅下面的搜索对象部分。也可以使用两种方法的组合——首先检查模式,如果没有找到匹配的 DN,则使用搜索。

BindAuthenticator

包 org.springframework.security.ldap.authentication 中的类 BindAuthenticator 实现绑定验证策略。它只是试图绑定用户。

PasswordComparisonAuthenticator

类 PasswordComparisonAuthenticator 实现口令比较认证策略。

30.4.2 连接到 LDAP 服务器

上面讨论的 bean 必须能够连接到服务器。它们都必须提供一个 SpringSecurityContextSource,这是 Spring LDAP 的 ContextSource 的扩展。除非你有特殊要求,否则通常将配置一个 DefaultSpringSecurityContextSource bean,该 bean 可以使用 LDAP 服务器的 URL 配置,也可以使用 "manager" 用户的用户名和密码配置,默认情况下,该用户名和密码将在绑定到服务器时使用(而不是匿名绑定)。有关的更多详细信息,请阅读此类的 Javadoc 和 Spring LDAP 的 AbstractContextSource。

30.4.3 LDAP 搜索对象

通常需要比简单的 DN 匹配更复杂的策略来定位目录中的用户条目。这可以封装在 LdapUserSearch 实例中,该实例可以提供给验证器实现,例如,以允许它们定位用户。提供的实现是 FilterBasedLdapUserSearch.

FilterBasedLdapUserSearch

该 bean 使用 LDAP 过滤器来匹配目录中的用户对象。该过程在 JDK DirContext 类 的 Javadoc 中对相应搜索方法进行了解释。正如在这里所解释的,搜索过滤器可以提供参数。对于这个类,唯一有效的参数是 {0},它将被替换为用户的登录名。

30.4.4 LdapAuthoritiesPopulator

在成功认证用户之后,LdapAuthenticationProvider 将尝试通过调用配置的 LdapAuthoritiesPopulator bean 为用户加载一组权限。DefaultLdapAuthoritiesPopulator 是一个实现,它将通过搜索用户所属的组的目录来加载权限(通常这些组是目录中的 groupOfNames 或 groupOfUniqueNames 条目)。请参阅 Javadoc 以了解该类的工作原理。

如果只希望使用 LDAP 进行身份验证,但是从差异源(例如数据库)加载权限,则可以提供此接口的自己的实现并注入该接口。

30.4.5 Spring Bean 配置

一个典型的配置,使用我们讨论过的一些 bean,可能是这样的:

<bean id="contextSource"
		class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
<constructor-arg value="ldap://monkeymachine:389/dc=springframework,dc=org"/>
<property name="userDn" value="cn=manager,dc=springframework,dc=org"/>
<property name="password" value="password"/>
</bean>

<bean id="ldapAuthProvider"
	class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
<constructor-arg>
<bean class="org.springframework.security.ldap.authentication.BindAuthenticator">
	<constructor-arg ref="contextSource"/>
	<property name="userDnPatterns">
	<list><value>uid={0},ou=people</value></list>
	</property>
</bean>
</constructor-arg>
<constructor-arg>
<bean
	class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
	<constructor-arg ref="contextSource"/>
	<constructor-arg value="ou=groups"/>
	<property name="groupRoleAttribute" value="ou"/>
</bean>
</constructor-arg>
</bean>

这将建立一个提供访问 LDAP 服务器的提供者,使用 URL ldap://monkeymachine:389/dc=springframework,dc=org。认证将通过尝试与 DN uid=<user-login-name>,ou=people,dc=springframework,dc=org 绑定来执行。在成功的认证之后,角色将通过在 DN ou=groups,dc=springframework,dc=org 下使用默认过滤器 (member=<user’s-DN>) 进行搜索分配给用户。角色名称将取自每个匹配的 "ou" 属性。

要配置用户搜索对象,该对象使用过滤器 (uid=<user-login-name>) 代替 DN 模式(或者除此之外),你需要配置以下 bean

<bean id="userSearch"
	class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg index="0" value=""/>
<constructor-arg index="1" value="(uid={0})"/>
<constructor-arg index="2" ref="contextSource" />
</bean>

并通过设置 BindAuthenticator bean 的 userSearch 属性来使用它。然后,认证器将调用搜索对象,以在试图绑定此用户之前获取正确的用户 DN。

30.4.6 LDAP 属性和自定义 UserDetails

使用 LdapAuthenticationProvider 进行认证的净结果与使用标准 UserDetailsService 接口的普通 Spring Security 认证相同。创建并存储在返回的 Authentication 对象中的 UserDetails 对象。与使用 UserDetailsService 一样,一个常见的要求是能够自定义这个实现并添加额外的属性。当使用 LDAP 时,这些通常是来自用户条目的属性。 UserDetails 对象的创建由提供程序的 UserDetailsContextMapper 策略控制,该策略负责将用户对象映射到 LDAP 上下文数据以及从 LDAP 上下文数据映射到用户对象:

public interface UserDetailsContextMapper {

UserDetails mapUserFromContext(DirContextOperations ctx, String username,
		Collection<GrantedAuthority> authorities);

void mapUserToContext(UserDetails user, DirContextAdapter ctx);
}

只有第一个方法与认证相关。如果提供此接口的实现并将其注入 LdapAuthenticationProvider,则可以精确控制 UserDetails 对象的创建方式。第一个参数是 Spring LDAP 的 DirContextOperations 的实例,它允许你访问在认证期间加载的 LDAP 属性。username 参数是用于身份验证的名称,最后一个参数是由配置的 LdapAuthoritiesPopulator 为用户加载的权限集合。

加载上下文数据的方式略有不同,这取决于你使用的认证类型。使用 BindAuthenticator,从绑定操作返回的上下文将用于读取属性,否则将使用从配置的 ContextSource 获得的标准上下文读取数据(当搜索被配置为定位用户时,这将是搜索对象)。

30.5 激活的目录认证

激活的目录支持自己的非标准身份验证选项,并且正常使用模式与标准 LdapAuthenticationProvider 不太匹配。通常使用域用户名(以 user@domain) 的形式)而不是使用 LDAP 专有名称执行认证。为了使这更容易,Spring Security 3.1 有一个为典型的激活的目录安装程序定制的认证提供者。

30.5.1 ActiveDirectoryLdapAuthenticationProvider

配置 ActiveDirectoryLdapAuthenticationProvider 非常简单。你只需要提供域名和 LDAP URL 来提供服务器的地址 [20]。一个示例配置会是这样的:

<bean id="adAuthenticationProvider"
class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
	<constructor-arg value="mydomain.com" />
	<constructor-arg value="ldap://adserver.mydomain.com/" />
</bean>
}

注意,不需要指定单独的 ContextSource 来定义服务器位置 - bean 是完全自包含的。例如,名为 "Sharon" 的用户能够通过输入用户名 sharon 或完整的激活目录用户 userPrincipalName(即 sharon@mydomain.com)进行认证。然后将定位用户的目录条目,并返回属性以供定制创建的 UserDetails 对象时使用(如上所述,为此可以注入 UserDetailsContextMapper)。所有与目录的交互都发生在用户自身的身份上。没有 "manager" 用户的概念。

默认情况下,用户权限是从用户条目的 memberOf 属性值中获取的。分配给用户的权限可以再次使用 UserDetailsContextMapper 定制。还可以向提供程序实例中注入 GrantedAuthoritiesMapper,以控制最终在 Authentication 对象中的权限。

激活目录错误代码

默认情况下,失败的结果将导致标准 Spring Security BadCredentialsException。如果将属性 convertSubErrorCodesToExceptions 设置为 true,则将解析异常消息,以尝试提取激活目录特定的错误代码并引发更具体的异常。查看类 Javadoc 以获取更多详细信息。



[19] 注意,这与使用 member={0} 的基础 DefaultLdapAuthoritiesPopulator 的默认配置不同。

[20] 还可以使用 DNS 查找来获得服务器的 IP 地址。目前不支持,但希望将来的版本支持。