LDAP 認證

LDAP(輕量級目錄訪問協議)常被組織用作使用者資訊的中央儲存庫和認證服務。它也可用於儲存應用程式使用者的角色資訊。

當 Spring Security 配置為接受使用者名稱/密碼進行認證時,會使用 Spring Security 基於 LDAP 的認證。然而,儘管使用了使用者名稱和密碼進行認證,它並不使用 UserDetailsService,因為在繫結認證中,LDAP 伺服器不返回密碼,因此應用程式無法執行密碼驗證。

LDAP 伺服器的配置方式有許多不同的場景,因此 Spring Security 的 LDAP 提供者是完全可配置的。它使用獨立的策略介面進行認證和角色檢索,並提供了預設實現,這些實現可以配置以處理各種情況。

所需的依賴項

要開始使用,請將 spring-security-ldap 依賴項新增到您的專案。使用 Spring Boot 時,請新增以下依賴項

Spring Security LDAP 依賴項
  • Maven

  • Gradle

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-ldap</artifactId>
</dependency>
depenendencies {
    implementation "org.springframework.boot:spring-boot-starter-data-ldap"
    implementation "org.springframework.security:spring-security-ldap"
}

先決條件

在使用 Spring Security 之前,您應該熟悉 LDAP。以下連結提供了相關概念的良好介紹,以及使用免費 LDAP 伺服器 OpenLDAP 設定目錄的指南:www.zytrax.com/books/ldap/。熟悉用於從 Java 訪問 LDAP 的 JNDI API 也會有所幫助。我們在 LDAP 提供者中不使用任何第三方 LDAP 庫(Mozilla、JLDAP 或其他),但廣泛使用了 Spring LDAP,因此如果您計劃新增自己的定製,熟悉該專案可能會有所幫助。

使用 LDAP 認證時,您應確保正確配置 LDAP 連線池。如果您對此不熟悉,請參閱Java LDAP 文件

設定嵌入式 LDAP 伺服器

您首先需要做的是確保有一個可以指向您配置的 LDAP 伺服器。為簡單起見,通常最好從嵌入式 LDAP 伺服器開始。Spring Security 支援使用以下任一選項:

在以下示例中,我們將 users.ldif 檔案暴露為類路徑資源,用於初始化嵌入式 LDAP 伺服器,其中包含兩個使用者:useradmin,它們的密碼均為 password

users.ldif
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: password

dn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: password

dn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user
member: uid=admin,ou=people,dc=springframework,dc=org
member: uid=user,ou=people,dc=springframework,dc=org

dn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin
member: uid=admin,ou=people,dc=springframework,dc=org

嵌入式 UnboundID 伺服器

如果您希望使用UnboundID,請指定以下依賴項

UnboundID 依賴項
  • Maven

  • Gradle

<dependency>
	<groupId>com.unboundid</groupId>
	<artifactId>unboundid-ldapsdk</artifactId>
	<version>6.0.11</version>
	<scope>runtime</scope>
</dependency>
depenendencies {
	runtimeOnly "com.unboundid:unboundid-ldapsdk:6.0.11"
}

然後,您可以使用 EmbeddedLdapServerContextSourceFactoryBean 配置嵌入式 LDAP 伺服器。這將指示 Spring Security 啟動一個記憶體中的 LDAP 伺服器

嵌入式 LDAP 伺服器配置
  • Java

  • Kotlin

@Bean
public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() {
	return EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer();
}
@Bean
fun contextSourceFactoryBean(): EmbeddedLdapServerContextSourceFactoryBean {
    return EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer()
}

或者,您可以手動配置嵌入式 LDAP 伺服器。如果您選擇這種方法,您將負責管理嵌入式 LDAP 伺服器的生命週期。

顯式嵌入式 LDAP 伺服器配置
  • Java

  • XML

  • Kotlin

