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"
}

先決條件

在嘗試將 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 作為 classpath 資源,以使用兩個使用者 useradmin 初始化嵌入式 LDAP 伺服器,這兩個使用者的密碼均為 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://127.0.0.1:53389/dc=springframework,dc=org");
}
<ldap-server
	url="ldap://127.0.0.1:53389/dc=springframework,dc=org" />
fun contextSource(container: UnboundIdContainer): ContextSource {
    return DefaultSpringSecurityContextSource("ldap://127.0.0.1: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 “compare” 操作來完成,其中提供的密碼傳遞到伺服器進行比較,而永遠不會檢索到真實的密碼值。當密碼使用隨機鹽正確雜湊時,無法執行 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/")
}