vlambda博客
学习文章列表

读书笔记《spring-security-third-edition》LDAP 目录服务

LDAP 目录服务

在本章中,我们将回顾 轻量级目录访问协议 (LDAP),并了解如何将它集成到启用 Spring Security 的应用程序中以提供身份验证、授权和为感兴趣的成分提供用户信息服务。

在本章的课程中,我们将讨论以下主题:

  • Learning some of the basic concepts related to the LDAP protocol and server implementations
  • Configuring a self-contained LDAP server within Spring Security
  • Enabling LDAP authentication and authorization
  • Understanding the model behind LDAP search and user matching
  • Retrieving additional user details from standard LDAP structures
  • Differentiating between LDAP authentication methods and evaluating the pros and cons of each type
  • Explicitly configuring Spring Security LDAP using Spring bean declarations
  • Connecting to external LDAP directories
  • Exploring the built-in support for Microsoft AD
  • We will also explore how to customize Spring Security for more flexibility when dealing with custom AD deployments

了解 LDAP

LDAP 起源于 30 多年前的逻辑目录模型——在概念上类似于组织结构图和地址簿的组合。如今,LDAP 越来越多地被用作集中企业用户信息、将数千个用户划分为逻辑组以及允许在许多不同系统之间统一共享用户信息的一种方式。

出于安全目的,LDAP 非常常用来促进集中的用户名和密码身份验证——用户的凭据存储在 LDAP 目录中,并且可以代表用户对目录进行身份验证请求。这简化了管理员的管理,因为用户凭据(登录 ID、密码和其他详细信息)存储在 LDAP 目录中的单个位置。此外,组织信息(例如组或团队分配、地理位置和公司层次结构成员资格)是根据用户在目录中的位置定义的。

LDAP

此时,如果您以前从未使用过 LDAP,您可能想知道它是什么。我们将使用 Apache Directory Server 2.0.0-M231.5 示例目录中的屏幕来说明示例 LDAP 模式,如以下屏幕截图所示:

读书笔记《spring-security-third-edition》LDAP 目录服务

[email protected] 的特定用户条目开始(在前面的屏幕截图中突出显示),我们可以推断出 admin1 从树中的这个节点开始向上移动。我们可以看到用户 aeinstein 是组织单元 (ou=users) 的成员,该组织单元本身是域 example.com (前面截图中显示的缩写 dc 代表域组件)。在此之前是 LDAP 树本身的组织元素(DITRoot DSE),在 Spring Security 的上下文中与我们无关。用户 aeinstein 在 LDAP 层次结构中的位置具有语义和明确意义——您可以想象一个更复杂的层次结构,可以轻松地说明大型组织的组织和部门边界。

沿着树向下走到单个叶子节点形成的完整的自上而下的路径形成了一个由沿途所有中间节点组成的字符串,与 admin1 的节点路径一样,如下所示:

    uid=admin1,ou=users,dc=example,dc=com

前面的节点路径是唯一的,称为节点的可分辨名称 (DN)。专有名称类似于数据库主键,允许唯一标识节点并位于复杂的树结构中。我们将看到一个节点的 DN 在 Spring Security LDAP 集成的整个身份验证和搜索过程中被广泛使用。

请注意,在与 admin1 相同的组织级别上列出了其他几个用户。假定所有这些用户与 admin1 处于同一组织职位。虽然这个示例组织相对简单和扁平,但 LDAP 的结构是任意灵活的,可以进行多层次的嵌套和逻辑组织。

