《Spring Security3》第九章(LDAP)第二部分翻译(LDAP高级配置)
LDAP的高级配置
一旦我们要了解LDAP基础集成之外的知识,就会发现security XML命名空间方式的配置中,Spring Security LDAP模块还有许多的可用配置。它包括查询用户的个人信息、用户认证的其它方式以及使用LDAP作为UserDetailsService且与DaoAuthenticationProvider结合。
实例JBCP LDAP用户
在JBCP Pets LDIF文件中,我们提供了许多的用户。在高级配置练习和自学中,以下的快速查询表可能会对你有所帮助。要注意的是除了userwithphone以外,所有用户的密码均为password。
用户名 |
角色 |
密码编码 |
ldapguest |
ROLE_USER |
Plaintext |
anotherldapuser |
ROLE_USER |
Plaintext |
ldapadmin |
ROLE_USER和ROLE_ADMIN |
Plaintext |
shapassworduser |
ROLE_USER |
{sha} |
sshapassworduser |
ROLE_USER |
{ssha} |
userwithphone |
ROLE_USER |
Plaintext(在telephoneNumber属性中) |
我们将会在后面的章节中介绍为什么密码编码很重要。
密码对比与绑定认证
有一些LDAP服务器可能会配置成不允许特定的用户直接绑定到服务器上,这样的话匿名绑定(这是我们到此为止所使用的用户搜索办法)就被禁止了。这可能发生在很大规模的组织中,它想要限制能够从目录中读取信息的用户集合。在这种情况下,标准的Spring Securiry LDAP认证就行不通了,必须使用一种替代策略,通过o.s.s.ldap.authentication.PasswordComparisonAuthenticator实现(BindAuthenticator的兄弟类)。
PasswordComparisonAuthenticator绑定到LDAP上并查找匹配用户所提供用户名的DN。它接下来会比较用户提供的密码和匹配的LDAP条目中存储的userPassword属性。如果编码后的密码相匹配,用户认证成功,接下来的流程与BindAuthenticator相同。
配置基本的密码对比
配置密码对比认证来替换绑定认证很简单,只需在<ldap-authentication-provider>中添加一个子元素即可,如下:
<ldap-authentication-provider server-ref="ldapLocal" user-search-filter="(uid={0})" group-search-base="ou=Groups"> <password-compare/> </ldap-authentication-provider>
默认的PasswordComparisonAuthenticator使用LDAP密码编码算法SHA(回忆一下我们在第四章:凭证安全存储中讨论过的SHA-1密码加密算法)。在重启服务之后,你可以使用用户名shapassworduser和密码password尝试登录。
LDAP密码编码和存储
LDAP支持多种的密码加密算法,从简单文本到单向加密算法(类似于我们在第四章中了解到的基于数据库认证)。最常用的LDAP密码存储格式是SHA(SHA-1单向加密)和SSHA(使用salt值的单向加密算法)。很多的LDAP实现支持RFC 2307, An Approach for Using
LDAP as a Network Information Service (http://tools.ietf.org/html/rfc2307)定义的其它的密码格式。
RFC 2307的设计者在密码存储方面做了一项很高明的事情。从目录中的得到的密码当然是按照一定的算法(SHA等)进行加密的,但是它以使用的加密算法作为前缀。这使得LDAP服务器能够很容易支持多种密码编码算法。例如,一个SHA编码的密码在目录中存储如下:
{SHA}5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8 |
我们可以看到密码存储算法很清楚地进行了标明(使用{SHA}标识),并于密码一起存储。
SSHA是尝试联合使用SHA-1哈希算法和密码salting,以防止目录攻击。正如我们在第四章中所了解的那样,salt在计算hash之前添加到密码中。当经过hash的密码存储到目录中后,salt值也拼在hash后的密码上。密码以{SSHA}开头,这样LDAP服务器能够知道用户提供的密码需要以不同的方式进行对比。大多数的现代LDAP使用SSHA作为默认的密码存储算法。
密码对比认证的缺点
现在你了解了一些关于LDAP用户密码并建立了PasswordComparisonAuthenticator,这是请你想一下如果使用sshapassworduser用户以及SSHA格式存储的密码登录会发生什么?试一下——把书放在一边,尝试一下,然后回来。
你的登录被拒绝了,对不?可是你还能够使用SHA编码密码的用户登录——为什么?当我们使用绑定授权时,密码编码方式和存储并不会影响我们——你觉得这会是为什么呢?
绑定认证不会受到影响是因为LDAP服务器进行了认证和校验用户密码。在使用密码对比认证的时候,Spring Security LDAP负责将密码编码成目录期望的格式然后与目录进行对比进行认证校验。
为了安全,密码对比策略并不能真正从目录上读取(基于安全策略,读取目录的密码通常会被拒绝)。作为替代,PasswordComparisonAuthenticator会从用户的目录条目作为根节点进行一个LDAP查找,试图查找与Spring Security编码密码值匹配的密码属性值。所以,当我们以sshapassworduser登录时,PasswordComparisonAuthenticator使用配置的SHA算法对密码进行编码并试图进行简单查找,这个查找失败了,因为目录中的用户密码是以SSHA的格式存储的。
当我们将PasswordComparisonAuthenticator的hash算法改成使用SSHA会发生什么呢,如下:
<password-compare hash="{ssha}"/>
重新启动并尝试——它还是不能好用。让我们想一下可能为什么。记住SSHA使用的是salted密码,而salt值是与密码一起存储LDAP目录上的。但是PasswordComparisonAuthenticator编码并不能从LDAP中获取任何信息(这在不允许的绑定的公司中是违背安全策略的)。所以当PasswordComparisonAuthenticator计算hash密码时,它不能确定使用什么salt值。
总之,PasswordComparisonAuthenticator在特定的环境下很有用(要考虑目录本身的安全),但是它并不像直接绑定认证灵活。
配置UserDetailsContextMapper
正如我们在前面讲到的,一个o.s.s.ldap.userdetails.UserDetailsContextMapper实例用来匹配用户条目和内存中的UserDetails对象。默认UserDetailsContextMapper的行为与JdbcDaoImpl类似,都是将一定数量的细节信息填充到要返回的UserDetails中——也就是说,除了用户名和密码以外并没有太多的信息。
但是,LDAP目录可以包括除了用户名、密码和角色以外更多的用户细节信息。Spring Security提供了方法从两个标准的LDAP对象模式——person和inetOrgPerson获取更多的用户信息。
明确配置UserDetailsContextMapper
为了配置一个不同与默认实现的UserDetailsContextMapper,我们只需简单声明希望LdapAuthenticationProvider返回什么类型的LdapUserDetails类即可。security命名空间解析器能够根据要求的LdapUserDetails类型,足够智能地实例化正确的UserDetailsContextMapper。
让我们配置使用inetOrgPerson版本的匹配器。
<ldap-authentication-provider server-ref="ldapLocal" user-search-filter="(uid={0})" group-search-base="ou=Groups" user-details-class="inetOrgPerson">
如果你重启应用并尝试使用LDAP用户登录,你会发现没有任何变化。实际上,UserDetailsContextMapper在幕后已经发生了变化,在本例中会根据inetOrgPerson模式从用户目录条目中读取额外的细节信息。
查看其它的用户细节
作为这节的辅助功能,我们将在JBCP Pets的账号信息页面增加“View LDAP User Profile”区域。我们会用这个页面来阐述更丰富的person和inetOrgPerson LDAP模式能提供更多(可选)的信息给使用LDAP的应用。
添加以下逻辑到AccountController中:
@RequestMapping(value="/account/viewLdapUserProfile. do",method=RequestMethod.GET) public void showViewLdapUserProfilePage(ModelMap model) { final Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); model.addAttribute("user", principal); if(principal instanceof LdapUserDetailsImpl) { model.addAttribute("isLdapUserDetails", Boolean.TRUE); } if(principal instanceof Person) { model.addAttribute("isLdapPerson", Boolean.TRUE); } if(principal instanceof InetOrgPerson) { model.addAttribute("isLdapInetOrgPerson", Boolean.TRUE); } }
这个代码将会查询LdapAuthenticationProvider存储在Authentication对象中的UserDetails (principal),并确定是什么类型的LdapUserDetailsImpl。页面的代码则要根据不同的UserDetails类型显示绑定到用户认证信息上的细节,JSP代码如下。这个JSP应该放在WebContent/WEB-INF/views/account/viewLdapUserProfile.jsp。
<!-- Common Header and Footer Omitted --> <h1>View Profile</h1> <p> Some information about you, from LDAP: </p> <ul> <li><strong>Username:</strong> ${user.username}</li> <li><strong>DN:</strong> ${user.dn}</li> <c:if test="${isLdapPerson}"> <li><strong>Description:</strong> ${user.description}</li> <li><strong>Telephone:</strong> ${user.telephoneNumber}</li> <li><strong>Full Name(s):</strong> <c:forEach items="${user.cn}" var="cn"> ${cn}<br /> </c:forEach> </li> </c:if> <c:if test="${isLdapInetOrgPerson}"> <li><strong>Email:</strong> ${user.mail}</li> <li><strong>Street:</strong> ${user.street}</li> </c:if> </ul>
我们也可以在WebContent/WEB-INF/views/account/home.jsp加一个链接:
<li><a href="viewLdapUserProfile.do">View LDAP User Profile</a></li>
我们已经增加了另外的两个用户即shapasswordperson和shapasswordinetorgperson,你可以用它们来进行比较可用数据元素的不同。重启系统并查看“View LDAP User Profile”页面的三种不同类型的用户(非person,person和inetOrgPerson)。你会发现,当user-details-class被配置成inetOrgPerson时,尽管要返回的是o.s.s.ldap.userdetails.InetOrgPerson,但是其中的域可能被填充也可能没有填充,这取决与目录条目中可以得到的属性。
实际上,inetOrgPerson拥有很多的属性远超过我们这个简单页面所提到的。你可以查看所有属性的列表:RFC 2798, Definition of the inetOrgPerson LDAP Object Class (http://tools.ietf.org/html/rfc2798)。
你可能会意识到,没有办法支持条目中指明的而标准模式中没有的属性。标准的UserDetailsContextMappers不支持任意列表的属性,但是可以通过使用user-context-mapper-ref属性指向自定义的UserDetailsContextMapper引用。
使用可代替的密码属性
在很多场景中,可能会需要使用其它的LDAP属性而不是userPassword进行认证。这可能在公司已经部署完自定义的LDAP模式时发生或不需要进行比较严格的密码管理(可以预见的是,这不是一个好主意,但在现实世界中它就存在)。
PasswordComparisonAuthenticator也支持对用户的密码与一个其它的LDAP条目属性进行校验,而不是标准的userPassword属性。这很容易配置,我们可以提供一个很简单的例子,使用简单文本的telephoneNumber属性,如下:
<ldap-authentication-provider server-ref="ldapLocal" user-search-filter="(uid={0})" group-search-base="ou=Groups" user-details-class="inetOrgPerson"> <password-compare hash="plaintext" password-attribute="telephoneNumber"/> </ldap-authentication-provider>
我们可以重启服务并使用用户userwithphone和密码(即电话号码)1112223333进行登录。
当然,这种方式的认证具有我们前面讨论过的PasswordComparisonAuthenticator基础认证的所有风险。但是,万一在LDAP中使用它的时候要注意。
使用LDAP作为UserDetailsService
需要注意的一件事就是LDAP也可以用作UserDetailsService。要记住的是UserDetailsService在Spring Security中要启用很多其它的功能,包括remember me和OpenID认证功能。
配置LDAP作为UserDetailsService到LDAP AuthenticationProvider中是很简单的。就像JDBC UserDetailsService,一个LDAP UserDetailsService作为<http>的兄弟节点进行声明。
<ldap-user-service id="ldapUserService" server-ref="ldapLocal" user-search-filter="(uid={0})" group-search-base="ou=Groups"/>
在功能上,o.s.s.ldap.userdetails.LdapUserDetailsService的配置与LdapAuthenticationProvider的配置基本完全相同,除了没有尝试使用安全实体的用户名绑定LDAP。相反的,<ldap-server>应用提供的凭证信息用来进行用户的查找。
【如果你想自己使用LDAP认证用户,请避免使用user-details-service-ref(引用了一个LdapUserDetailsService)配置<authentication-provider>的常见错误。】
就像我们前面提到的那样,LdapUserDetailsService使用<ldap-server>提供的manager-dn来获取自己的信息——这意味着它不会尝试绑定用户到LDAP上,这可能与你期望的行为不一样。LdapUserDetailsService一般用来支持系统的其它组件,如OpenID登录或remember me中,在这里认证已经通过的但是用户的详细信息在认证过程中并没有提供。
注意使用LDAP UserDetailsService时的remember me
注意的是,如果此时重新启动服务器会失败,并有如下的错误信息:
More than one UserDetailsService registered. Please use a specific Id reference in <remember-me/> <openid-login/> or <x509 /> elements.
如果我们回忆一下在第三章:增强用户体验和第四章:凭证安全存储中对于remember me功能的介绍,我们就会记得remember me依赖UserDetailsService来查找remember me cookie中的用户名。不幸的是,AbstractRememberMeServices只可能查找一个UserDetailsService获取用户信息。所以,我们使用remember me功能时只能有一个配置的UserDetailsService而不能是两个。这使得使用相同的登录页同时支持LDAP认证和JDBC,并为用户提供remember me功能变得很难实现。调整<remember-me>配置引用某一个UserDetailsService(通过Spring Bean ID)足以明确告诉Spring Security你想做什么。
配置基于内存的remember me服务
在LDAP中另一个关于使用remember me要注意的是——为了给LDAP认证的用户提供remember me功能,你必须(通常会这样)使用基于JDBC持久化的token remember me服务。
你可能还记得在第三章中讨论的remember me cookie的组成,基于内存的remember me算法(在InMemoryTokenRepositoryImpl)依赖于从UserDetails得到的用户名和密码。在很多场景下,LDAP服务器配置成不允许读取userPassword属性(这就是为什么PasswordComparisonAuthenticator写成那个样子),所以LdapUserDetailsMapper很可能不能为UserDetails对象填充password属性。缺失这个关键的属性会导致创建remember me cookie失败,而且会潜在的影响cookie的安全。
避免这个问题也很简单——配置位于JDBC中的remember me cookie存储,它只依赖于用户名来校验cookie(我们在第四章已经讨论过)。
集成外部的LDAP服务器
很可能你在测试完与嵌入式LDAP服务器集成后就要与外部的LDAP 服务器交互了。幸运的是,这是简单,通过建立嵌入式LDAP 服务器的<ldap-server>指令的简单语法就能完成。
以下的代码就是一个连接在10389端口上的外部LDAP 服务器的示例配置:
<ldap-server url="ldap://localhost:10389/dc=jbcppets,dc=com" id="ldapLocal" manager-dn="uid=admin,ou=system" manager-password="secret"/>
这里的明显区别(除了LDAP URL)就是提供了用户的DN和密码。这个账号(实际上是可选的)应该允许绑定到目录上并且能够进行所有相关用户和组信息的查询。使用这些凭证对LDAP服务器URL的绑定结果就是原来进行安全系统其它的LDAP操作。
注意很多的LDAP服务器支持SSL加密的LDAP(LDAPS)——这无疑也是出于安全的考虑,Spring LDAP也支持。只需在LDAP服务器URL上使用ldaps://开始即可。LDAPS通常运行在636TCP端口。
注意有很多的商业或非商业的LDAP实现。用于连接、用户绑定、填充GrantedAuthoritys的具体配置完全取决于其提供者和目录的结构。在下一个章节中,我们会讲解一个很通用的LDAP实现,即Microsoft Active Directory。
推荐阅读
-
《Spring Security3》第四章第二部分翻译(JdbcDaoImpl的高级配置)
-
《Spring Security3》第六章第六部分翻译(Spring Security基于bean的高级配置)
-
《Spring Security3》第七章第二部分翻译(高级ACL)(上)
-
《Spring Security3》第七章第二部分翻译(高级ACL)(下)
-
《Spring Security3》第九章(LDAP)第二部分翻译(LDAP高级配置)
-
《Spring Security3》第九章(LDAP)第一部分翻译(LDAP基本配置)
-
《Spring Security3》第九章(LDAP)第三部分翻译(LDAP明确配置)
-
《Spring Security3》第十章(CAS)第二部分翻译(CAS高级配置)
-
《Spring Security3》第九章(LDAP)第一部分翻译(LDAP基本配置)
-
《Spring Security3》第七章第二部分翻译(高级ACL)(上)