@Bean
UnboundIdContainer ldapContainer() {
	return new UnboundIdContainer("dc=springframework,dc=org",
				"classpath:users.ldif");
}
<b:bean class="org.springframework.security.ldap.server.UnboundIdContainer"
	c:defaultPartitionSuffix="dc=springframework,dc=org"
	c:ldif="classpath:users.ldif"/>
@Bean
fun ldapContainer(): UnboundIdContainer {
    return UnboundIdContainer("dc=springframework,dc=org","classpath:users.ldif")
}

嵌入式 ApacheDS 伺服器

Spring Security 使用 ApacheDS 1.x,該版本已不再維護。遺憾的是,ApacheDS 2.x 僅釋出了里程碑版本,沒有穩定版本。一旦 ApacheDS 2.x 釋出穩定版本,我們將考慮進行更新。

如果您希望使用Apache DS,請指定以下依賴項

ApacheDS 依賴項
  • Maven

  • Gradle

<dependency>
	<groupId>org.apache.directory.server</groupId>
	<artifactId>apacheds-core</artifactId>
	<version>1.5.5</version>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>org.apache.directory.server</groupId>
	<artifactId>apacheds-server-jndi</artifactId>
	<version>1.5.5</version>
	<scope>runtime</scope>
</dependency>
depenendencies {
	runtimeOnly "org.apache.directory.server:apacheds-core:1.5.5"
	runtimeOnly "org.apache.directory.server:apacheds-server-jndi:1.5.5"
}

然後,您可以配置嵌入式 LDAP 伺服器

嵌入式 LDAP 伺服器配置
  • Java

  • XML

  • Kotlin

@Bean
ApacheDSContainer ldapContainer() {
	return new ApacheDSContainer("dc=springframework,dc=org",
				"classpath:users.ldif");
}
<b:bean class="org.springframework.security.ldap.server.ApacheDSContainer"
	c:defaultPartitionSuffix="dc=springframework,dc=org"
	c:ldif="classpath:users.ldif"/>
@Bean
fun ldapContainer(): ApacheDSContainer {
    return ApacheDSContainer("dc=springframework,dc=org", "classpath:users.ldif")
}

LDAP ContextSource

一旦您有了可以指向您配置的 LDAP 伺服器,就需要配置 Spring Security 指向用於認證使用者的 LDAP 伺服器。為此,建立一個 LDAP ContextSource(它等同於 JDBC 的 DataSource)。如果您已經配置了 EmbeddedLdapServerContextSourceFactoryBean,Spring Security 將建立一個指向嵌入式 LDAP 伺服器的 LDAP ContextSource

帶有嵌入式 LDAP 伺服器的 LDAP Context Source
  • Java

  • Kotlin

@Bean
public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() {
	EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean =
			EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer();
	contextSourceFactoryBean.setPort(0);
	return contextSourceFactoryBean;
}
@Bean
fun contextSourceFactoryBean(): EmbeddedLdapServerContextSourceFactoryBean {
    val contextSourceFactoryBean = EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer()
    contextSourceFactoryBean.setPort(0)
    return contextSourceFactoryBean
}

或者,您可以顯式配置 LDAP ContextSource 以連線到提供的 LDAP 伺服器

LDAP Context Source
  • Java

  • XML

  • Kotlin

ContextSource contextSource(UnboundIdContainer container) {
	return new DefaultSpringSecurityContextSource("ldap://:53389/dc=springframework,dc=org");
}
<ldap-server
	url="ldap://:53389/dc=springframework,dc=org" />
fun contextSource(container: UnboundIdContainer): ContextSource {
    return DefaultSpringSecurityContextSource("ldap://:53389/dc=springframework,dc=org")
}

認證

Spring Security 的 LDAP 支援不使用UserDetailsService,因為 LDAP 繫結認證不允許客戶端讀取密碼,甚至無法讀取密碼的雜湊版本。這意味著無法讀取密碼並由 Spring Security 進行認證。

因此,LDAP 支援是透過 LdapAuthenticator 介面實現的。LdapAuthenticator 介面還負責檢索任何必需的使用者屬性。這是因為屬性的許可權可能取決於正在使用的認證型別。例如,如果以使用者身份進行繫結,則可能需要以使用者自己的許可權讀取屬性。