Spring Security LDAP 支持由 Spring LDAP 模块 (http://www.springsource.org/ldap ),这实际上是一个独立于核心 Spring Framework 和 Spring Security 项目的项目。它被认为是稳定的,并围绕标准 Java LDAP 功能提供了一组有用的包装器。

通用 LDAP 属性名称

树中的每个实际条目都由一个或多个对象类定义。对象类是组织的逻辑单元,对一组语义相关的属性进行分组。通过将树中的条目声明为特定对象类(例如人)的实例,LDAP 目录的组织者能够为目录的用户提供目录的每个元素代表什么的清晰指示。

LDAP 具有一组丰富的标准模式,涵盖可用的 LDAP 对象类及其适用属性(以及大量其他信息)。如果您计划使用 LDAP 进行大量工作,强烈建议您查看一个很好的参考指南,例如 Zytrax OpenLDAP 一书的附录 (http://www.zytrax.com/books/ldap/ape/),或Internet2 Consortium's Guide to Person-related架构http://middleware.internet2.edu/eduperson/)。

在上一节中,我们了解到 LDAP 树中的每个条目都有一个 DN,它在树中唯一地标识它。 DN 由一系列属性组成,其中一个(或多个)用于唯一标识 DN 表示的条目沿树向下的路径。由于 DN 描述的每个路径段都代表一个 LDAP 属性,因此您可以参考可用的、定义良好的 LDAP 模式和对象类来确定任何给定 DN 中每个属性的含义。

我们在下表中包含了一些常见属性及其含义。这些属性往往是组织属性——这意味着它们通常用于定义 LDAP 树的组织结构——并且在您可能在典型 LDAP 安装中看到的结构中从上到下排序:

属性名称

说明

示例

直流

域组件:通常是 LDAP 层次结构中组织的最高级别。

dc=jbcpcalendar,dc=com

c

国家/地区:某些 LDAP 层次结构是按国家/地区高层次构建的。

c=美国

o

组织名称: 是用于对LDAP资源进行分类的上级业务组织。

o=甲骨文公司

组织单位:是一个部门性的业务组织,一般在一个组织内。

ou=产品开发

cn

通用名称:这是对象的通用名称,或唯一的或人类可读的名称。对于人类,这通常是人的全名,而对于 LDAP 中的其他资源(计算机等),它通常是主机名。

cn=Super Visor

cn=吉姆·鲍勃

uid

User ID:虽然本质上不是组织性的,但 uid 属性通常是 Spring 在用户身份验证和搜索期间寻找的。

uid=svisor

用户密码

用户密码:此属性存储与此属性关联的 person 对象的密码。它通常使用 SHA 或类似的东西进行单向散列。

userPassword=纯文本

userPassword={SHA}cryptval

然而,上表中的属性确实倾向于组织目录树上的属性,因此可能会形成各种搜索表达式或映射,您将使用它们来配置 Spring Security 以与 LDAP 服务器交互。

请记住,有数百个标准 LDAP 属性——这些只是您在集成时可能会看到的一小部分 一个完全填充的 LDAP 服务器。

更新我们的依赖

我们已经包含了本章所需的所有依赖项,因此您无需对 build.gradle 文件进行任何更新。但是,如果您只是为自己的应用程序添加 LDAP 支持,则需要在 build.gradle 中添加 spring-security-ldap 作为依赖项,如下所示:

    //build.gradle

    dependencies {
    // LDAP:
    compile('org.springframework.boot:spring-boot-starter-data-ldap')
    compile("org.springframework.ldap:spring-ldap-core")
    compile("org.springframework.security:spring-security-ldap")
    compile("org.springframework:spring-tx")
    compile("com.unboundid:unboundid-ldapsdk")
       ...
    }
由于 Gradle 的工件解析问题, spring-tx 必须被拉入,否则 Gradle 将获取一个不起作用的旧版本。

如前所述,Spring Security 的 LDAP 支持建立在 Spring LDAP 之上。 Gradle 会自动将此依赖项作为传递依赖项引入,因此无需显式列出它。

如果您使用 ApacheDS 在 Web 应用程序中运行 LDAP 服务器,就像我们在日历应用程序中所做的那样,您需要添加对相关 ApacheDS JAR 的依赖项。无需对我们的示例应用程序进行这些更新,因为我们已经包含了它们。请注意,如果您连接到外部 LDAP 服务器,则不需要这些依赖项:

//build.gradle

    compile 'org.apache.directory.server:apacheds-core:2.0.0-M23'
    compile 'org.apache.directory.server:apacheds-protocol-ldap:2.0.0-M23'
    compile 'org.apache.directory.server:apacheds-protocol-shared:2.0.0
    -M23'

配置嵌入式 LDAP 集成

现在让我们启用 JBCP 日历应用程序以支持基于 LDAP 的身份验证。幸运的是,这是一个相对简单的练习,使用嵌入式 LDAP 服务器和示例 LDIF 文件。对于本练习,我们将使用为本书创建的 LDIF 文件,旨在捕捉 LDAP 和 Spring Security 的许多常见配置场景。我们已经包含了更多的示例 LDIF 文件,其中一些来自 Apache DS 2.0.0-M23,还有一个来自 Spring Security 单元测试,您也可以选择对其进行试验。

配置 LDAP 服务器引用

第一步是配置嵌入式 LDAP 服务器。 Spring Boot 会自动配置一个嵌入式 LDAP 服务器,但我们需要稍微调整一下配置。对您的 application.yml 文件进行以下更新:

      //src/main/resources/application.yml

      spring:
      ## LDAP
       ldap:
         embedded:

           ldif: classpath:/ldif/calendar.ldif
           base-dn: dc=jbcpcalendar,dc=com
           port: 33389
你应该从源头开始 chapter06.00-日历

我们正在从 classpath 加载 calendar.ldif 文件,并使用它来填充 LDAP 服务器。 root 属性使用指定的 DN 声明 LDAP 目录的根。这应该对应于我们正在使用的 LDIF 文件中的逻辑根 DN。

Be aware that for embedded LDAP servers, the base-dn attribute is required. If it is not specified or is specified incorrectly, you may receive several odd errors upon initialization of the Apache DS server. Also, be aware that the ldif resource should only load a single ldif, otherwise the server will fail to start up. Spring Security requires a single resource, since using something such as classpath*:calendar.ldif does not provide the deterministic ordering that is required.

稍后,当我们声明 LDAP 用户服务和其他配置元素时,我们将在 Spring Security 配置文件中重用此处定义的 bean ID。 <ldap-server> 声明中的所有其他属性在使用嵌入式 LDAP 模式时都是可选的。

启用 LDAP AuthenticationProviderNext 接口

接下来,我们需要配置另一个 AuthenticationProvider 接口,该接口根据 LDAP 提供程序检查用户凭据。只需更新 Spring Security 配置以使用 o.s.s.ldap.authentication.LdapAuthenticationProvider 引用,如下所示:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Override
    public void configure(AuthenticationManagerBuilder auth)
    throws Exception {
       auth
           .ldapAuthentication()
           .userSearchBase("")
           .userSearchFilter("(uid={0})")
           .groupSearchBase("ou=Groups")
           .groupSearchFilter("(uniqueMember={0})")
           .contextSource(contextSource())
           .passwordCompare()
           .passwordAttribute("userPassword");
    }
    @Bean
    public DefaultSpringSecurityContextSource contextSource() {
       return new DefaultSpringSecurityContextSource(
               Arrays.asList("ldap://localhost:33389/"),
               "dc=jbcpcalendar,dc=com");
    }

我们稍后将讨论这些属性。现在,让应用程序备份并运行,并尝试使用 [email protected] 作为用户名和 admin1 作为密码登录。您应该已登录!

您的源代码应如下所示 chapter05.01-日历

嵌入式 LDAP 故障排除

您很可能会遇到嵌入式 LDAP 难以调试的问题。 Apache DS 通常对其错误消息不太友好,在 Spring Security 嵌入模式下更是如此。如果您在尝试访问浏览器中的应用程序时遇到 404 错误,则很可能是事情没有正常启动。如果您无法运行这个简单的示例,需要仔细检查以下内容:

  • Ensure the baseDn attribute is set on the DefaultSpringSecurityContextSource declaration in your configuration file, and make sure it matches the root defined in the LDIF file that's loaded at startup. If you get errors referencing missing partitions, it's likely that either the root attribute was missed or doesn't match your LDIF file.
  • Be aware that a failure starting up the embedded LDAP server is not a fatal failure. In order to diagnose errors loading LDIF files, you will need to ensure that the appropriate log settings, including logging for the Apache DS server, are enabled, at least at error level. The LDIF loader is under the org.apache.directory.server.protocol.shared.store package, and this should be used to enable the logging of LDIF load errors.
  • If the application server shuts down non-gracefully, you may be required to delete some files in your temporary directory (%TEMP% on Windows systems or /tmp on Linux-based systems) in order to start the server again. The error messages regarding this are (fortunately) fairly clear. Unfortunately, embedded LDAP isn't as seamless and easy to use as the embedded H2 database, but it is still quite a bit easier than trying to download and configure many of the freely-available external LDAP servers.

一般来说,用于故障排除或访问 LDAP 服务器的出色工具是 Apache Directory Studio 项目,它提供独立和 Eclipse 插件版本。可在 http://directory.apache.org/studio/ 免费下载。如果您想继续阅读本书,您可能需要立即下载 Apache Directory Studio 2.0.0-M23。

了解 Spring LDAP 身份验证的工作原理

我们看到我们能够使用 LDAP 目录中定义的用户登录。但是当用户向 LDAP 中的用户发出登录请求时,究竟会发生什么呢? LDAP 身份验证过程包含以下三个基本步骤:

  1. Authenticate the credentials supplied by the user against the LDAP directory.
  2. Determine the GrantedAuthority object that the user has, based on their information in LDAP.
  3. Pre-load information from the LDAP entry for the user into a custom UserDetails object, for further use by the application.

验证用户凭据

对于第一步,针对 LDAP 目录的身份验证,将自定义身份验证提供程序连接到 AuthenticationManagero.s.s.ldap.authentication.LdapAuthenticationProvider 接口获取用户提供的凭据并根据 LDAP 目录验证它们,如下图所示:

读书笔记《spring-security-third-edition》LDAP 目录服务

我们可以看到 o.s.s.ldap.authentication.LdapAuthenticator 接口定义了一个委托,以允许提供者以可定制的方式发出身份验证请求。我们已经隐式配置到这一点的实现,o.s.s.ldap.authentication.BindAuthenticator,尝试使用用户的凭据绑定(登录)到 LDAP 服务器,就好像它是用户自己制作的一样一个连接。对于嵌入式服务器,这足以满足我们的身份验证需求;但是,外部 LDAP 服务器可能更严格,在这些服务器中,可能不允许用户绑定到 LDAP 目录。幸运的是,存在另一种身份验证方法,我们将在本章后面进行探讨。

如上图所示,请记住,搜索是在由 DefaultSpringSecurityContextSource 引用的 baseDn 属性中指定的凭据创建的 LDAP 上下文中执行的。对于嵌入式服务器,我们不使用此信息,但对于外部服务器引用,除非提供 baseDn,否则将使用匿名绑定。对于需要有效凭据来搜索 LDAP 目录的组织来说,保留对目录中信息的公开可用性的一些控制是非常常见的,因此,在实际场景中几乎总是需要 baseDnbaseDn 属性表示具有绑定目录和执行搜索的有效访问权限的用户的完整 DN。

使用 Apache Directory Studio 演示身份验证

我们将通过使用 Apache Directory Studio 1.5 连接到我们的嵌入式 LDAP 实例并执行 Spring Security 正在执行的相同步骤来演示身份验证过程是如何工作的。我们将在整个模拟过程中使用 [email protected]。这些步骤将有助于确保牢牢掌握幕后发生的事情,并在您难以确定正确配置的情况下提供帮助。

确保日历应用程序已启动并正常工作。接下来,启动 Apache Directory Studio 1.5 并关闭 Welcome 屏幕。

匿名绑定到 LDAP

第一步是匿名绑定到 LDAP。绑定是匿名完成的,因为我们没有在 DefaultSpringSecurityContextSource 对象上指定 baseDnpassword 属性。在 Apache Directory Studio 中,使用以下步骤创建连接:

  1. Click on File | New | LDAP Browser | LDAP Connection.
  2. Click on Next.

  1. Enter the following information, and then click on Next:
    • Connection name: calendar-anonymous
    • Hostname: localhost
    • Port: 33389
  2. We did not specify baseDn, so select No Authentication as the Authentication Method.
  3. Click on Finish.

您可以放心地忽略指示不存在默认架构信息的消息。您现在应该看到您已连接到嵌入式 LDAP 实例。

搜索用户

现在我们有了一个连接,我们可以使用它来查找我们希望绑定到的用户的 DN,方法是执行以下步骤:

  1. Right-click on DIT and select New | New Search.
  2. Enter a search base of dc=jbcpcalendar,dc=com. This corresponds to the baseDn attribute of our DefaultSpringSecurityContextSource object that we specified.
  3. Enter a filter of [email protected]. This corresponds to the value we specified for the userSearchFilter method of AuthenticationManagerBuilder. Note that we have included the parentheses and have substituted the username we are attempting to log in with with the {0} value.
  4. Click on Search.
  5. Click on the DN of the single result returned by our search. You can now see that our LDAP user is displayed. Note that this DN matches the value we searched for. Remember this DN, as it will be used in our next step.

以用户身份绑定到 LDAP

现在我们已经找到了用户的完整 DN,我们需要尝试以该用户身份绑定到 LDAP 以验证提交的密码。这些步骤与我们在匿名绑定中所做的相同,只是我们将指定我们正在验证的用户的凭据。

在 ApacheDS 中,使用以下步骤创建连接:

  1. Select File | New | LDAP Browser | LDAP Connection.
  2. Click on Next.
  3. Enter the following information and click on Next:
    • Connection name: calendar-user1
    • Hostname: localhost
    • Port: 33389
  4. Leave Authentication Method as Simple Authentication.
  5. Enter the DN from our search result as Bind DN. The value should be [email protected],ou=Users,dc=jbcpcalendar,dc=com.
  6. The Bind password should be the password that was submitted at the time of login. In our case, we want to use admin1 to successfully authenticate. If the wrong password was entered, we would fail to connect and Spring Security would report an error.
  7. Click on Finish.

当 Spring Security 能够与提供的用户名和密码成功绑定时(类似于我们创建连接的方式),Spring Security 将确定该用户的用户名和密码是否正确。然后 Spring Security 将继续确定用户的角色成员资格。

确定用户角色成员资格

在用户通过 LDAP 服务器成功认证后,接下来必须确定授权信息。授权由主体的角色列表定义,并确定通过 LDAP 身份验证的用户的角色成员资格,如下图所示:

读书笔记《spring-security-third-edition》LDAP 目录服务

我们可以看到,在根据 LDAP 对用户进行身份验证后,LdapAuthenticationProvider 委托给 LdapAuthoritiesPopulatorDefaultLdapAuthoritiesPopulator 接口将尝试在位于 LDAP 层次结构中另一个条目处或之下的属性中定位已验证用户的 DN。 groupSearchBase 方法中定义了搜索用户角色分配的位置的 DN;在我们的示例中,我们将其设置为 groupSearchBase("ou=Groups")。当用户的 DN 位于 groupSearchBase 的 DN 下方的 LDAP 条目中时,在其中找到其 DN 的条目上的属性用于将角色授予他们。

Spring Security 角色如何与 LDAP 用户关联可能有点令人困惑,所以让我们看看 JBCP 日历 LDAP 存储库,看看用户与角色的关联是如何工作的。 DefaultLdapAuthoritiesPopulator 接口使用 AuthenticationManagerBuilder 声明的几种方法来管理用户角色的搜索。这些属性大致按以下顺序使用:

  1. groupSearchBase: It defines the base DN under which the LDAP integration should look for one or more matches for the user's DN. The default value performs a search from the LDAP root, which may be expensive.
  2. groupSearchFilter: It defines the LDAP search filter used to match the user's DN to an attribute of an entry located under groupSearchBase. This search filter is parameterized with two parameters—the first ({0}) being the user's DN, and the second ({1}) being the user's username. The default value is uniqueMember={0}.
  3. groupRoleAttribute: It defines the attribute of the matching entries, which will be used to compose the user's GrantedAuthority object. The default value is cn.
  4. rolePrefix: It is the prefix that will be prepended to the value found in groupRoleAttribute, to make a Spring Security GrantedAuthority object. The default value is ROLE_.

这对于新开发人员来说可能有点抽象并且难以理解,因为它与我们迄今为止所看到的基于 JDBC 和 JPA 的 UserDetailsS​​ervice 实现的任何东西都非常不同。让我们继续浏览 JBCP 日历 LDAP 目录中的 [email protected] 用户的登录过程。

使用 Apache Directory Studio 确定角色

我们现在将尝试使用 Apache Directory Studio 确定我们用户的角色。使用我们之前创建的 calendar-user1 连接,执行以下步骤:

  1. Right-click on DIT and select New | New Search.
  2. Enter a search base of ou=Groups,dc=jbcpcalendar,dc=com. This corresponds to the baseDn attribute of the DefaultSpringSecurityContextSource object we specified, plus the groupSearchBase attribute we specified for the AuthenticationManagerBuilder object.
  3. Enter a filter of [email protected],ou=Users,dc=jbcpcalendar,dc=com. This corresponds to the default groupSearchFilter attribute of (uniqueMember={0}). Notice that we have substituted the full DN of the user we found in our previous exercise for the {0} value.
  4. Click on Search.
  5. You will observe that the User group is the only group returned in our search results. Click on the DN of the single result returned by our search. You can now see the User group displayed in Apache DS. Note that the group has a uniqueMember attribute with the full DN of our user and other users.

Spring Security 现在为每个结果创建 GrantedAuthority 对象,方法是将找到的组的名称强制为大写并将 ROLE_ 附加到组名。伪代码类似于以下代码片段:

    foreach group in groups:

    authority = ("ROLE_"+group).upperCase()

    grantedAuthority = new GrantedAuthority(authority)
Spring LDAP is as flexible as your gray matter. Keep in mind that, although this is one way to organize an LDAP directory to be compatible with Spring Security, typical usage scenarios are exactly the opposite—an LDAP directory already exists that Spring Security needs to be wired into. In many cases, you will be able to reconfigure Spring Security to deal with the hierarchy of the LDAP server; however, it's key that you plan effectively and understand how Spring works with LDAP when it's querying. Use your brain, map out the user search and group search, and come up with the most optimal plan you can think of—keep the scope of searches as minimal and as precise as possible.

您能否描述一下登录过程的结果对于我们的 [email protected] 用户有何不同?如果您在这一点上感到困惑,我们建议您稍作休息并尝试使用 Apache Directory Studio 来浏览嵌入式 LDAP 服务器,该服务器由应用程序的运行配置。如果您尝试按照前面描述的算法自己搜索目录,则可以更容易地掌握 Spring Security 的 LDAP 配置流程。

映射 UserDetails 的附加属性

最后,一旦 LDAP 查找为用户分配了一组 GrantedAuthority 对象,o.s.s.ldap.userdetails.LdapUserDetailsMapper 将咨询 o.s.s.ldap.userdetails.UserDetailsContextMapper 检索任何其他详细信息以填充 UserDetails 对象以供应用程序使用。

使用 AuthenticationManagerBuilder,到目前为止,我们已经配置了 LdapUserDetailsMapper 将用于使用从用户条目中收集的信息填充 UserDetails 对象LDAP 目录:

读书笔记《spring-security-third-edition》LDAP 目录服务

稍后我们将看到如何配置 UserDetailsContextMapper 以从标准 LDAP personinetOrgPerson 对象中提取大量信息。使用基线 LdapUserDetailsMapper,存储的只是 usernamepasswordGrantedAuthority

尽管 LDAP 用户身份验证和详细信息检索在幕后涉及更多机制,但您会注意到整个过程似乎有点类似于我们在 第 4 章基于 JDBC 的身份验证(对用户进行身份验证并填充 GrantedAuthority) 。与 JDBC 身份验证一样,可以执行 LDAP 集成的高级配置。让我们更深入地研究,看看有什么可能!

高级 LDAP 配置

一旦我们超越了 LDAP 集成的基础知识,Spring Security LDAP 模块中还有大量额外的配置功能,这些功能仍然在 security WebSecurityConfigurerAdapter 配置样式中。其中包括检索用户个人信息、用户身份验证的附加选项以及将 LDAP 用作 UserDetailsS​​ervice 接口以及标准 DaoAuthenticationProvider 类。

示例 JBCP LDAP 用户

我们在 JBCP 日历 LDIF 文件中提供了许多不同的用户。以下快速参考图表可以帮助您进行高级配置练习或自我探索:

用户名/密码

角色

密码编码

[email protected]/admin1

ROLE_ADMIN, ROLE_USER

纯文本

[email protected]/user1

ROLE_USER

纯文本

[email protected]/shauser

ROLE_USER

{sha}

[email protected]/sshauser

ROLE_USER

{ssha}

[email protected]/hasphone

ROLE_USER

纯文本(在 telephoneNumber 属性中)

我们将在下一节解释为什么密码编码很重要。

密码比较与绑定身份验证

某些 LDAP 服务器将被配置为不允许某些个人用户直接绑定到服务器,或者禁用匿名绑定(到目前为止,我们一直用于用户搜索)。这往往发生在希望一组受限用户能够从目录中读取信息的大型组织中。

在这些情况下,标准 Spring Security LDAP 身份验证策略将不起作用,必须使用替代策略,由 o.s.s.ldap.authentication.PasswordComparisonAuthenticatorBindAuthenticator 的兄弟
类)实现
):

