http://dev2dev.bea.com.cn/techdoc/webser/2005042001.html目录
1 J2EE Security 和 LDAP Security
2 JAAS和WebLogic Security Framework
3 了解WebLogic LDAP Authentication Provider
4 定制自己的Custom LDAP Authentication Provider
5 部署中的注意事项
6 结束语
7 参考资料
从WebLogic Server 7.0开始,WebLogic Server的安全机制有了全面的改变,实现了一个更加规范的基于JAAS的Security Framework,以及提供了一系列设计良好的Security Service Provider Interface。这样我们可以根据自己的具体需求,通过Custom Security Authentication Provider来实现安全上的定制功能。
本文将以WebLogic(WebLogic Server 8.1) Security和 LDAP为基础,介绍Custom LDAP Authentication Provider如何给我们带来更多的灵活性,和系统安全设计上更多的空间;以及讨论如何实现一个Custom LDAP Authentication Provider和部署过程中的一些良好经验。
由于本文涉及到的范围太广,不可能一一详细讨论;为了使没有相关基础的读者也能够阅读理解本文,因此我将在文章前半部分,试图通过最简洁扼要的描述,来使大家对于J2EE Security,WebLogic Security Framework以及LDAP 等有一个初步的清晰认识;进而可以开发出自己的LDAP Authentication Provider。因此很多地方做了比较有限的描述或者介绍,更多详细的内容可以参考文后附带的参考资料或者文中给出的链接。
1 J2EE Security 和 LDAP Security
Sun J2EE推出以来,其安全部分的规范就一直倍受关注。我们最常见到安全规范的两个方面分别是Servlet Security 和 EJB Security。目前绝大多数的Servlet容器,J2EE容器都能很好的支持这些安全规范。
WebLogic Server作为业界领先的J2EE服务器对J2EE Security的支持是非常优秀的。我们这里将结合WebLogic Security和使用越来越广泛的LDAP做一个简要的介绍,这些是设计开发Custom LDAP Authentication Provider的技术基础。
1.1 Authentication 和Authorization
这里需要大家先明确安全上的两个重要名词:一个是认证(Authentication),一个是授权(Authorization)。认证是回答这个人是谁的问题,即完成用户名和密码的匹配校验;授权是回答这个人能做什么的问题。我们讨论的J2EE Security包括Declarative Authorization和Programmatic Authorization,即一个是通过web.xml,ejb-jar.xml等部署描述符中的安全声明通过容器提供的服务来完成权限控制的;一个是通过HttpServletRequest.isUserInRole()和EJBContext.isCallerInRole()这样的编程接口在应用中自己完成权限控制的。
1.2 资源(Resource)和Security Role
资源原本只包括 Web Resource和EJB Resource,但在WebLogic Security中扩展到几乎任何一个WebLogic Platform中的资源,具体可以参考http://e-docs.bea.com/wls/docs81/secwlres/types.html#1213777。授权就是针对资源的访问控制。
J2EE Security是基于Security Role的。我们可以将一组资源与一个Security Role进行关联来达到控制的目的——只有拥有该Role权限的用户才能够访问这些资源。简单的说,我们可以通过给用户分配不同的Security Role来完成权限的控制。复杂的情况下包括用户/用户组,以及Principal和Role的映射关系等等。下面是一个声明性安全在web application(war包中WEB-INF/web.xml)中的示例:
<web-app> <security-constraint> <web-resource-collection> <web-resource-name>Success</web-resource-name> <url-pattern>/welcome.jsp</url-pattern> <http-method>GET</http-method> <http-method>POST</http-method> </web-resource-collection> <auth-constraint> <role-name>webuser</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>BASIC</auth-method> <realm-name>default</realm-name> </login-config> <security-role> <role-name>webuser</role-name> </security-role> </web-app> |
只有拥有角色webuser的用户才能够访问welcome.jsp页面,否则容器会返回401无权访问的错误。更多信息请参考http://e-docs.bea.com/wls/docs81/security/index.html。
同时我们需要在weblogic.xml(war包中WEB-INF/weblogic.xml)中对security role和principal进行映射关系的配置:
<weblogic-web-app> <security-role-assignment> <role-name>PayrollAdmin</role-name> <principal-name>Tanya</principal-name> </security-role-assignment> </weblogic-web-app> |
这样拥有Principal “Tanya”的用户(Principal将封装到Subject中,用户将和Subject关联)将会拥有PayrollAdmin的权限。
注意:一般情况下为了简化设计,本文中将假设security role即是principal name(如果不配置security-role-assignment,WebLogic会默认做此假设)。即上例中Principal-name也为PayrollAdmin。
1.3 LDAP Security
LDAP是轻量级目录服务(Lightweight Directory Access Protocol)。越来越多的应用开始采用LDAP作为后端用户存储。在安全上,LDAP Security是基于ACL(Access Control List)的,它通过给一个用户组分配LDAP 操作资源(比如对一个子树的查询,修改等)来最终完成权限的控制。因此在LDAP中,授权工作是以用户组为单位进行的。一个用户组一般来说是拥有如下一组属性的LDAP Entry:
图1-3-1
其中objectclass可以为groupOfUniqueNames或者groupOfNames,它们对应的组成员属性分别是uniquemember和member。如果是动态组,objectclass为groupOfURLs。动态组一般应用在成员可以通过某种业务逻辑运算来决定的情况下。比如,经理为ZHANGSAN的全部员工。下面是一个典型的动态组,memberURL属性定义了哪些entry属于该组:
图1-3-2
从图1-3-1中我们可以看出,用户WANTXIAOMING,ZHANGSAN,LISI属于组HR Managers。这种组和成员的关系是通过属性uniquemember来决定的。同时LADP Group 支持嵌套,即一个组可以是另外一个组的成员,比如我们将Accounting Managers组分配给HR Managers组作为其成员:
图1-3-3
这样将表示Accounting Managers中的成员,同时也是组HR Managers的成员。通过这种层级关系可以使权限分配变的更加灵活。
下面是一些名词的解释,希望大家对LDAP有更好的理解:
a) Objectclass —— LDAP对象类,抽象上的概念类似与一般我们理解的class。根据不同的objectclass,我们可以判断这个entry是否属于某一个类型。比如我们需要找出LDAP中的全部用户:(objectclass=person)再比如我们需要查询全部的LDAP组:(objectclass=groupOfUniqueNames)
b) Entry —— entry可以被称为条目,或者节点,是LDAP中一个基本的存储单元;可以被看作是一个DN和一组属性的集合。 属性可以定义为多值或者单值。
c) DN —— Distinguished Name,LDAP中entry的唯一辨别名,一般有如下的形式:uid=ZHANGSAN, ou=staff, ou=people, o=examples。LDAP中的entry只有DN是由LDAP Server来保证唯一的。
d) LDAP Search filter ——使用filter对LDAP进行搜索。 Filter一般由 (attribute=value) 这样的单元组成,比如:(&(uid=ZHANGSAN)(objectclass=person)) 表示搜索用户中,uid为ZHANGSAN的LDAP Entry.再比如:(&(|(uid= ZHANGSAN)(uid=LISI))(objectclass=person)),表示搜索uid为ZHANGSAN, 或者LISI的用户;也可以使用*来表示任意一个值, 比如(uid=ZHANG*SAN),搜索uid值以 ZHANG开头SAN结尾的Entry。更进一步,根据不同的LDAP属性匹配规则,可以有如下的Filter: (&(createtimestamp>=20050301000000)(createtimestamp<=20050302000000)),表示搜索创建时间在20050301000000和20050302000000之间的entry。
Filter中 “&” 表示“与”;“!”表示“非”;“|”表示“或”。根据不同的匹配规则,我们可以使用“=”,“~=”,“>=”以及“<=”,更多关于LDAP Filter读者可以参考LDAP相关协议:http://www.ietf.org/rfc/rfc2254.txt。
e) Base DN —— 执行LDAP Search时一般要指定basedn,由于LDAP是树状数据结构,指定basedn后,搜索将从BaseDN开始,我们可以指定Search Scope为:只搜索basedn(base),basedn直接下级(one level),和basedn全部下级(sub tree level)。
下面是一个典型的LDAP Tree结构,右侧显示Entry uid=ZHANGSAN, ou=staff, ou=people, o=examples的属性,该entry代表了一个名字叫张三的用户:
图1-3-4
2 JAAS和WebLogic Security Framework
现在越来越多的人开始了解JAAS,使用JAAS。WebLogic Security Framework就是基于JAAS的。因此我们需要对此有一个非常准确的理解才能够设计开发Custom Authentication Provider。
下面我们从几个名词入手,了解JAAS和 WebLogic Security Framework的关键之处:
2.1 Principal,Subject和LoginModule
a) Principal
当用户成功验证后,系统将会生成与该用户关联的各种Principal。我们这里将Principal进行简化的设计,认为一个Principal就是用户登录帐号和它所属于的组(LDAP Group)。这样当用户登录成功后,我们将会在LDAP中执行搜索,找出用户属于哪些组,并将这些组的名字,或者其标识作为Principal返回。这样,当用户在LDAP中属于某一个组,并且这个组的名字对应到 web.xml (或者ejb-jar.xml)中的Security role,那么这个用户就可以看作拥有访问这个Security Role定义的资源的权限。
在WebLogic Security Framework中,这个LDAP Group的名字(Principal)和Security Role的映射关系,可以通过一个 Role Mapping Provider来实现动态的匹配,即用户的动态权限控制。比如在运行时根据某一个业务逻辑来决定用户是否拥有某一个权限。关于Role Mapping Provider,读者可以参考下面链接的内容:http://e-docs.bea.com/wls/docs81/dvspisec/rm.html#1145542。
b) Subject
JAAS规定由Subject封装用户以及用户认证信息,其中包括Principals。下面是WebLogic Security Framework中Subject的组成图示:
图2-1-1
这样当用户试图访问一个受限的J2EE资源时,比如一个web URL,或者一个 EJB Method(可以在web.xml或者ejb-jar.xml中定义,由Security Role控制),WebLogic Security Framework将会通过 Authorization Provider检查用户当前的Subject中是否包含有是否可以访问受限资源的Principals。由于Principals将和J2EE Security Role在weblogic.xml中定义一个映射关系(或者通过其他业务逻辑来确定这种关系),因此通过这样的关系,可以最终知道用户是否有某一个J2EE Resource的访问权限。
c) LoginModule
JAAS LoginModule是一个Authentication Provider必须的组成部分。LoginModule是认证的核心引擎,它负责对用户身份进行验证,同时将返回与用户关联的Principals(用户登录帐号,以及LDAP Groups),然后放入Subject中,供后续的访问控制使用。
我们将在LoginModules中完成LDAP的相关认证,查询操作,将用户在LDAP中所属于的组搜索出来,作为认证后的结果封装到Subject中返回。
2.2 WebLogic Authentication认证过程
下面我们了解一下WebLogic的认证过程。以下图片来自http://e-docs.bea.com/wls/docs81/dvspisec/atn.html 我将其中主要部分进行说明。
图2-2-1
Security Framework在WebLogic Server启动时初始化Authentication Provider(5)。当有认证请求进入时,Security Framework首先将通过AuthenticationProvider.getLoginModuleConfiguration()来获取一个AppConfigurationEntry对象。通过AppConfigurationEntry(详见http://java.sun.com/security/jaas/apidoc/javax/security/auth/login/AppConfigurationEntry.html )可以初始化一个LoginModule。初始化LoginModule的方法为:public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options),可以看到里面有Subject, CallbackHandler等等重要参数。
被实例化的JAAS LoginModule将完成用户的一系列验证任务。验证完成后,在(6a)中principals将被Principal Validation Provider签名,在( 6b)中存放到与用户关联的Subject里面。Security Framework最后将拿着该用户的Subject去完成其他权限控制等任务。
3 了解WebLogic LDAP Authentication Provider
现在我们有信心了解一下WebLogic LDAP Authentication Provider的工作原理了。这里将以WebLogic提供的iPlanet Authentication Provider的配置为例进行说明。在这里也需要明确说明一下,为了方便进行描述,我们将实际属于LoginModule的行为也一并归结到Provider中。没有单独将两个的行为分开,目的是为了突出整个完整的过程。
3.1 iPlanet Authentication Provider配置
图3-1-1
从上图可以看出我们需要指定LDAP服务器的地址,端口,连接LDAP使用的Principal(不同于前面讨论的Principal,这个Principal实际是一个连接LDAP的用户,也就是一个LDAP 中用户Entry的 DN,它必须要有相关的LDAP 搜索等权限)和Credential(一般来说就是口令)。
再看下面关于Users的配置:
图3-1-2
3.1.1 User Object Class —— 前面已经对objectclass进行过说明,指明LDAP Entry属于哪一类
3.1.2 User Name Attribute —— 用户登录帐号在LDAP Entry中的属性,一般为UID或者cn
3.1.3 User Base DN —— 所有的用户将会放置到这个子树下面,因此在Provider中对用户进行的搜索将会从这个basedn开始
3.1.4 User Search Scope —— 指定搜索范围为Basedn的直接一级或者全部下级
3.1.5 User From Name Filter —— 使用这个filter可以搜索出用户信息。其中%u将会被用户输入的登录帐号替换,从而查询中LDAP中的用户信息
图3-1-3
3.1.6 Group Base DN —— 从该Base DN开始搜索用户组
3.1.7 Group From Name Filter —— %g将会被组的名字替换,通过该filter可以搜索出符合条件的LDAP Group
3.1.8 Static group name attribute —— 组名字的属性,属性cn对应的值就是组的名字
图3-1-4
3.1.9 Static Member DN Attribute —— 静态成员属性,通过该属性可以判断一个Entry是否属于一个组
3.1.10 Static Group DNs From Member DN Filter —— 通过该filter可以找出用户属于哪些组
3.1.11 Dynamic Group —— 动态组是在运行时根据某种业务逻辑,来决定成员隶属关系的LDAP Group
3.2 iPlanet Authentication Provider的工作原理
从上面配置的介绍中可以看出,后端存储为LDAP的情况下,在Console中我们需要配置的参数已经清楚的表明它所要完成工作的内容。
首先,它使用配置的User BaseDN和Filter,来根据用户输入的登录帐号进行搜索,找出存放在LDAP中的用户Entry。如果找到一个用户,那么Provider就使用该用户的DN和用户输入的口令,进行验证。验证可以使用LDAP Bind和LDAP Compare,这需要根据不同LDAP的特点来进行选择。
A. LDAP Bind —— 将当前的LDAP Connection绑定到一个用户身份上。这样后续的使用该Connection的LDAP Operation都将以该身份进行。LDAP Bind需要两个重要参数,一个是用户Entry的DN,一个是该用户的口令。
B. LDAP Compare —— LDAP Compare是一个为兼容X.500的古老操作,它用于检查一个属性值是否包含在指定Entry中的属性里。这样我们可以在知道用户password存放在哪个属性的前提下,对该属性进行compare。如果成功,表明口令正确。如果属性值为散列后的口令(绝大多数情况),有的LDAP Server支持这样的验证,有的不支持,比如iPlanet LDAP Server 5。
验证成功后,Provider将使用Console中关于Groups和Memberships中的配置,查找用户属于哪些LDAP Group,而且由于这些组本身可能会有一些嵌套,因此对于搜索到的组还需要进行查询。即使用filter: (&(uniquemember=uid=ZHANGSAN,ou=staff,ou=people,o=examples)(objectclass=groupOfUniqueNames))从Group Base DN开始搜索,将返回用户所属的第一层次Group;然后对于这些返回的组DN,仍然需要使用上面的Filter进行搜索(uniquemember值替换为组的DN),找出嵌套关系,直到查询完成没有组嵌套为止(此处需要防止陷入嵌套的循环中,比如Group A 包含了Group B; Group B又包含了Group A,有的LDAP Server可以自动检测出,有的需要我们程序来判断)。
然后将用户登录的帐号,用户所属组的名字(属性CN的值或其他),放入Subject中。最后调用Principal Validator Provider,对Subject中的principals进行签名,来表明该Subject是这个Provider生成的。这样防止其他攻击者伪造Subject以及Principal进行欺骗。此处也可以解释为何在不同的WLS Domain间不能够传递Subject,我们可以通过设置域信任来完成这种Subject的传递。设置域信任使用的Credential就是签名用的Key。
图3-1-5表明了如何设置WebLogic Domain Credential,默认情况下WebLogic Server会在启动的时候随即生成一个Credentials(在WLS6.1时,这个值就是system用户的口令):
可以想见如果我们实现自己的Principal Validator Provider,让它去一个集中的验证服务器中对Subject进行签名,或者验证Subject,这样就可以实现域信任,进而完成Application(EJB Tier)层的SSO。
通过以上的讨论,我们对于实现自己的LDAP Authentication Provider是不是又增加了一份信心?
4 定制自己的Custom LDAP Authentication Provider
为何要定制自己的Authentication Provider? 由于WebLogic Server已经提供了很多默认的Authentication Provider在一般情况下我们确实没有必要实现自己的Provider。但是面对某些针对安全方面的复杂需求时,WebLogic Server提供的Provider很有可能不满足这些需求,此时就需要我们定制自己的Provider。
在这一章的开头部分中我需要简要讨论关于WebLogic MBean Types,以及WebLogic Console扩展等内容,目的在于让读者了解到我们通过WebLogic Console可以完成对Custom Security Provider的配置和部署,我将以WebLogic 提供的Sample Security Provider为示例进行说明。详细的信息可以参考以下的一些资源:
http://e-docs.bea.com/wls/docs81/dvspisec/atn.html#1106272
http://e-docs.bea.com/wls/docs81/dvspisec/atn.html#1106241
上面两个链接描述了如何创建MBean Types以及在控制台上配置Custom Authentication Provider.下面这个链接中专门介绍了WebLogic Console的扩展:
http://dev2dev.bea.com.cn/techdoc/webser/2005012102.html
读者可以从http://dev2dev.bea.com/codelibrary/code/security_prov81.jsp下载WLS提供的Sample Security Provider。
4.1 MBean Types和WebLogic Console
一般情况下,人们可能更习惯通过WebLogic Console对Security Provider进行配置。这里我将简要描述这个过程,以及可以到达的一个效果。限于篇幅就不详细讨论了。
从weblogic.management.security.authentication.Authenticator扩展MBean Types。 MBean Types是MBean(http://java.sun.com/products/JavaManagement/wp/)的工厂,我们扩展SampleSecurityProviders81 包中的SimpleSampleAuthenticator.xml(MBean Definition File),增加一个我们自定义的参数LDAP Server IP:
<MBeanAttribute Name = "LDAPServerIP" Type = "java.lang.String" Writeable = "true" Default = ""127.0.0.1"" /> |
这样在Provider中我们将通过MbeanMaker(WLS提供)生成的SimpleSampleAuthenticatorMBean中取到这个属性:SimpleSampleAuthenticatorMBean.getLDAPServerIP()。MBean将在初始化Provider的时候作为参数传入。这样我们就可以通过MBean中的参数控制Provider的行为。
当然这个参数是可以在WebLogic Console中设置的,通过对MBean Types的扩展,在WebLogic Console上看到的画面如下:
图4-1-1
这样我们可以通过Console修改配置参数(修改的Security Provider参数将保存在config.xml中,默认的值将保存在MBean Jar File中)。
4.2 为何定制LDAP Authentication Provider
当我们面临越来越复杂的安全方面的业务需求时,或者面临较高的性能要求,需要根据目标LDAP做针对性的优化时,或者需要将我们已有的认证,或授权模块集成到WebLogic平台时,WebLogic提供的现成的Provider往往不能满足我们的需求。
4.2.1 复杂的业务需求
当系统要求用户不仅仅输入用户名(j_username),口令(j_password),还需要输入其他信息,比如登录的地点,系统的名字,用户的类型等等。如果是采用基于J2EE Form的验证方式, 登录信息需要提交到j_security_check(Servlet规范定义由容器负责实现的Servlet),导致我们没法处理更多的信息。
这个时候,如果能够实现我们自己的 Authentication Provider,那么我们就可以通过TextInputCallback来获取登录表单中更多的信息了;进而通过这些信息在Provider中完成符合我们需要的处理。
比如搜狐的登录页面上需要选择用户的类型:
图4-2-1
4.2.2 性能需求或者调优
有时,有的用户会比较困惑为何WebLogic LDAP Security Provider在压力测试中的表现不很理想,用户需要较长时间的等待,才能够登录到系统中。由于这些Provider是 WebLogic产品的一部分,因此缺乏对不同目标LDAP Server的有针对性的优化。这样就使得我们无法充分发挥具体LDAP Server的性能调优。
比如,有的LDAP Server支持动态组(LDAP Dynamic Group,成员关系是运行时根据ldap server,basedn,filter等动态决定的),可以使用如下的Filter查询用户属于哪些动态组:
(uniquemember=uid=MAXQ,ou=staff,ou=people,o=examples)
有的LDAP Server虽然支持动态组,但是支持的有限,不能使用上面的Filter获取用户属于哪些动态组。在WebLogic iPlanet Authentication Provider的实现中,它先是搜索出全部的动态组,然后再遍历这些动态组,依次去LDAP中检查用户是否属于一个组;很明显,这样虽然最大程度的满足了不同LDAP Server的要求(从产品的角度讲可能是必须的),但是与LDAP交互的次数大增,并发用户量一大性能下降的比较明显。
此时,如果系统中的LDAP支持上面的Filter或者有更好的搜索方式,那么完全可以通过定制Provider完成对性能的优化。
4.2.3 已有权限控制的集成
如果系统中已经存在了现成的满足需求的认证模块,并且已经很好的工作;在系统转向J2EE架构,并使用WebLogic Server做J2EE容器时,我们可能会更愿意直接在Provider中加入这个认证模块。
综上,我只是列举了一些可以驱动我们开发自己Provider的需求,相信在读者实际工作中可能会面临更复杂的情况,开发自己的Provider将是一个非常好的选择。
4.3 LDAP Authentication Provider实现
本文之前为了表述的方便没有单独提到LoginModule,认为LoginModule的行为就是LDAP Authentication Provider的行为。到了目前的具体实现阶段,我们必须分开Authentication Provider和JAAS LoginModule。最终部署到WebLogic上的实际只是LDAP AuthenticationProvider Implements。
WebLogic SecurityFramework通过Authentication Provider获取具体的JAAS LoginModule。通过LoginModule完成最终登录的工作。因此我们必须先实现一个AuthenticationProvider。
我们一般通过weblogic.security.spi.AuthenticationProvider 来实现自己的AuthenticationProvider。这里介绍其中的几个重要方法:
a) public void initialize(ProviderMBean mbean, SecurityServices services)
初始化一个Provider。通过参数MBean我们可以获取到在WebLogic Console中配置的各项参数。进而初始化我们的Provider,然后通过Provider传递到LoginModule中。
b) public void shutdown()
释放一些与Provider,LoginModule等相关的资源。
c) public AppConfigurationEntry getLoginModuleConfiguration()
这个方法非常重要,通过该方法,WebLogic Security Framework可以获取用于初始化LoginModule的AppConfigurationEntry。AppConfigurationEntry中存放了LoginModule的类名等信息,比如使用如下代码返回一个AppConfigurationEntry:
return new AppConfigurationEntry( "examples.security.providers.authentication.SampleLoginModuleImpl", controlFlag, options); |
其中LoginModule Name就
是"examples.security.providers.authentication.SampleLoginModuleImpl",我们通过它就可以实例化一个LoginModule并通过LoginModule.initialize()方法进行初始化。
d) public AppConfigurationEntry getAssertionModuleConfiguration()
该方法将返回一个与Identity Assertion Provider关联的LoginModule。这个Assertion LoginModule,将只会验证用户是否存在,以及如果存在返回用户的Principals。 该方法也比较重要,需要正确实现,比如我们使用CLIENT-CERT这种WEB认证方式,该方法就会被调用。
Provider的实现比较简单,读者可以在http://dev2dev.bea.com/codelibrary/code/security_prov81.jsp下载WebLogic提供的Samples,查看SampleAuthenticationProviderImpl的代码。
4.4 LDAP LoginModule 逻辑流程
实现了Provider后,必须拥有我们自己的LDAP LoginModule。下面是一个简单的用于演示的验证逻辑流程图。实际的一个LoginModule由于不同的业务需求,情况可能会复杂得多。这里只是描述了最核心最基本的逻辑,使读者能有一个清晰的思路。后面我将以这个流程为例进行实现。
4.5 LoginModule代码示例和讲解
这里我将使用Netscape LDAP SDK for java作为开发工具实现LDAP相关的操作,读者可以到http://docs.sun.com/db/doc/816-6402-10 下载开发手册,从http://www.mozilla.org/directory/ 下载SDK 包。一般来说还可以通过JNDI来操作LDAP,我个人认为Sun LDAP JNDI Provider中关于Connection Pool的实现非常优秀。但不管使用哪种SDK,对LDAP的编程原理上基本都是相同的(因为基于同样的LDAP协议),不同的可能仅仅是接口,类的名字而已。
4.5.1 初始化Connection Pool
为了有效使用宝贵,并且有限的LDAP连接,必须使用连接池。下面的代码初始化了一个LDAP连接池:
/** * */ static ConnectionPool pool; /** * * LDAPException */ public LdapClient() throws LDAPException { pool= new ConnectionPool( 1, 150, "127.0.0.1", 389, "cn=Directory Manager", "88888888"); } |
Sun JNDI LDAP Service Provider(JDK1.4)中可以通过在环境变量中设置具体的参数来启用连接池,达到复用连接的目的。具体可以参考链接:http://java.sun.com/products/jndi/tutorial/ldap/connect/index.html,下面是示例代码:
Hashtable env= new Hashtable();
…
env.put("com.sun.jndi.ldap.connect.pool", "true");
DirContext ctx= new InitialDirContext( env);
4.5.2 根据用户输入的登录帐号,搜索用户Entry
下面这个方法实现了从LDAP中搜索用户Entry DN的最简单的过程。实际上我们可以在其中实现很多定制的功能。比如允许用户使用多于一个的帐号登录,只要这些帐号能够通过LDAP Searche最终返回一个唯一的用户DN即可。
* * LDAPException */ public String getUserDN( String uid) throws LDAPException{ LDAPConnection conn= pool.getConnection(); try { String[] attrs= new String[] {}; LDAPSearchResults sr= conn.search("o=examples", 2, "(uid="+ uid +")", attrs, false); if ( sr.hasMoreElements()) { LDAPEntry entry= sr.next(); return entry.getDN(); } throw new LDAPException("No Such Object:"+ uid, LDAPException.NO_SUCH_OBJECT); }catch ( LDAPException ex) { throw ex; }finally { try { if (conn!= null) pool.close(conn); }catch ( Exception ex) {} } } |
首先需要从池中获取一个LDAP连接,然后使用LDAPConnection.search方法进行搜索。我这里以一个典型的LDAP Search接口为例进行说明,其他API比如JNDI等,基于同样的LDAP协议,接口中同样参数的含义都是相同的。
public LDAPSearchResults search(java.lang.String base, int scope, java.lang.String filter, java.lang.String[] attrs, boolean attrsOnly) |
1) Base: 表明从该Basedn开始搜索,可以通过MBean获取
2) Scope: 搜索的范围:
a) LDAPv2.SCOPE_BASE, 只搜索basedn指定的entry
b) LDAPv2.SCOPE_ONE, 在basedn的下一级entry中搜索,不包括basedn
c) LDAPv3.SCOPE_SUB,在basedn的全部下级entry中搜索,包括basedn
3) Filter:过虑条件。比如uid=ZHANGSAN,搜索UID为ZHANGSAN的用户;再比如uid=ZHANG*,搜索 ZHANG开头的帐号;或者uid=Z*S,搜索以Z开头S结尾的帐号。前面已经介绍过这里就不多说了。
4) attrs:返回该attrs数组中指定的属性,比如new String[]{“uid”},只返回属性uid,其他属性将会不在结果中返回。 一般来说我们会要求开发人员只将需要的属性返回,这样避免返回无用的属性,降低网络和Server等方面的资源开销;而且如果存在一个有较大属性集合的Entry,并且你并不使用到这个较大的属性集合。举个实际例子来说,比如你的系统中拥有很多成员数目超过2万或者更多的LDAP Groups,并且你希望通过LDAP Search找出某一个用户属于的组CN,那么搜索结果只返回组的CN已经可以满足你的要求了,这时就没有必要将全部member属性也返回了。这在后面我会有代码来说明。
这里还有一点很重要的细节,相信对读者会有帮助。比如,你希望搜索到modifytimestamp等Operational Attributes。这些属性(LDAP Server自己负责维护的,用户无法修改的)必须要在参数attrs中指定,LDAP Server才会返回给客户端。如果用户希望返回全部User Attributes的同时,返回指定的 Operational Attributes那该怎么办?不在attrs列表中的属性将不会被返回,一旦我指定了attrs,是否需要将全部的属性都列在attrs参数中?实际上此时只需要传入 new String[]{ “*”, “createtimestamp”}这样的参数即可;“*”号即代表了全部的User Attributes。在Sun JNDI LDAP Service Provider中也是这样,尽管找遍Sun关于JNDI的文档也找不到这样的说明。LDAP协议对此的说明在 http://www.ietf.org/rfc/rfc2251.txt 第28页。
5) attrsOnly:只返回属性名称,不包含值。我们一般设置为false。
我们下面看一下Sun JNDI中关于LDAP Search的接口:
public NamingEnumeration search(String name,
String filter,
SearchControls cons)
throws NamingException
name参数就是上面的basedn,SearchControls中封装更多的设置,比如:SearchControls. setSearchScope(int scope)其中的scope对应上面的2); SearchControls.setReturningAttributes(String[] attrs),设置指定返回的属性,对应上面的4)。
其他的参数还包括Size Limit,即限制搜索结果的数目(LDAP返回的Entry一般情况下是没有排序的,除非使用一些Sort Control),当你的搜索可能会返回成千上万的Entry时,限制搜索的数目是非常明智也是必须的。关于这些,读者可以参考API文档或者LDAP协议。
4.5.3 根据用户的Entry DN,和用户输入的口令完成身份验证
下面的方法实现了到LDAP的身份验证。LDAP中的用户实际上就是一个LDAP Entry,一次验证实际是将Connection与这个Entry进行绑定,因此验证时我们需要两个必须的参数,一个是Entry的DN,一个是口令。LDAP的身份验证方式有多种,包括SASL以及基于证书的验证等,我们这里只介绍Simple Authentication。关于SASL更多信息读者可以参考:http://www.ietf.org/rfc/rfc2222.txt。
/** * * dn * pwd * * LDAPException */ public boolean authentication( String dn, String pwd) throws LDAPException { LDAPConnection conn= pool.getConnection(); try { conn.authenticate( dn, pwd); return true; }catch ( LDAPException ex) { if ( ex.getLDAPResultCode()== LDAPException.INVALID_CREDENTIALS) { return false; // 用户口令错误 } if ( ex.getLDAPResultCode()== LDAPException.NO_SUCH_OBJECT) { return false; // 用户不存在 } throw ex; }finally { try { if ( conn!= null) pool.close(conn); }catch ( Exception ex) { } } } |
我们这里使用 LDAP Bind操作完成对用户的身份验证。本文前面曾提到也可以使用LDAP Compare,通过比较口令属性(userpassword)中的值来完成验证。我们需要根据不同的LDAP Server选择合适的验证方法。
比如iPlanet LDAP Server 5,只能通过Bind完成认证;而Oracle Internet Directory可以通过LDAP Compare完成验证,而且还支持口令策略。
如果我们使用了连接池,连接池中的连接一般都是使用权限较大的用户初始化的,这样这些连接才可以完成对LDAP的搜索操作;而当通过这些连接对普通用户进行身份验证时,如果通过验证,连接的身份将被改变为普通的用户(或称为与普通用户的身份关联)。普通用户很可能没有除了bind以外的任何权限,所以在连接被放入池中前,我们必须要恢复连接的身份。
这样我们必须执行两次LDAP Bind,一次用于对普通用户验证身份;一次用于恢复连接的较大权限的用户身份。我们看到这样效率是比较低的,可能你在LDAP Server端统计有2万次bind请求,实际上只有1万人次登录。
对于特定的LDAP Server,比如 Oracle Internet Directory,可以通过LDAP Compare对用户身份进行验证,并且不会改变连接关联的用户身份。这样我在使用池的情况下只需要一次LDAP Compare即可。效率有很大提高。如果不通过定制LDAP Authentication Provider,这样的调优是没法实现的。
Netscape LDAP SDK的ConnectionPool的实现中,在连接放入池中前会检查连接的身份,如果身份被改变,那么会重新进行bind。所以我们没有必要在代码中再做bind。
4.5.4 根据用户的DN搜索用户属于的组列表
有了上面的基础后,这个方法就很容易理解了。下面我们来看看如何返回用户所属的组。
/** * * groupbasedn * memberDn * * LDAPException */ public List getGroupMembership( String groupbasedn, String memberDn) throws LDAPException { LDAPConnection conn= pool.getConnection(); try { LDAPSearchResults sr= conn.search( groupbasedn, 2, "(uniquemember="+ memberDn +")", new String[] {"cn"}, false); List groups= new java.util.ArrayList(); while ( sr.hasMoreElements()) { LDAPEntry entry= sr.next(); LDAPAttribute attr= entry.getAttribute("cn"); if ( attr!= null) { String[] values= attr.getStringValueArray(); if ( values!= null && values.length>0) groups.add( values[0]); } } return groups; }catch ( LDAPException ex) { throw ex; }finally { try { if ( conn!= null) pool.close(conn); }catch ( Exception ex) { } } } |
成员和组的membership主要是通过uniquemember或者 member属性来定义的。成员不仅仅可以使用用户,也可以是组,因为组可以嵌套。
a) 上面的方法中只实现了一个层次的搜索,即用户——组的搜索,而组间的嵌套搜索没有实现。读者可以根据系统内的具体情况,在此处也可以做一些优化。
b) 组成员的数量可能比较大,为了避免不必要的开销,我们指定只返回组的cn属性。
c) 动态组的优化。在WebLogic默认的实现中,Provider(实际为LoginModule)会不断的拿动态组中定义的URL中的filter和用户的DN去LDAP做搜索,来看用户是否属于该组。本文前面也讨论了,这样效率很低,完全可以放在Provider本地实现URL和Entry的匹配。
4.5.5 LoginModule中的login()方法实现
一切准备就绪后,我们就可以完成LoginModule.login()这个最核心的方法了。下面我根据代码中的注释逐条说明。
// A boolean loginSucceeded; // B List principals= new java.util.ArrayList(); /** * * * LoginException */ public boolean login() throws LoginException { // C Callback[] callbacks= new Callback[] { new NameCallback("username: "), new PasswordCallback("password: ",false)}; try { callbackHandler.handle( callbacks); }catch (IOException e) { throw new LoginException(e.toString()); }catch (UnsupportedCallbackException e) { throw new LoginException(e.toString() + " " +e.getCallback().toString()); } // String userName = ((NameCallback)callbacks[0]).getName(); if ( userName== null || userName.length()== 0) { throw new LoginException("User login name is empty!"); } // PasswordCallback passwordCallback= (PasswordCallback)callbacks[1]; char[] password = passwordCallback.getPassword(); passwordCallback.clearPassword(); if ( password== null || password.length== 0) { throw new LoginException("User password is empty!"); } try { // D String dn= this.getUserDN( userName); if ( dn== null) { throw new LoginException("User "+ userName +" doesn't exist."); } // E boolean authResult= this.authentication( dn, String.valueOf( password)); if ( authResult== false) { throw new FailedLoginException("User login failed."); } // F principals.add( new WLSUserImpl( userName)); // G List groups= this.getGroupMembership( "ou=groups,o=examples", dn); for ( int i=0, n=groups.size(); i<n; i++) { String cn= String.valueOf(groups.get(i)); // H principals.add( new WLSGroupImpl(cn)); } }catch ( LDAPException ex) { java.io.StringWriter sw = new java.io.StringWriter(); ex.printStackTrace(new java.io.PrintWriter(sw)); sw.flush(); throw new LoginException( sw.toString()); } return loginSucceeded= true; } |
a) 用于保存验证结果,在后续方法(commit等)中使用
b) 用于保存和用户关联的Principal,在后续方法(commit等)中使用
c) 在JAAS中通过CallbackHandler来完成用户和底层安全认证系统间信息的交换,这里我们通过CallbackHandler获取用户输入的登录帐号和口令。CallbackHandler将在初始化LoginModule时由WebLogic Security Framework传入。读者可能会问,我是否可以在此要求WebLogic Security Framework传入我自己实现的CallbackHandler,进而获取更多的信息,支持更多的Callback?很遗憾,不可以。只有在一种情况下(本文前面也提到)有一个变通,就是在Web应用中,通过Form Based这种认证方式进行用户身份验证时,可以获取更多的登录表单中提交的cgi变量。
下面的代码将演示这种技术:
Callback[] callbacks= new Callback[] { new NameCallback("username: "), new PasswordCallback("password: ",false), new TextInputCallback("my_hidden_field")}; try { callbackHandler.handle( callbacks); }catch (IOException e) { throw new LoginException(e.toString()); }catch (UnsupportedCallbackException e) { throw new LoginException(e.toString() + " " +e.getCallback().toString()); } String value= ((TextInputCallback)callbacks[2]).getText(); |
这样我们通过((TextInputCallback)callbacks[2]).getText()来获取表单中更多的信息。其中TextInputCallback的Prompt必须要和表单中对应域的名字一致,否则取不到信息。如果有多个域,则需要传入多个TextInputCallback。
同时如果登录表单中没有这些对应TextInputCallback的域,或者使用JNDI方式验证,那么会抛出UnsupportedCallbackException。
下面是一个符合Servlet安全规范的登录表单,其中域my_hidden_field的值将通过CallbackHandler传递到的TextInputCallback中:
<html> <form method=”POST” action=”j_security_check”> <input type=”text” name=”j_username”> <input type=”password” name=”j_password”> <input type=”hidden” name=”my_hidden_field”value=”Hello Callback!”> </form> </html> |
d) 生成一个用于存放Principal的集合对象
e) 通过登录帐号获取LDAP中的Entry DN
f) 通过DN和用户口令进行身份验证
g) 将通过验证的用户名加入到Principal列表中,这些以后都将代表用户的一定权限
h) 查找用户属于哪些组
i) 将这些组的名字加入到Principal列表中
4.5.6 LoginModule中的commit()和abort()方法实现
完成了以上的验证后,我们需要通过LoginModule.commit()方法提交验证结果;或者abort()方法取消验证结果。
// A private Subject subject; /** * * * LoginException */ public boolean commit() throws LoginException { if (loginSucceeded) { // B subject.getPrincipals().addAll(principals); return true; }else { return false; } } /** * * * LoginException */ public boolean abort() throws LoginException { // C subject.getPrincipals().removeAll(principals); return true; } |
我这里仍然通过代码中的注释进行相应的说明:
a) JAAS Subject,在LoginModule初始化时,由WebLogic Security Framework负责传入,用户的权限信息(Principals等)都将封装到该Subject中。同时Principal Validator会对Subject中的Principals进行签名,防止被黑客纂改。
b) 用户认证成功的情况下,将用户关联的Principals加入到Subject中。这样用户在进行后续的访问时,WebLogic Security Framework会检查与用户关联的Subject中是否有可以访问受限资源的Principal。
c) 用户登录失败的情况下(有可能是其他的LoginModule导致的整体登录失败,具体参考JAAS Control Flag),我们在commit中添加的Principals必须从Subject中删除。
通过上面的描述和讨论,相信读者已经对如何实现一个LDAP AuthenticationProvider/LoginModule有了比较清楚的了解。限于篇幅省略了很多细节,对此有需要的读者可以根据文中给出的相关链接做进一步的了解;或者与我交流。从这里我们也可以看到核心的逻辑还是比较简单的。其中将LDAP Group作为用户的Principal,是J2EE Security与LDAP Security结合的非常重要的一步。通过这种结合,我们可以将LDAP作为后端,设计出基于J2EE,JAAS的用户身份验证,权限管理等系统。
5 部署中的注意事项
开发完成LDAP Authentication Provider后,需要将通过WebLogic MBean Maker生成的MBean Jar File放到/server/lib/mbeantypes目录下。由于WebLogic Server在启动时会加载这些Provider,因此在一个分布式的环境中,WebLogic Domain内的全部WLServer(包括Managed Server)均需要部署该Jar包。
同时由于使用了Provider MBean,因此,当你删除了MBean Types中定义的,并且通过修改保存在config.xml(WebLogic Domain配置文件)中的属性时,重新部署的Jar包会导致WebLogic Server无法正常启动,因为它没有办法按照config.xml中配置的属性初始化MBean。此时需要手工修改config.xml,删除相关的属性即可。
<examples.security.providers.authentication.simple.SimpleSampleAuthenticator
ControlFlag="OPTIONAL"
Name="Security:Name=myrealmSimpleSampleAuthenticator"
UserBaseDN="ou=people, o=examples" Realm="Security:Name=myrealm"/>
比如UserBaseDN已经不需要通过MBean来管理,并且在MBean Types中已经删除,那么直接删除这里的UserBaseDN="ou=people, o=examples" 就可以了。
6 结束语
本文所讨论的内容有些过于广泛,从我个人角度讲,写起来也比较困难,很多技术问题没有进行深入的讨论。其实只是希望通过本文,能够帮助读者对WebLogic Security,J2EE Security,LDAP Security以及JAAS有一个初步的认识;能够给希望实现LDAP Authentication Provider,或者被WebLogic LDAP Authentication Provider困惑的读者一些启示。只要能够对读者有所帮助,本文的目的就算达到了。
限于篇幅,本文很多地方的表述可能并不清晰,也可能会有一些错误,如果在阅读过程中产生任何疑问或者有任何看法都可以通过E-mail来与我交流,我也非常希望能和大家交流。
7 参考资料
a) http://e-docs.bea.com/wls/docs81/secwlres/types.html#1213777 关于WebLogic Resource的更多说明
b) http://e-docs.bea.com/wls/docs81/security/index.html 关于WebLogic Security的全部内容
c) http://e-docs.bea.com/wls/docs81/dvspisec/rm.html#1145542 关于Role Mapping Provider的更多内容
d) http://dev2dev.bea.com.cn/techdoc/webser/2005012102.html 关于WebLogic Console的扩展
e) http://dev2dev.bea.com/codelibrary/code/security_prov81.jsp Custom Seuciryt Provider的Samples
f) http://java.sun.com/products/JavaManagement/wp/ 更多关于Sun JMX
g) http://java.sun.com/products/jndi/tutorial/ldap/connect/index.html 关于如何使用Sun JNDI LDAP Service Provider中提供的连接池
h) http://www.ietf.org/rfc/rfc2254.txt 更多关于LDAP Filter
i) http://www.ietf.org/rfc/rfc2251.txt LDAP V3协议
j) http://www.ietf.org/rfc/rfc2222.txt 更多关于SASL
8 关于作者
作者马晓强,高级软件工程师,现在广东从事J2EE,LDAP相关的设计开发工作。对WebLogic,LDAP以及Single Sign-On等产品、技术很感兴趣。您可以通过 E-mail: iammxq.com.cn 与他联系。