Spring Security 提供了兩種 LdapAuthenticator 實現:

使用繫結認證

繫結認證是使用 LDAP 認證使用者最常見的機制。在繫結認證中,使用者的憑據(使用者名稱和密碼)被提交給 LDAP 伺服器進行認證。使用繫結認證的優勢在於使用者的機密資訊(密碼)無需暴露給客戶端,這有助於保護它們不被洩露。

以下示例展示了繫結認證配置

繫結認證
  • Java

  • XML

  • Kotlin

@Bean
AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) {
	LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource);
	factory.setUserDnPatterns("uid={0},ou=people");
	return factory.createAuthenticationManager();
}
<ldap-authentication-provider
	user-dn-pattern="uid={0},ou=people"/>
@Bean
fun authenticationManager(contextSource: BaseLdapPathContextSource): AuthenticationManager {
    val factory = LdapBindAuthenticationManagerFactory(contextSource)
    factory.setUserDnPatterns("uid={0},ou=people")
    return factory.createAuthenticationManager()
}

前面的簡單示例將透過在提供的模式中替換使用者登入名來獲取使用者的 DN,並嘗試使用登入密碼以該使用者身份進行繫結。如果所有使用者都儲存在目錄中的單個節點下,這是可以的。如果您希望配置 LDAP 搜尋過濾器來定位使用者,則可以使用以下方法:

使用搜索過濾器的繫結認證
  • Java

  • XML

  • Kotlin

@Bean
AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) {
	LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource);
	factory.setUserSearchFilter("(uid={0})");
	factory.setUserSearchBase("ou=people");
	return factory.createAuthenticationManager();
}
<ldap-authentication-provider
		user-search-filter="(uid={0})"
	user-search-base="ou=people"/>
@Bean
fun authenticationManager(contextSource: BaseLdapPathContextSource): AuthenticationManager {
    val factory = LdapBindAuthenticationManagerFactory(contextSource)
    factory.setUserSearchFilter("(uid={0})")
    factory.setUserSearchBase("ou=people")
    return factory.createAuthenticationManager()
}

如果與前面顯示的 ContextSource 定義一起使用,這將透過使用 (uid={0}) 作為過濾器,在 DN ou=people,dc=springframework,dc=org 下執行搜尋。同樣,使用者登入名被替換為過濾器名稱中的引數,因此它會搜尋 uid 屬性等於使用者名稱的條目。如果沒有提供使用者搜尋基礎,則從根部開始搜尋。

使用密碼認證

密碼比較是將使用者提供的密碼與儲存在倉庫中的密碼進行比較。這可以透過檢索密碼屬性的值並在本地檢查來完成,也可以透過執行 LDAP“比較”操作來完成,在該操作中,提供的密碼被傳遞給伺服器進行比較,而實際的密碼值永遠不會被檢索。當密碼使用隨機鹽值正確雜湊時,無法進行 LDAP 比較。

最小密碼比較配置
  • Java

  • XML

  • Kotlin

@Bean
AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) {
	LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory(
			contextSource, NoOpPasswordEncoder.getInstance());
	factory.setUserDnPatterns("uid={0},ou=people");
	return factory.createAuthenticationManager();
}
<ldap-authentication-provider
		user-dn-pattern="uid={0},ou=people">
	<password-compare />
</ldap-authentication-provider>
@Bean
fun authenticationManager(contextSource: BaseLdapPathContextSource?): AuthenticationManager? {
    val factory = LdapPasswordComparisonAuthenticationManagerFactory(
        contextSource, NoOpPasswordEncoder.getInstance()
    )
    factory.setUserDnPatterns("uid={0},ou=people")
    return factory.createAuthenticationManager()
}

以下示例展示了包含一些定製的更高階配置

密碼比較配置
  • Java

  • XML

  • Kotlin