读书笔记《spring-security-third-edition》LDAP 目录服务

PasswordComparisonAuthenticator 接口绑定到 LDAP 并搜索与用户提供的用户名匹配的 DN。然后它将用户提供的密码与匹配的 LDAP 条目中存储的 userPassword 属性进行比较。如果编码的密码匹配,则用户通过身份验证并且流程继续,就像 BindAuthenticator 一样。

配置基本密码比较

配置密码比较认证而不是绑定认证就像在 AuthenticationManagerBuilder 声明中添加一个方法一样简单。更新 SecurityConfig.java 文件,如下:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Override
    public void configure(AuthenticationManagerBuilder auth)
       throws Exception {
       auth
         .ldapAuthentication()
         .userSearchBase("")
         .userSearchFilter("(uid={0})")
         .groupSearchBase("ou=Groups")
         .groupSearchFilter("(uniqueMember={0})")
         .contextSource(contextSource())
         .passwordCompare()
           .passwordEncoder(new LdapShaPasswordEncoder())
           .passwordAttribute("userPassword");
    }

通过声明 passwordCompare 方法使用的 PasswordCompareConfigurer 类使用 PlaintextPasswordEncoder 进行密码编码。要使用SHA-1密码算法,我们需要设置密码编码器,我们可以使用o.s.s.a.encoding.LdapShaPasswordEncoder来支持SHA(回想一下,我们在 中广泛讨论了 SHA-1 密码算法第 4 章基于 JDBC 的身份验证)。

在我们的 calendar.ldif 文件中,我们将 password 字段设置为 userPasswordPasswordCompareConfigurer 类的默认 password 属性是 password。因此,我们还需要用 passwordAttribute 方法覆盖 password 属性。

重新启动服务器后,您可以尝试使用 [email protected] 作为 usernameshauser 作为 password

您的代码应如下所示 chapter06.02-日历

LDAP 密码编码和存储

LDAP 普遍支持各种密码编码算法,从明文到单向散列算法(类似于我们在前一章中探讨的那些)以及数据库支持的身份验证。 LDAP 密码最常见的存储格式是 SHASHA-1 单向哈希)和 SSHASHA-1 使用盐值进行单向散列)。许多 LDAP 实现通常支持的其他密码格式在 RFC 2307将 LDAP 用作网络信息服务的方法http://tools.ietf.org/html/rfc2307)。 RFC 2307 的设计者在密码存储方面做了一件非常聪明的事情。当然,保留在目录中的密码使用任何适当的算法(SHA 等)进行编码,但是,它们会以用于对密码进行编码的算法作为前缀。这使得 LDAP 服务器很容易支持多种密码编码算法。例如,SHA 编码密码以 {SHA}5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8 形式存储在目录中。

我们可以看到密码存储算法非常清楚地用{SHA}符号表示并与密码一起存储。

SSHA 表示法试图将强 SHA-1 哈希算法与密码加盐结合起来,以防止字典攻击。与我们在前一章中讨论过的密码加盐一样,加盐是在计算哈希之前添加到密码中的。当哈希密码存储在目录中时,盐值将附加到哈希密码。密码前面带有 {SSHA},以便 LDAP 目录知道用户提供的密码需要进行不同的比较。大多数现代 LDAP 服务器使用 SSHA 作为其默认密码存储算法。

密码比较验证器的缺点

现在您对 LDAP 如何使用密码有所了解,并且我们已经设置了 PasswordComparisonAuthenticator,您认为如果您使用我们的 [email protected] 登录会发生什么用户及其密码,以 SSHA 格式存储?

去吧,把书放在一边试试看,然后再回来。

你的登录被拒绝了,对吧?然而,您仍然能够以使用 SHA 编码密码的用户身份登录。为什么?当我们使用绑定身份验证时,密码编码和存储并不重要。你认为这是为什么?

绑定身份验证无关紧要的原因是 LDAP 服务器负责用户密码的身份验证和验证。使用密码比较身份验证,Spring Security LDAP 负责将密码编码为目录期望的格式,然后将其与目录匹配以验证身份验证。