@Bean
AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) {
	LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory(
			contextSource, new BCryptPasswordEncoder());
	factory.setUserDnPatterns("uid={0},ou=people");
	factory.setPasswordAttribute("pwd");  (1)
	return factory.createAuthenticationManager();
}
<ldap-authentication-provider
		user-dn-pattern="uid={0},ou=people">
	<password-compare password-attribute="pwd"> (1)
		<password-encoder ref="passwordEncoder" /> (2)
	</password-compare>
</ldap-authentication-provider>
<b:bean id="passwordEncoder"
	class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
@Bean
fun authenticationManager(contextSource: BaseLdapPathContextSource): AuthenticationManager {
    val factory = LdapPasswordComparisonAuthenticationManagerFactory(
        contextSource, BCryptPasswordEncoder()
    )
    factory.setUserDnPatterns("uid={0},ou=people")
    factory.setPasswordAttribute("pwd") (1)
    return factory.createAuthenticationManager()
}
1 將密碼屬性指定為 pwd

LdapAuthoritiesPopulator

Spring Security 的 LdapAuthoritiesPopulator 用於確定為使用者返回哪些許可權。以下示例展示瞭如何配置 LdapAuthoritiesPopulator

LdapAuthoritiesPopulator 配置
  • Java

  • XML

  • Kotlin

@Bean
LdapAuthoritiesPopulator authorities(BaseLdapPathContextSource contextSource) {
	String groupSearchBase = "";
	DefaultLdapAuthoritiesPopulator authorities =
		new DefaultLdapAuthoritiesPopulator(contextSource, groupSearchBase);
	authorities.setGroupSearchFilter("member={0}");
	return authorities;
}

@Bean
AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource, LdapAuthoritiesPopulator authorities) {
	LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource);
	factory.setUserDnPatterns("uid={0},ou=people");
	factory.setLdapAuthoritiesPopulator(authorities);
	return factory.createAuthenticationManager();
}
<ldap-authentication-provider
	user-dn-pattern="uid={0},ou=people"
	group-search-filter="member={0}"/>
@Bean
fun authorities(contextSource: BaseLdapPathContextSource): LdapAuthoritiesPopulator {
    val groupSearchBase = ""
    val authorities = DefaultLdapAuthoritiesPopulator(contextSource, groupSearchBase)
    authorities.setGroupSearchFilter("member={0}")
    return authorities
}

@Bean
fun authenticationManager(
    contextSource: BaseLdapPathContextSource,
    authorities: LdapAuthoritiesPopulator): AuthenticationManager {
    val factory = LdapBindAuthenticationManagerFactory(contextSource)
    factory.setUserDnPatterns("uid={0},ou=people")
    factory.setLdapAuthoritiesPopulator(authorities)
    return factory.createAuthenticationManager()
}

Active Directory

Active Directory 支援其自己的非標準認證選項,其正常使用模式與標準的 LdapAuthenticationProvider 不太契合。通常,認證是使用域使用者名稱(形式為 user@domain)而不是使用 LDAP 判別名(distinguished name)來執行的。為了簡化此過程,Spring Security 提供了一個專門為典型 Active Directory 設定定製的認證提供者。

配置 ActiveDirectoryLdapAuthenticationProvider 非常簡單。您只需提供域名和提供伺服器地址的 LDAP URL。

也可以透過 DNS 查詢來獲取伺服器的 IP 地址。目前尚不支援此功能,但希望未來的版本會支援。

以下示例配置 Active Directory

Active Directory 配置示例
  • Java

  • XML

  • Kotlin

@Bean
ActiveDirectoryLdapAuthenticationProvider authenticationProvider() {
	return new ActiveDirectoryLdapAuthenticationProvider("example.com", "ldap://company.example.com/");
}
<bean id="authenticationProvider"
        class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
	<constructor-arg value="example.com" />
	<constructor-arg value="ldap://company.example.com/" />
</bean>
@Bean
fun authenticationProvider(): ActiveDirectoryLdapAuthenticationProvider {
    return ActiveDirectoryLdapAuthenticationProvider("example.com", "ldap://company.example.com/")
}