出于安全考虑,密码比较认证实际上不能从目录中读取密码(读取目录密码通常会被安全策略拒绝)。相反,PasswordComparisonAuthenticator 执行 LDAP 搜索,以用户的目录条目为根,尝试匹配由 Spring Security 编码的密码确定的 password 属性和值。

因此,当我们尝试使用 [email protected] 登录时,PasswordComparisonAuthenticator 正在使用配置的 SHA 算法对密码进行编码并尝试执行一个简单的匹配失败,因为该用户的目录密码以 SSHA 格式存储。

我们目前的配置,使用LdapShaPasswordEncoder,已经支持SHASSHA,所以目前还是不行。让我们想想为什么会这样。请记住,SSHA 使用加盐密码,加盐值与密码一起存储在 LDAP 目录中。但是,PasswordComparisonAuthenticator 的编码使其无法从 LDAP 服务器读取任何内容(这通常违反了不允许绑定的公司的安全策略)。因此,当 PasswordComparisonAuthenticator 计算散列密码时,它无法确定要使用的盐值。

总之,PasswordComparisonAuthenticator 在目录本身的安全性是一个问题的某些有限情况下很有价值,但它永远不会像直接绑定身份验证那样灵活。

配置 UserDetailsContextMapper 对象

如前所述,o.s.s.ldap.userdetails.UserDetailsContextMapper 接口的一个实例用于将用户进入 LDAP 服务器的条目映射到内存中的 UserDetails 对象。默认的 UserDetailsContextMapper 对象的行为类似于 JpaDaoImpl,给定返回的 UserDetails 对象上填充的详细程度——也就是说,不是除了用户名和密码之外,还返回了很多信息。

但是,LDAP 目录可能包含比用户名、密码和角色更多的关于单个用户的详细信息。 Spring Security 附带了两种额外的方法,可以从两个标准 LDAP 对象模式中提取更多用户数据——personinetOrgPerson

UserDetailsContextMapper 的隐式配置

为了配置一个不同于默认的 UserDetailsContextMapper 实现,我们只需要声明我们希望 LdapAuthenticationProvider 返回哪个 LdapUserDetails 类。安全命名空间解析器将足够智能,可以根据请求的 LdapUserDetails 接口的类型实例化正确的 UserDetailsContextMapper 实现。

让我们重新配置我们的 SecurityConfig.java 文件以使用映射器的 inetOrgPerson 版本。更新 SecurityConfig.java 文件,如以下代码所示:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Override
    public void configure(AuthenticationManagerBuilder auth)
    throws Exception {
       auth
           .ldapAuthentication()
           .userSearchBase("")
           .userSearchFilter("(uid={0})")
           .groupSearchBase("ou=Groups")
           .groupSearchFilter("(uniqueMember={0})")
           .userDetailsContextMapper(
               new InetOrgPersonContextMapper())
           .contextSource(contextSource())
           .passwordCompare()
              // Supports {SHA} and {SSHA}
               .passwordEncoder(new LdapShaPasswordEncoder())
               .passwordAttribute("userPassword");
    }
如果我们删除 passwordEncoder 方法,那么使用 SHA 密码的 LDAP 用户将无法进行身份验证。

如果您要重新启动应用程序并尝试以 LDAP 用户身份登录,您会看到没有任何变化。事实上,UserDetailsContextMapper 已经在后台进行了更改,以在用户目录条目中提供 inetOrgPerson 模式的属性的情况下读取附加详细信息。

尝试使用 [email protected] 作为 usernameadmin1 作为 password 进行身份验证。它应该无法进行身份验证。

查看其他用户详细信息

为了在这方面为您提供帮助,我们将在 JBCP 日历应用程序中添加查看当前帐户的功能。我们将使用这个页面来说明更富有的人和 inetOrgPerson LDAP 模式如何为您的支持 LDAP 的应用程序提供额外的(可选的)信息。

您可能已经注意到本章附带了一个名为 AccountController 的附加控制器。可以看到相关代码,如下:

    //src/main/java/com/packtpub/springsecurity/web/controllers/AccountController.java

    ...
    @RequestMapping("/accounts/my")
    public String view(Model model) {
    Authentication authentication = SecurityContextHolder.
    getContext().getAuthentication();
    // null check on authentication omitted
    Object principal = authentication.getPrincipal();
    model.addAttribute("user", principal);
    model.addAttribute("isLdapUserDetails", principal instanceof
    LdapUserDetails);
    model.addAttribute("isLdapPerson", principal instanceof Person);
    model.addAttribute("isLdapInetOrgPerson", principal instanceof
    InetOrgPerson);
    return "accounts/show";
    }
    ...

上述代码会通过LdapAuthenticationProvider检索Authentication对象中存储的UserDetails对象(主体),判断LdapUserDetailsImplinterface 是的。然后页面代码本身将根据已绑定到用户身份验证信息的 UserDetails 对象的类型显示各种详细信息,如下面的 JSP 代码所示。我们也已经包含了 JSP:

    //src/main/resources/templates/accounts/show.html

    <dl>
       <dt>Username</dt>
       <dd id="username" th:text="${user.username}">ChuckNorris</dd>
       <dt>DN</dt>
       <dd id="dn" th:text="${user.dn}"></dd>
       <span th:if="${isLdapPerson}">
           <dt>Description</dt>
           <dd id="description" th:text="${user.description}"></dd>
           <dt>Telephone</dt>
           <dd id="telephoneNumber" th:text="${user.telephoneNumber}"></dd>
           <dt>Full Name(s)</dt>
           <span th:each="cn : ${user.cn}">
           <dd th:text="${cn}"></dd>
           </span>
       </span>
       <span th:if="${isLdapInetOrgPerson}">
           <dt>Email</dt>
           <dd id="email" th:text="${user.mail}"></dd>
           <dt>Street</dt>
           <dd id="street" th:text="${user.street}"></dd>
       </span>
    </dl>

唯一真正需要做的工作是在我们的 header.html 文件中添加一个链接,如以下代码片段所示:

    //src/main/resources/templates/fragments/header.html

    <li>
    <p class="navbar-text">Welcome &nbsp;
       <a id="navMyAccount" th:href="@{/accounts/my}">
         <div class="navbar-text" th:text="${#authentication.name}">
         User</div>
       </a>
    </p>
    </li>

我们添加了以下两个用户,您可以使用它们来检查可用数据元素的差异:

用户名

密码

类型

[email protected]

shainet

inetOrgPerson

[email protected]

人物

您的代码应如下所示 chapter05.03-日历

重新启动服务器并通过单击右上角的 用户名 检查每种用户类型的 Account Details 页面.您会注意到,当 UserDetails 类配置为使用 inetOrgPerson 时,虽然返回的是 o.s.s.ldap.userdetails.InetOrgPerson,但这些字段可能或者可能不会根据目录条目中的可用属性填充。

事实上,inetOrgPerson 有更多我们在这个简单页面上说明的属性。您可以查看 RFC 2798 中的完整列表,inetOrgPerson LDAP 对象类的定义 (http://tools.ietf.org/html/rfc2798)。

您可能注意到的一件事是,没有工具支持可能在 Object 条目上指定但不属于标准模式的附加属性。标准的 UserDetailsContextMapper 接口不支持任意的属性列表,但是可以通过使用 参考您自己的 UserDetailsContextMapper 接口来自定义它>userDetailsContextMapper 方法。

使用备用密码属性

在某些情况下,出于验证目的,可能需要使用备用 LDAP 属性而不是 userPassword。这可能发生在公司部署了自定义 LDAP 模式或不需要强密码管理的情况下(可以说,这绝不是一个好主意,但在现实世界中确实会发生)。

PasswordComparisonAuthenticator 接口还支持根据备用 LDAP 条目属性而不是标准 userPassword 属性验证用户密码的能力。这很容易配置,我们可以使用纯文本 telephoneNumber 属性演示一个简单的示例。更新SecurityConfig.java,如下:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Override
    public void configure(AuthenticationManagerBuilder auth)
    throws Exception {
       auth
         .ldapAuthentication()
         .userSearchBase("")
         .userSearchFilter("(uid={0})")
        .groupSearchBase("ou=Groups")
         .groupSearchFilter("(uniqueMember={0})")
         .userDetailsContextMapper(new InetOrgPersonContextMapper())
         .contextSource(contextSource())
         .passwordCompare()
            .passwordAttribute("telephoneNumber");
    }

我们可以重新启动服务器并尝试使用 [email protected] 作为 username0123456789 作为 password (电话号码)属性。

您的代码应如下所示 chapter05.04-日历

当然,这种类型的身份验证具有我们之前讨论过的所有基于 PasswordComparisonAuthenticator 的身份验证的风险;但是,最好在它附带 LDAP 实现的情况下意识到它。

使用 LDAP 作为 UserDetailsS​​ervice

需要注意的一点是 LDAP 也可以用作 UserDetailsS​​ervice。正如我们将在本书后面讨论的那样,需要 UserDetailsS​​ervice 来启用 Spring Security 基础设施中的各种其他功能,包括记住我和 OpenID 身份验证功能。

我们将修改我们的 AccountController 对象以使用 LdapUserDetailsS​​ervice 接口来获取用户。在执行此操作之前,请确保删除 passwordCompare 方法,如以下代码片段所示:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Override
    public void configure(AuthenticationManagerBuilder auth)
    throws Exception {
       auth
         .ldapAuthentication()
         .userSearchFilter("(uid={0})")
         .groupSearchBase("ou=Groups")
         .userDetailsContextMapper(new InetOrgPersonContextMapper())
         .contextSource(contextSource());
    }

配置 LdapUserDetailsS​​ervice

将 LDAP 配置为 UserDetailsS​​ervice 函数的 c配置与 LDAP AuthenticationProvider 的配置非常相似。与 JDBC UserDetailsS​​ervice 一样,LDAP UserDetailsS​​ervice 接口被配置为 <http> 声明的兄弟。对 SecurityConfig.java 文件进行以下更新:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
       return super.userDetailsService();
   }

在功能上,o.s.s.ldap.userdetails.LdapUserDetailsS​​ervice 的配置方式与 LdapAuthenticationProvider 几乎完全相同,只是没有尝试使用主体的用户名绑定到 LDAP .相反,DefaultSpringSecurityContextSource 提供的凭据引用自身,并用于执行用户查找。

不要犯非常常见的配置错误 AuthenticationManagerBuilderUserDetailsS​​erviceLdapUserDetailsS​​ervice 如果您打算针对 LDAP 本身对用户进行身份验证!如前所述, 由于安全原因, password 属性通常无法从 LDAP 中检索,这使得 UserDetailsS​​ervice 对身份验证无用。如前所述, LdapUserDetailsS​​ervice 使用 baseDn 属性随 DefaultSpringSecurityContextSource 声明以获取其信息——这意味着它不会尝试将用户绑定到 LDAP,因此可能不会像您预期的那样运行。

更新 AccountController 以使用 LdapUserDetailsS​​ervice

我们现在将更新 AccountController 对象以使用 LdapDetailsUserDetailsS​​ervice 接口来查找它显示的用户:

    //src/main/java/com/packtpub/springsecurity/web/controllers/AccountController.java

    @Controller
    public class AccountController {
    private final UserDetailsService userDetailsService;
    @Autowired
    public AccountController(UserDetailsService userDetailsService) {
       this.userDetailsService = userDetailsService;
    }
    @RequestMapping("/accounts/my")
    public String view(Model model) {
       Authentication authentication = SecurityContextHolder.
       getContext().getAuthentication();
       // null check omitted
       String principalName = authentication.getName();
       Object principal = userDetailsService.
       loadUserByUsername(principalName);
       ...
    }
    }

显然,这个例子有点傻,但是它演示了LdapUserDetailsS​​ervice的使用。继续并重新启动应用程序并尝试使用 username 作为 [email protected]password 作为 admin1< /kbd>。你能弄清楚如何修改控制器来显示任意用户的信息吗?

您能弄清楚应该如何修改安全设置以限制对管理员的访问吗?

您的代码应如下所示 chapter05.05-日历

将 Spring Security 与外部 LDAP 服务器集成

一旦您测试了与嵌入式 LDAP 服务器的基本集成,您可能会希望与外部 LDAP 服务器进行交互。幸运的是,这非常简单,可以使用稍微不同的语法以及我们提供的用于设置嵌入式 LDAP 服务器的相同 DefaultSpringSecurityContextSource 指令来完成。

更新 Spring Security 配置以连接到端口 33389 上的外部 LDAP 服务器,如下所示:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Override
    public void configure(AuthenticationManagerBuilder auth)
    throws Exception {
       auth
        .ldapAuthentication()
         .userSearchFilter("(uid={0})")
         .groupSearchBase("ou=Groups")
         .userDetailsContextMapper(new InetOrgPersonContextMapper())
         //.contextSource(contextSource())
         .contextSource()
             .managerDn("uid=admin,ou=system")
             .managerPassword("secret")
             .url("ldap://localhost:33389/dc=jbcpcalendar,dc=com");
    }

此处的显着差异(除了 LDAP URL)是提供了帐户的 DN 和密码。应该允许该帐户(实际上是可选的)绑定到目录并在所有相关 DN 中搜索用户和组信息。对 LDAP 服务器 URL 应用这些凭证所产生的绑定用于跨 LDAP 安全系统的其余 LDAP 操作。

请注意,许多 LDAP 服务器也支持 SSL 加密的 LDAP(LDAPS)——当然,出于安全目的,这是首选,并且受 Spring LDAP 堆栈支持。只需在 LDAP 服务器 URL 的开头使用 ldaps://。 LDAPS 通常在 TCP 端口 636 上运行。请注意,LDAP 有许多商业和非商业实现。您将用于连接性、用户绑定和 GrantedAuthoritys 人口的确切配置参数将完全取决于供应商和目录结构。我们将在下一节介绍一个非常常见的 LDAP 实现,即 Microsoft AD。

如果您手边没有 LDAP 服务器并想尝试一下,请继续将以下代码添加到您的 SecurityConfig.java 文件中,该文件将启动我们一直使用的嵌入式 LDAP 服务器:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Override
    public void configure(AuthenticationManagerBuilder auth)
    throws Exception {
       auth
         .ldapAuthentication()
         .userSearchBase("")
         .userSearchFilter("(uid={0})")
         .groupSearchBase("ou=Groups")
         .groupSearchFilter("(uniqueMember={0})")
         .userDetailsContextMapper(new InetOrgPersonContextMapper())
         .contextSource()
           .managerDn("uid=admin,ou=system")
           .managerPassword("secret")
           .url("ldap://localhost:10389/dc=jbcpcalendar,dc=com")
           .root("dc=jbcpcalendar,dc=com")
           .ldif("classpath:/ldif/calendar.ldif")
           .and()
               .passwordCompare()
                .passwordEncoder(new LdapShaPasswordEncoder())
                .passwordAttribute("userPassword")
       ;
    }

如果这不令人信服,请使用 Apache Directory Studio 启动 LDAP 服务器并将 calendar.ldif 导入其中。然后,您可以连接到外部 LDAP 服务器。继续并重新启动应用程序并尝试使用 username 作为 [email protected]password 作为 shauser< /kbd>。

您的代码应如下所示 chapter05.06-日历

显式 LDAP bean 配置

在本节中,我们将引导您完成显式配置与外部 LDAP 服务器的连接和支持针对外部服务器的身份验证所需的 LdapAuthenticationProvider 接口所需的 bean 配置集。与其他基于 bean 的显式配置一样,您确实希望避免这样做,除非您发现自己处于安全命名空间样式配置的功能不支持您的业务或技术要求的情况。在这种情况下,请继续阅读!

配置外部 LDAP 服务器引用

为了实现这个配置,我们假设我们有一个本地 LDAP 服务器在端口 10389 上运行,与上一节中提供的 DefaultSpringSecurityContextSource 接口示例对应的配置相同. SecurityConfig.java 文件中已经提供了所需的 bean 定义。事实上,为了简单起见,我们提供了整个 SecurityConfig.java 文件。查看以下代码片段中的 LDAP 服务器参考:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Bean
    public DefaultSpringSecurityContextSource contextSource() {return new    
    DefaultSpringSecurityContextSource(
       Arrays.asList("ldap://localhost:10389/"), 
       "dc=jbcpcalendar,dc=com"){{
          setUserDn("uid=admin,ou=system");
          setPassword("secret");
    }};
    }

接下来,我们需要配置 LdapAuthenticationProvider,这有点复杂。

配置 LdapAuthenticationProvider 接口

如果你已经阅读并理解了本章的解释,描述了 Spring Security LDAP 身份验证在幕后是如何工作的,那么这个 bean 配置将是完全可以理解的,尽管有点复杂。我们将为 LdapAuthenticationProvider 配置以下特征:

  • User credential binding authentication (not password comparison)
  • Use of InetOrgPerson in UserDetailsContextMapper

请看以下步骤:

  1. Let's get to it—we'll explore the already configured LdapAuthenticationProvider interface first, as follows:
        //src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

        @Bean
        public LdapAuthenticationProvider authenticationProvider 
        (BindAuthenticator ba,LdapAuthoritiesPopulator lap,
         \UserDetailsContextMapper cm){
            return new LdapAuthenticationProvider(ba, lap){{
              setUserDetailsContextMapper(cm);
           }};
        }
  1. The next bean provided for us is BindAuthenticator, and the supporting FilterBasedLdapUserSearch bean is used to locate the user's DN in the LDAP directory prior to binding, as follows:
        //src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

        @Bean
        public BindAuthenticator bindAuthenticator
        (FilterBasedLdapUserSearch userSearch)
        {
            return new BindAuthenticator(contextSource()){{
               setUserSearch(userSearch);
           }};
       }
        @Bean
        public FilterBasedLdapUserSearch filterBasedLdapUserSearch(){
           return new FilterBasedLdapUserSearch("", 
           //user-search-base "(uid={0})", //user-search-filter
           contextSource()); //ldapServer
        }

最后,LdapAuthoritiesPopulatorUserDetailsContextMapper 执行我们在本章前面讨论的角色:

            //src/main/java/com/packtpub/springsecurity/configuration/
            SecurityConfig.java

            @Bean
            public LdapAuthoritiesPopulator authoritiesPopulator(){
               return new DefaultLdapAuthoritiesPopulator(contextSource(),
               "ou=Groups"){{
                  setGroupSearchFilter("(uniqueMember={0})");
           }};
        }
        @Bean
        public userDetailsContextMapper userDetailsContextMapper(){
           return new InetOrgPersonContextMapper();
        }
  1. In the next step, we must update Spring Security to utilize our explicitly configured LdapAuthenticationProvider interface. Update the SecurityConfig.java file to use our new configuration, ensuring you remove the old ldapAuthentication method, as follows:
        //src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

        @Autowired
        private LdapAuthenticationProvider authenticationProvider;
        @Override
        public void configure(AuthenticationManagerBuilder auth)
        throws Exception {
           auth.authenticationProvider(authenticationProvider);
        }

至此,我们已经使用显式 Spring bean 表示法完全配置了 LDAP 身份验证。在 LDAP 集成中使用这种技术在少数情况下很有用,例如当安全名称空间不公开某些配置属性时,或者当需要自定义实现类来提供针对特定业务场景定制的功能时。我们将在本章稍后研究如何通过 LDAP 连接到 Microsoft AD 时探讨一个这样的场景。

  1. Go ahead and start the application and give the configuration a try with the username as [email protected] and the password as shauser. Assuming you have an external LDAP server running, or you have kept the configured in-memory DefaultSpringSecurityContextSource object, everything should still be working.
您的代码应如下所示 chapter05.07-日历

将角色发现委托给 UserDetailsS​​ervice

填充可用于显式 bean 配置的用户角色的一种技术是实现对在 UserDetailsS​​ervice 中通过用户名查找用户的支持,并从中获取 GrantedAuthority 对象资源。配置很简单,只需将 ldapAuthoritiesPopulator ID bean 替换为更新的 UserDetailsS​​erviceLdapAuthoritiesPopulator 对象,并引用 UserDetailsS​​ervice。对 SecurityConfig.java 文件进行以下更新,确保删除之前的 ldapAuthoritiesPopulator bean 定义:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    //@Bean
    //public LdapAuthoritiesPopulator authoritiesPopulator(){
        //return new DefaultLdapAuthoritiesPopulator(contextSource(),
       //"ou=Groups"){{
              //setGroupSearchFilter("(uniqueMember={0})");
        //   }};
      //}
    @Bean
    public LdapAuthoritiesPopulator authoritiesPopulator(
       UserDetailsService userDetailsService){ 
         return new UserDetailsServiceLdapAuthoritiesPopulator
         (userDetailsService);
    }

我们还需要确保我们已经定义了 userDetailsS​​ervice。为简单起见,在内存中添加一个 UserDetailsS​​ervice 接口,如下所示:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Bean
    @Override
    public UserDetailsManager userDetailsService() {
       InMemoryUserDetailsManager manager = new 
        InMemoryUserDetailsManager();
       manager.createUser(User.withUsername("[email protected]")
       .password("user1").roles("USER").build());
       manager.createUser(
           User.withUsername("[email protected]")
               .password("admin1").roles("USER", "ADMIN").build());
       return manager;
    }

您现在应该能够使用 [email protected] 作为 usernameadmin1 作为 password 进行身份验证。当然,我们也可以用内存中的 UserDetailsS​​ervice 接口替换我们在 第 4 章基于 JDBC 的身份验证,以及在 第 5 章使用 Spring Data 进行身份验证

您的代码应如下所示 chapter05.08-日历

您可能注意到的后勤和管理问题是用户名和角色必须同时在 LDAP 服务器和 UserDetailsS​​ervice 使用的存储库中进行管理——这对于大型用户群来说可能不是一个可扩展的模型.

此方案更常见的用途是需要 LDAP 身份验证以确保安全应用程序的用户是有效的公司用户,但应用程序本身想要存储授权信息。这将潜在的特定于应用程序的数据保留在 LDAP 目录之外,这可能是一个有益的关注点分离。

通过 LDAP 与 Microsoft Active Directory 集成

Microsoft AD 的便利功能之一不仅是它与基于 Microsoft Windows 的网络架构无缝集成,而且还可以配置为使用 LDAP 协议公开 AD 的内容。如果您在一家大量使用 Microsoft Windows 的公司工作,那么您所做的任何 LDAP 集成都可能会针对您的 AD 实例。

根据您对 Microsoft AD 的配置(以及目录管理员配置它以支持 Spring Security LDAP 的意愿),您可能会遇到困难,不是身份验证和绑定过程,而是 AD 信息到用户的 GrantedAuthority Spring Security 系统中的对象。

我们的 LDAP 浏览器中 JBCP 日历公司的示例 AD LDAP 树类似于以下屏幕截图:

读书笔记《spring-security-third-edition》LDAP 目录服务

您在这里看不到的是 ou=Groups,我们之前在示例 LDAP 结构中看到了它;这是因为 AD 将组成员身份作为属性存储在用户自己的 LDAP 条目上。

让我们使用我们最近获得的显式 bean 配置知识来编写一个 LdapAuthoritiesPopulator 实现,该实现从用户的 memberOf 属性中获取 GrantedAuthority。在以下部分中,您将找到本章示例代码中提供的 ActiveDirectoryLdapAuthoritiesPopulator.java 文件:

    //src/main/java/com/packtpub/springsecurity/ldap/userdetails/ad/
    ActiveDirectoryLdapAuthoritiesPopulator.java

    public final class ActiveDirectoryLdapAuthoritiesPopulator
    implements LdapAuthoritiesPopulator {
       public Collection<? extends GrantedAuthority>
         getGrantedAuthorities(DirContextOperations userData, String
          username) {
           String[] groups = userData.getStringAttributes("memberOf");
           List<GrantedAuthority> authorities = new 
            ArrayList<GrantedAuthority>();
         for (String group : groups) {
           LdapRdn authority = new DistinguishedName(group).removeLast();
           authorities.add(new SimpleGrantedAuthority
           (authority.getValue()));
       }
       return authorities;
    }
    }

现在,我们需要更改配置以支持我们的 AD 结构。假设我们从上一节中详述的 bean 配置开始,进行以下更新:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Bean
    public DefaultSpringSecurityContextSource contextSource() {
       return new DefaultSpringSecurityContextSource(Arrays.asList
       ("ldap://corp.jbcpcalendar.com/"), "dc=corp,dc=jbcpcalendar,
        dc=com"){{     
             setUserDn("CN=Administrator,CN=Users," +                  
             "DC=corp,DC=jbcpcalendar,DC=com");
             setPassword("admin123!");
       }};
    }
    @Bean
    public LdapAuthenticationProvider authenticationProvider(                                    
    BindAuthenticator ba, LdapAuthoritiesPopulator lap){
       // removed UserDetailsContextMapper
       return new LdapAuthenticationProvider(ba, lap);
    }
    @Bean
    public FilterBasedLdapUserSearch filterBasedLdapUserSearch(){
       return new FilterBasedLdapUserSearch("CN=Users", //user-search-base
       "(sAMAccountName={0})", //user-search-filter
       contextSource()); //ldapServer
    }
    @Bean
    public LdapAuthoritiesPopulator authoritiesPopulator(){
       return new ActiveDirectoryLdapAuthoritiesPopulator();
    }

如果您定义了它,您将需要删除 SecurityConfig.java 文件中的 UserDetailsS​​ervice 声明。最后,您需要从 AccountController 中删除对 UserDetailsS​​ervice 的引用。

sAMAccountName 属性是我们在标准 LDAP 条目中使用的 uid 属性的 AD 等效项。尽管大多数 AD LDAP 集成可能比此示例更复杂,但这应该为您提供一个起点,以探索您对 Spring Security LDAP 集成内部工作原理的概念性理解;即使支持复杂的集成也会容易得多。

If you want to run this sample, you will need an instance of AD up and running that matches the schema displayed in the screenshot. The alternative is to adjust the configuration to match your AD schema. A simple way to play around with AD is to install Active Directory Lightweight Directory Services, which can be found at http://www.microsoft.com/download/en/details.aspx?id=14683. Your code should look like chapter05.09-calendar.

Spring Security 4.2 中内置的 AD 支持

Spring Security 在 Spring Security 3.1 中添加了 AD 支持。事实上,上一节中的 ActiveDirectoryLdapAuthoritiesPopulator 类是基于新增的支持。为了利用 Spring Security 4.2 中的内置支持,我们可以将整个 SecurityConfig.java 文件替换为以下配置:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Bean
    public AuthenticationProvider authenticationProvider(){
       ActiveDirectoryLdapAuthenticationProvider ap = new 
       ActiveDirectoryLdapAuthenticationProvider("corp.jbcpcalendar.com",
       "ldap://corp.jbcpcalendar.com/");
       ap.setConvertSubErrorCodesToExceptions(true);
       return ap;
    }

当然,如果您要使用它,您需要确保将其连接到 AuthenticationManager。我们已经这样做了,但是可以在以下代码片段中找到有关配置外观的提示:

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

    @Autowired
    private AuthenticationProvider authenticationProvider;
    @Override
    public void configure(AuthenticationManagerBuilder auth)
    throws Exception {
       auth.authenticationProvider(authenticationProvider);
   }

提供的 ActiveDirectoryLdapAuthenticationProvider 类有几点需要注意,如下:

  • The users that need to be authenticated must be able to bind to AD (there is no manager user.
  • The default method for populating users' authorities is to search the users' memberOf attributes.
  • Users must contain an attribute named userPrincipalName, which is in the username@<domain> format. Here, <domain> is the first constructor argument to ActiveDirectoryLdapAuthenticationProvider. This is due to the fact that, after the bind occurs, this is how the context for the memberOf lookup is found.

由于现实世界中发生的复杂 LDAP 部署,内置支持很可能会为您提供如何与自定义 LDAP 模式集成的指南。

概括

我们已经看到,可以依赖 LDAP 服务器来提供身份验证和授权信息,以及在请求时提供丰富的用户配置文件信息。在本章中,我们介绍了 LDAP 术语和概念,以及 LDAP 目录通常如何组织以与 Spring Security 一起使用。我们还从 Spring Security 配置文件探索了独立(嵌入式)和外部 LDAP 服务器的配置。

我们涵盖了针对 LDAP 存储库的用户身份验证和授权,以及随后映射到 Spring Security 参与者。我们还看到了 LDAP 中身份验证方案、密码存储和安全机制的差异,以及 Spring Security 中如何处理它们。我们还学习了将 LDAP 目录中的用户详细信息属性映射到 UserDetails 对象,以便在 LDAP 和启用 Spring 的应用程序之间进行丰富的信息交换。我们还明确了 LDAP 的 bean 配置,以及这种方法的优缺点。

我们还介绍了与 AD 的集成。

在下一章中,我们将讨论 Spring Security 的 remember-me 功能,该功能允许用户的会话在关闭浏览器后安全地持续存在。