﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>BlogJava-自然-文章分类-Server</title><link>http://www.blogjava.net/masen/category/29350.html</link><description /><language>zh-cn</language><lastBuildDate>Thu, 21 Feb 2008 08:22:12 GMT</lastBuildDate><pubDate>Thu, 21 Feb 2008 08:22:12 GMT</pubDate><ttl>60</ttl><item><title>session的详细解释</title><link>http://www.blogjava.net/masen/articles/180875.html</link><dc:creator>Masen</dc:creator><author>Masen</author><pubDate>Wed, 20 Feb 2008 07:48:00 GMT</pubDate><guid>http://www.blogjava.net/masen/articles/180875.html</guid><wfw:comment>http://www.blogjava.net/masen/comments/180875.html</wfw:comment><comments>http://www.blogjava.net/masen/articles/180875.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/masen/comments/commentRss/180875.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/masen/services/trackbacks/180875.html</trackback:ping><description><![CDATA[<p>Java基础－关于session的详细解释 <br />
来源:不详 (2006-06-01 16:49:09) </p>
<p>&nbsp;<br />
一、术语session </p>
<p>　　在我的经验里，session这个词被滥用的程度大概仅次于transaction，更加有趣的是transaction与session在某些语境下的含义是相同的。</p>
<p>　　session，中文经常翻译为会话，其本来的含义是指有始有终的一系列动作/消息，比如打电话时从拿起电话拨号到挂断电话这中间的一系列过程可以称之为一个session。</p>
<p>有时候我们可以看到这样的话&#8220;在一个浏览器会话期间，...&#8221;，这里的会话一词用的就是其本义，是指从一个浏览器窗口打开到关闭这个期间①。最混乱的是&#8220;用户（客户端</p>
<p>）在一次会话期间&#8221;这样一句话，它可能指用户的一系列动作（一般情况下是同某个具体目的相关的一系列动作，比如从登录到选购商品到结账登出这样一个网上购物的过程</p>
<p>，有时候也被称为一个transaction），然而有时候也可能仅仅是指一次连接，也有可能是指含义①，其中的差别只能靠上下文来推断②。</p>
<p>　　然而当session一词与网络协议相关联时，它又往往隐含了&#8220;面向连接&#8221;和/或&#8220;保持状态&#8221;这样两个含义，&#8220;面向连接&#8221;指的是在通信双方在通信之前要先建立一个通信</p>
<p>的渠道，比如打电话，直到对方接了电话通信才能开始，与此相对的是写信，在你把信发出去的时候你并不能确认对方的地址是否正确，通信渠道不一定能建立，但对发信人</p>
<p>来说，通信已经开始了。&#8220;保持状态&#8221;则是指通信的一方能够把一系列的消息关联起来，使得消息之间可以互相依赖，比如一个服务员能够认出再次光临的老顾客并且记得上</p>
<p>次这个顾客还欠店里一块钱。这一类的例子有&#8220;一个TCP session&#8221;或者&#8220;一个POP3 session&#8221;③。</p>
<p>　　而到了web服务器蓬勃发展的时代，session在web开发语境下的语义又有了新的扩展，它的含义是指一类用来在客户端与服务器之间保持状态的解决方案④。有时候</p>
<p>session也用来指这种解决方案的存储结构，如&#8220;把xxx保存在session里&#8221;⑤。由于各种用于web开发的语言在一定程度上都提供了对这种解决方案的支持，所以在某种特定语</p>
<p>言的语境下，session也被用来指代该语言的解决方案，比如经常把Java里提供的javax.servlet.http.HttpSession简称为session⑥。</p>
<p>　　鉴于这种混乱已不可改变，本文中session一词的运用也会根据上下文有不同的含义，请大家注意分辨。</p>
<p>　　在本文中，使用中文&#8220;浏览器会话期间&#8221;来表达含义①，使用&#8220;session机制&#8221;来表达含义④，使用&#8220;session&#8221;表达含义⑤，使用具体的&#8220;HttpSession&#8221;来表达含义⑥</p>
<p>　　二、HTTP协议与状态保持</p>
<p>　　HTTP协议本身是无状态的，这与HTTP协议本来的目的是相符的，客户端只需要简单的向服务器请求下载某些文件，无论是客户端还是服务器都没有必要纪录彼此过去的行</p>
<p>为，每一次请求之间都是独立的，好比一个顾客和一个自动售货机或者一个普通的（非会员制）大卖场之间的关系一样。</p>
<p>　　然而聪明（或者贪心？）的人们很快发现如果能够提供一些按需生成的动态信息会使web变得更加有用，就像给有线电视加上点播功能一样。这种需求一方面迫使HTML逐步</p>
<p>添加了表单、脚本、DOM等客户端行为，另一方面在服务器端则出现了CGI规范以响应客户端的动态请求，作为传输载体的HTTP协议也添加了文件上载、cookie这些特性。其中</p>
<p>cookie的作用就是为了解决HTTP协议无状态的缺陷所作出的努力。至于后来出现的session机制则是又一种在客户端与服务器之间保持状态的解决方案。</p>
<p>　　让我们用几个例子来描述一下cookie和session机制之间的区别与联系。笔者曾经常去的一家咖啡店有喝5杯咖啡免费赠一杯咖啡的优惠，然而一次性消费5杯咖啡的机会微</p>
<p>乎其微，这时就需要某种方式来纪录某位顾客的消费数量。想象一下其实也无外乎下面的几种方案：</p>
<p>　　1、该店的店员很厉害，能记住每位顾客的消费数量，只要顾客一走进咖啡店，店员就知道该怎么对待了。这种做法就是协议本身支持状态。</p>
<p>　　2、发给顾客一张卡片，上面记录着消费的数量，一般还有个有效期限。每次消费时，如果顾客出示这张卡片，则此次消费就会与以前或以后的消费相联系起来。这种做法</p>
<p>就是在客户端保持状态。</p>
<p>　　3、发给顾客一张会员卡，除了卡号之外什么信息也不纪录，每次消费时，如果顾客出示该卡片，则店员在店里的纪录本上找到这个卡号对应的纪录添加一些消费信息。这</p>
<p>种做法就是在服务器端保持状态。</p>
<p>　　由于HTTP协议是无状态的，而出于种种考虑也不希望使之成为有状态的，因此，后面两种方案就成为现实的选择。具体来说cookie机制采用的是在客户端保持状态的方案</p>
<p>，而session机制采用的是在服务器端保持状态的方案。同时我们也看到，由于采用服务器端保持状态的方案在客户端也需要保存一个标识，所以session机制可能需要借助于</p>
<p>cookie机制来达到保存标识的目的，但实际上它还有其他选择。</p>
<p>　　三、理解cookie机制 </p>
<p>　　cookie机制的基本原理就如上面的例子一样简单，但是还有几个问题需要解决：&#8220;会员卡&#8221;如何分发；&#8220;会员卡&#8221;的内容；以及客户如何使用&#8220;会员卡&#8221;。</p>
<p>　　正统的cookie分发是通过扩展HTTP协议来实现的，服务器通过在HTTP的响应头中加上一行特殊的指示以提示浏览器按照指示生成相应的cookie。然而纯粹的客户端脚本如</p>
<p>JavaScript或者VBScript也可以生成cookie。</p>
<p>　　而cookie的使用是由浏览器按照一定的原则在后台自动发送给服务器的。浏览器检查所有存储的cookie，如果某个cookie所声明的作用范围大于等于将要请求的资源所在</p>
<p>的位置，则把该cookie附在请求资源的HTTP请求头上发送给服务器。意思是麦当劳的会员卡只能在麦当劳的店里出示，如果某家分店还发行了自己的会员卡，那么进这家店的</p>
<p>时候除了要出示麦当劳的会员卡，还要出示这家店的会员卡。</p>
<p>　　cookie的内容主要包括：名字，值，过期时间，路径和域。</p>
<p>　　其中域可以指定某一个域比如.google.com，相当于总店招牌，比如宝洁公司，也可以指定一个域下的具体某台机器比如www.google.com或者froogle.google.com，可以用</p>
<p>飘柔来做比。</p>
<p>　　路径就是跟在域名后面的URL路径，比如/或者/foo等等，可以用某飘柔专柜做比。</p>
<p>　　路径与域合在一起就构成了cookie的作用范围。</p>
<p>　　如果不设置过期时间，则表示这个cookie的生命期为浏览器会话期间，只要关闭浏览器窗口，cookie就消失了。这种生命期为浏览器会话期的cookie被称为会话cookie。</p>
<p>会话cookie一般不存储在硬盘上而是保存在内存里，当然这种行为并不是规范规定的。如果设置了过期时间，浏览器就会把cookie保存到硬盘上，关闭后再次打开浏览器，这</p>
<p>些cookie仍然有效直到超过设定的过期时间。</p>
<p>　　存储在硬盘上的cookie可以在不同的浏览器进程间共享，比如两个IE窗口。而对于保存在内存里的cookie，不同的浏览器有不同的处理方式。对于IE，在一个打开的窗口</p>
<p>上按Ctrl-N（或者从文件菜单）打开的窗口可以与原窗口共享，而使用其他方式新开的IE进程则不能共享已经打开的窗口的内存cookie；对于Mozilla Firefox0.8，所有的进</p>
<p>程和标签页都可以共享同样的cookie。一般来说是用javascript的window.open打开的窗口会与原窗口共享内存cookie。浏览器对于会话cookie的这种只认cookie不认人的处理</p>
<p>方式经常给采用session机制的web应用程序开发者造成很大的困扰。</p>
<p>　　下面就是一个goolge设置cookie的响应头的例子</p>
<p>HTTP/1.1 302 Found<br />
Location: http://www.google.com/intl/zh-CN/<br />
Set-Cookie: PREF=ID=0565f77e132de138:NW=1:TM=1098082649:LM=1098082649:S=KaeaCFPo49RiA_d8; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; </p>
<p>domain=.google.com<br />
Content-Type: text/html</p>
<p><br />
　　这是使用HTTPLook这个HTTP Sniffer软件来俘获的HTTP通讯纪录的一部分</p>
<p>&nbsp;</p>
<p>　　浏览器在再次访问goolge的资源时自动向外发送cookie</p>
<p><br />
　　使用Firefox可以很容易的观察现有的cookie的值</p>
<p>　　使用HTTPLook配合Firefox可以很容易的理解cookie的工作原理。</p>
<p>&nbsp;</p>
<p>　　IE也可以设置在接受cookie前询问</p>
<p><br />
　　这是一个询问接受cookie的对话框。</p>
<p>　　四、理解session机制</p>
<p>　session机制是一种服务器端的机制，服务器使用一种类似于散列表的结构（也可能就是使用散列表）来保存信息。</p>
<p>　　当程序需要为某个客户端的请求创建一个session的时候，服务器首先检查这个客户端的请求里是否已包含了一个session标识 - 称为session id，如果已包含一个</p>
<p>session id则说明以前已经为此客户端创建过session，服务器就按照session id把这个session检索出来使用（如果检索不到，可能会新建一个），如果客户端请求不包含</p>
<p>session id，则为此客户端创建一个session并且生成一个与此session相关联的session id，session id的值应该是一个既不会重复，植蝗菀妆徽业焦媛梢苑略斓淖址</p>
<p>飧鰏ession id将被在本次响应中返回给客户端保存。</p>
<p>　　保存这个session id的方式可以采用cookie，这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器。一般这个cookie的名字都是类似于SEEESIONID，而</p>
<p>。比如weblogic对于web应用程序生成的cookie，JSESSIONID=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764，它的名字就是JSESSIONID。</p>
<p>　　由于cookie可以被人为的禁止，必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器。经常被使用的一种技术叫做URL重写，就是把session id直接</p>
<p>附加在URL路径的后面，附加方式也有两种，一种是作为URL路径的附加信息，表现形式为http://...../xxx;jsessionid=ByOK ... 99zWpBng!-145788764另一种是作为查询字</p>
<p>符串附加在URL后面，表现形式为http://...../xxx?jsessionid=ByOK ... 99zWpBng!-145788764<br />
这两种方式对于用户来说是没有区别的，只是服务器在解析的时候处理的方式不同，采用第一种方式也有利于把session id的信息和正常程序参数区分开来。</p>
<p>　　为了在整个交互过程中始终保持状态，就必须在每个客户端可能请求的路径后面都包含这个session id。</p>
<p>　　另一种技术叫做表单隐藏字段。就是服务器会自动修改表单，添加一个隐藏字段，以便在表单提交时能够把session id传递回服务器。</p>
<p>　　这种技术现在已较少应用，笔者接触过的很古老的iPlanet6(SunONE应用服务器的前身)就使用了这种技术。实际上这种技术可以简单的用对action应用URL重写来代替。</p>
<p>　　在谈论session机制的时候，常常听到这样一种误解&#8220;只要关闭浏览器，session就消失了&#8221;。其实可以想象一下会员卡的例子，除非顾客主动对店家提出销卡，否则店家</p>
<p>绝对不会轻易删除顾客的资料。对session来说也是一样的，除非程序通知服务器删除一个session，否则服务器会一直保留，程序一般都是在用户做log off的时候发个指令去</p>
<p>删除session。然而浏览器从来不会主动在关闭之前通知服务器它将要关闭，因此服务器根本不会有机会知道浏览器已经关闭，之所以会有这种错觉，是大部分session机制都</p>
<p>使用会话cookie来保存session id，而关闭浏览器后这个session id就消失了，再次连接服务器时也就无法找到原来的session。如果服务器设置的cookie被保存到硬盘上，或</p>
<p>者使用某种手段改写浏览器发出的HTTP请求头，把原来的session id发送给服务器，则再次打开浏览器仍然能够找到原来的session。</p>
<p>　　恰恰是由于关闭浏览器不会导致session被删除，迫使服务器为seesion设置了一个失效时间，当距离客户端上一次使用session的时间超过这个失效时间时，服务器就可以</p>
<p>认为客户端已经停止了活动，才会把session删除以节省存储空间。</p>
<p>　　五、理解javax.servlet.http.HttpSession</p>
<p>　　HttpSession是Java平台对session机制的实现规范，因为它仅仅是个接口，具体到每个web应用服务器的提供商，除了对规范支持之外，仍然会有一些规范里没有规定的细</p>
<p>微差异。这里我们以BEA的Weblogic Server8.1作为例子来演示。</p>
<p>　　首先，Weblogic Server提供了一系列的参数来控制它的HttpSession的实现，包括使用cookie的开关选项，使用URL重写的开关选项，session持久化的设置，session失效</p>
<p>时间的设置，以及针对cookie的各种设置，比如设置cookie的名字、路径、域，cookie的生存时间等。</p>
<p>　　一般情况下，session都是存储在内存里，当服务器进程被停止或者重启的时候，内存里的session也会被清空，如果设置了session的持久化特性，服务器就会把session</p>
<p>保存到硬盘上，当服务器进程重新启动或这些信息将能够被再次使用，Weblogic Server支持的持久性方式包括文件、数据库、客户端cookie保存和复制。</p>
<p>　　复制严格说来不算持久化保存，因为session实际上还是保存在内存里，不过同样的信息被复制到各个cluster内的服务器进程中，这样即使某个服务器进程停止工作也仍</p>
<p>然可以从其他进程中取得session。</p>
<p>　　cookie生存时间的设置则会影响浏览器生成的cookie是否是一个会话cookie。默认是使用会话cookie。有兴趣的可以用它来试验我们在第四节里提到的那个误解。</p>
<p>　　cookie的路径对于web应用程序来说是一个非常重要的选项，Weblogic Server对这个选项的默认处理方式使得它与其他服务器有明显的区别。后面我们会专题讨论。</p>
<p>　　关于session的设置参考[5] http://e-docs.bea.com/wls/docs70/webapp/weblogic_xml.html#1036869</p>
<p>　　六、HttpSession常见问题</p>
<p>　　（在本小节中session的含义为⑤和⑥的混合）</p>
<p>　　1、session在何时被创建</p>
<p>　　一个常见的误解是以为session在有客户端访问时就被创建，然而事实是直到某server端程序调用HttpServletRequest.getSession(true)这样的语句时才被创建，注意如</p>
<p>果JSP没有显示的使用 关闭session，则JSP文件在编译成Servlet时将会自动加上这样一条语句HttpSession session = HttpServletRequest.getSession(true);这也是JSP中</p>
<p>隐含的session对象的来历。</p>
<p>　　由于session会消耗内存资源，因此，如果不打算使用session，应该在所有的JSP中关闭它。</p>
<p>　　2、session何时被删除</p>
<p>　　综合前面的讨论，session在下列情况下被删除a.程序调用HttpSession.invalidate();或b.距离上一次收到客户端发送的session id时间间隔超过了session的超时设置;</p>
<p>或c.服务器进程被停止（非持久session）</p>
<p>　　3、如何做到在浏览器关闭时删除session</p>
<p>　　严格的讲，做不到这一点。可以做一点努力的办法是在所有的客户端页面里使用javascript代码window.oncolose来监视浏览器的关闭动作，然后向服务器发送一个请求来</p>
<p>删除session。但是对于浏览器崩溃或者强行杀死进程这些非常规手段仍然无能为力。</p>
<p>　　4、有个HttpSessionListener是怎么回事</p>
<p>　　你可以创建这样的listener去监控session的创建和销毁事件，使得在发生这样的事件时你可以做一些相应的工作。注意是session的创建和销毁动作触发listener，而不</p>
<p>是相反。类似的与HttpSession有关的listener还有HttpSessionBindingListener，HttpSessionActivationListener和HttpSessionAttributeListener。 </p>
<p>5、存放在session中的对象必须是可序列化的吗</p>
<p>　　不是必需的。要求对象可序列化只是为了session能够在集群中被复制或者能够持久保存或者在必要时server能够暂时把session交换出内存。在Weblogic Server的</p>
<p>session中放置一个不可序列化的对象在控制台上会收到一个警告。我所用过的某个iPlanet版本如果session中有不可序列化的对象，在session销毁时会有一个Exception，很</p>
<p>奇怪。</p>
<p>　　6、如何才能正确的应付客户端禁止cookie的可能性</p>
<p>　　对所有的URL使用URL重写，包括超链接，form的action，和重定向的URL，具体做法参见[6]<br />
http://e-docs.bea.com/wls/docs70/webapp/sessions.html#100770</p>
<p>　　7、开两个浏览器窗口访问应用程序会使用同一个session还是不同的session</p>
<p>　　参见第三小节对cookie的讨论，对session来说是只认id不认人，因此不同的浏览器，不同的窗口打开方式以及不同的cookie存储方式都会对这个问题的答案有影响。</p>
<p>　　8、如何防止用户打开两个浏览器窗口操作导致的session混乱</p>
<p>　　这个问题与防止表单多次提交是类似的，可以通过设置客户端的令牌来解决。就是在服务器每次生成一个不同的id返回给客户端，同时保存在session里，客户端提交表单</p>
<p>时必须把这个id也返回服务器，程序首先比较返回的id与保存在session里的值是否一致，如果不一致则说明本次操作已经被提交过了。可以参看《J2EE核心模式》关于表示层</p>
<p>模式的部分。需要注意的是对于使用javascript window.open打开的窗口，一般不设置这个id，或者使用单独的id，以防主窗口无法操作，建议不要再window.open打开的窗口</p>
<p>里做修改操作，这样就可以不用设置。</p>
<p>　　9、为什么在Weblogic Server中改变session的值后要重新调用一次session.setValue<br />
做这个动作主要是为了在集群环境中提示Weblogic Server session中的值发生了改变，需要向其他服务器进程复制新的session值。</p>
<p>　　10、为什么session不见了</p>
<p>　　排除session正常失效的因素之外，服务器本身的可能性应该是微乎其微的，虽然笔者在iPlanet6SP1加若干补丁的Solaris版本上倒也遇到过；浏览器插件的可能性次之，</p>
<p>笔者也遇到过3721插件造成的问题；理论上防火墙或者代理服务器在cookie处理上也有可能会出现问题。</p>
<p>　　出现这一问题的大部分原因都是程序的错误，最常见的就是在一个应用程序中去访问另外一个应用程序。我们在下一节讨论这个问题。</p>
<p>　　七、跨应用程序的session共享</p>
<p>　　常常有这样的情况，一个大项目被分割成若干小项目开发，为了能够互不干扰，要求每个小项目作为一个单独的web应用程序开发，可是到了最后突然发现某几个小项目之</p>
<p>间需要共享一些信息，或者想使用session来实现SSO(single sign on)，在session中保存login的用户信息，最自然的要求是应用程序间能够访问彼此的session。</p>
<p>　　然而按照Servlet规范，session的作用范围应该仅仅限于当前应用程序下，不同的应用程序之间是不能够互相访问对方的session的。各个应用服务器从实际效果上都遵守</p>
<p>了这一规范，但是实现的细节却可能各有不同，因此解决跨应用程序session共享的方法也各不相同。</p>
<p>　　首先来看一下Tomcat是如何实现web应用程序之间session的隔离的，从Tomcat设置的cookie路径来看，它对不同的应用程序设置的cookie路径是不同的，这样不同的应用</p>
<p>程序所用的session id是不同的，因此即使在同一个浏览器窗口里访问不同的应用程序，发送给服务器的session id也可以是不同的。</p>
<p>&nbsp;</p>
<p>　　根据这个特性，我们可以推测Tomcat中session的内存结构大致如下。</p>
<p>&nbsp;</p>
<p>　　笔者以前用过的iPlanet也采用的是同样的方式，估计SunONE与iPlanet之间不会有太大的差别。对于这种方式的服务器，解决的思路很简单，实际实行起来也不难。要么</p>
<p>让所有的应用程序共享一个session id，要么让应用程序能够获得其他应用程序的session id。</p>
<p>　　iPlanet中有一种很简单的方法来实现共享一个session id，那就是把各个应用程序的cookie路径都设为/（实际上应该是/NASApp，对于应用程序来讲它的作用相当于根）</p>
<p>。</p>
<p>/NASApp </p>
<p><br />
　　需要注意的是，操作共享的session应该遵循一些编程约定，比如在session attribute名字的前面加上应用程序的前缀，使得setAttribute("name", "neo")变成</p>
<p>setAttribute("app1.name", "neo")，以防止命名空间冲突，导致互相覆盖。</p>
<p><br />
　　在Tomcat中则没有这么方便的选择。在Tomcat版本3上，我们还可以有一些手段来共享session。对于版本4以上的Tomcat，目前笔者尚未发现简单的办法。只能借助于第三</p>
<p>方的力量，比如使用文件、数据库、JMS或者客户端cookie，URL参数或者隐藏字段等手段。</p>
<p>　　我们再看一下Weblogic Server是如何处理session的。</p>
<p>&nbsp;</p>
<p><br />
　　从截屏画面上可以看到Weblogic Server对所有的应用程序设置的cookie的路径都是/，这是不是意味着在Weblogic Server中默认的就可以共享session了呢？然而一个小</p>
<p>实验即可证明即使不同的应用程序使用的是同一个session，各个应用程序仍然只能访问自己所设置的那些属性。这说明Weblogic Server中的session的内存结构可能如下</p>
<p>&nbsp;</p>
<p>　　对于这样一种结构，在session机制本身上来解决session共享的问题应该是不可能的了。除了借助于第三方的力量，比如使用文件、数据库、JMS或者客户端cookie，URL</p>
<p>参数或者隐藏字段等手段，还有一种较为方便的做法，就是把一个应用程序的session放到ServletContext中，这样另外一个应用程序就可以从ServletContext中取得前一个应</p>
<p>用程序的引用。示例代码如下，</p>
<p>　　应用程序A</p>
<p>context.setAttribute("appA", session); </p>
<p>　　应用程序B</p>
<p>contextA = context.getContext("/appA");<br />
HttpSession sessionA = (HttpSession)contextA.getAttribute("appA"); </p>
<p>　　值得注意的是这种用法不可移植，因为根据ServletContext的JavaDoc，应用服务器可以处于安全的原因对于context.getContext("/appA");返回空值，以上做法在</p>
<p>Weblogic Server 8.1中通过。</p>
<p>　　那么Weblogic Server为什么要把所有的应用程序的cookie路径都设为/呢？原来是为了SSO，凡是共享这个session的应用程序都可以共享认证的信息。一个简单的实验就</p>
<p>可以证明这一点，修改首先登录的那个应用程序的描述符weblogic.xml，把cookie路径修改为/appA访问另外一个应用程序会重新要求登录，即使是反过来，先访问cookie路径</p>
<p>为/的应用程序，再访问修改过路径的这个，虽然不再提示登录，但是登录的用户信息也会丢失。注意做这个实验时认证方式应该使用FORM，因为浏览器和web服务器对basic认</p>
<p>证方式有其他的处理方式，第二次请求的认证不是通过session来实现的。具体请参看[7] secion 14.8 Authorization，你可以修改所附的示例程序来做这些试验。</p>
<p>　　八、总结</p>
<p>　　session机制本身并不复杂，然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器，服务器的经验当作普遍适</p>
<p>用的经验，而是始终需要具体情况具体分析。 <br />
(http://www.fanqiang.com)</p>
<p><br />
&nbsp;</p>
<p>&nbsp;<br />
&nbsp;&nbsp; <br />
&nbsp;相关文章 </p>
<p>&nbsp;</p>
<img src ="http://www.blogjava.net/masen/aggbug/180875.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/masen/" target="_blank">Masen</a> 2008-02-20 15:48 <a href="http://www.blogjava.net/masen/articles/180875.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>数字签名和哈希函数</title><link>http://www.blogjava.net/masen/articles/180703.html</link><dc:creator>Masen</dc:creator><author>Masen</author><pubDate>Tue, 19 Feb 2008 08:15:00 GMT</pubDate><guid>http://www.blogjava.net/masen/articles/180703.html</guid><wfw:comment>http://www.blogjava.net/masen/comments/180703.html</wfw:comment><comments>http://www.blogjava.net/masen/articles/180703.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/masen/comments/commentRss/180703.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/masen/services/trackbacks/180703.html</trackback:ping><description><![CDATA[<p align="center">　　 <span class="da"><strong>数字签名和哈希函数</strong></span></p>
<strong>问题的提出</strong><br />
　　 懂得一点公钥密码基础知识的人都知道，发信息的人用自己的私钥对所发信息进行加密( Encryption )，接收信息者用发信者的公钥来解密( Decryption )，就可以保证信息的真实性、完整性和不可否认性。（注：这里提到的加密、解密是指密码运算，其目的并非信息保密。）那么，我们也可以笼统地说，以上方法就已经达到了数字签名的目的。因为首先，私钥是发信者唯一持有的，别的任何人不可能制造出这份密文来，所以可以相信这份密文以及对应的明文不是伪造的（当然，发信者身份的确定还要通过数字证书来保证）；出于同样原因，发信者也不能抵赖、否认自己曾经发过这份信息；另外，信息在传输当中不可能被篡改，因为如果有人试图篡改，密文就解不出来。这样，用私钥加密，公钥解密的技术方法就可以代替传统签名、盖章，保证了信息的真实性、完整性和不可否认性。 <br />
　　 但是，这样做在实际使用中却存在一个问题：要发的信息可能很长，非对称密码又比较复杂，运算量大，而为了保证安全，私钥通常保存在USB Key或IC卡中，加密运算也是在Key或卡中进行。一般来说，小小的USB Key或IC卡中的微处理器都做得比较简单而处理能力较弱，这样，加密所用的时间就会很长而导致无法实用。<br />
　　 另外，即使对于网站服务器而言，虽然它的处理能力很强，但服务器要同时处理许许多多签名加密的事情，也同样存在着加密耗时长系统效率低的问题。<br />
　　 有没有解决这个问题的办法呢？有的，常用的方法是使用哈希函数。<br />
<br />
<strong>什么是哈希函数</strong><br />
　　 哈希（Hash）函数在中文中有很多译名，有些人根据Hash的英文原意译为&#8220;散列函数&#8221;或&#8220;杂凑函数&#8221;，有些人干脆把它音译为&#8220;哈希函数&#8221;，还有些人根据Hash函数的功能译为&#8220;压缩函数&#8221;、&#8220;消息摘要函数&#8221;、&#8220;指纹函数&#8221;、&#8220;单向散列函数&#8221;等等。 <br />
1、Hash算法是把任意长度的输入数据经过算法压缩，输出一个尺寸小了很多的固定长度的数据，即哈希值。哈希值也称为输入数据的数字指纹（Digital Fingerprint）或消息摘要（Message Digest）等。Hash函数具备以下的性质：<br />
2、给定输入数据，很容易计算出它的哈希值；<br />
3、反过来，给定哈希值，倒推出输入数据则很难，计算上不可行。这就是哈希函数的单向性，在技术上称为抗原像攻击性； <br />
4、给定哈希值，想要找出能够产生同样的哈希值的两个不同的输入数据，（这种情况称为碰撞，Collision），这很难，计算上不可行，在技术上称为抗碰撞攻击性； <br />
5、哈希值不表达任何关于输入数据的信息。<br />
<br />
　　 哈希函数在实际中有多种应用，在信息安全领域中更受到重视。从哈希函数的特性，我们不难想象，我们可以在某些场合下，让哈希值来&#8220;代表&#8221;信息本身。例如，检验哈希值是否发生改变，借以判断信息本身是否发生了改变。` <br />
<br />
<strong>怎样构建数字签名</strong><br />
　　 好了，有了Hash函数，我们可以来构建真正实用的数字签名了。 <br />
　　 发信者在发信前使用哈希算法求出待发信息的数字摘要，然后用私钥对这个数字摘要，而不是待发信息本身，进行加密而形成一段信息，这段信息称为数字签名。发信时将这个数字签名信息附在待发信息后面，一起发送过去。收信者收到信息后，一方面用发信者的公钥对数字签名解密，得到一个摘要H；另一方面把收到的信息本身用哈希算法求出另一个摘要H&#8217;，再把H和H&#8217;相比较，看看两者是否相同。根据哈希函数的特性，我们可以让简短的摘要来&#8220;代表&#8221;信息本身，如果两个摘要H和H&#8217;完全符合，证明信息是完整的；如果不符合，就说明信息被人篡改了。 <br />
　　 数字签名也可以用在非通信，即离线的场合，同样具有以上功能和特性。 <br />
　　 由于摘要一般只有128位或160位比特，比信息本身要短许多倍，USB Key或IC卡中的微处理器对摘要进行加密就变得很容易，数字签名的过程一般在一秒钟内即可完成。<br />
<img height="360" src="http://www.cfca.com.cn/zhishi/image/wz-002-001.gif" width="480"  alt="" /><br />
<strong>哈希函数的安全性</strong><br />
　　 哈希函数的安全性直接关系到数字签名的安全性，如果哈希函数被攻破，数字签名的有效性就会受到质疑。 <br />
　　 目前，已经发明的Hash函数有多种，如Snefru、N-Hash、LOKI、AR、GOST、MD、SHA等。它们在数学上实现的方法各有不同，安全性也各有不同。目前比较常用的Hash函数是MD5和SHA-1。 MD5哈希函数以512位来处理输入数据，每一分组又划分为16个32位的子分组。算法的输出由4个32位分组组成，将它们级联起来，形成一个128位的固定长度的哈希值，即输入数据的摘要。SHA-1哈希函数在MD4的基础上增加了数学运算的复杂程度，即SHA=MD4+扩展转换+附加轮+更好的雪崩效应（哈希值中，为0的比特和为1的比特，其总数应该大致相等；输入数据中一个比特的变化，将导致哈希值中一半以上的比特变化，这就叫做雪崩效应）。SHA能够产生160位的哈希值。对SHA还没有已知的密码攻击，并且由于它产生的哈希值位数长于MD5，所以它能更有效地抵抗穷举攻击（包括生日攻击）。<br />
<br />
　　 但是，任何一种算法都有其漏洞和局限性。任何一个哈希函数都会存在碰撞——即在一些特定情况下，两个不同的文件或信息会指向同一个数字摘要。在一般情况下，类似碰撞只能尽可能地减少，而不能完全避免。从理论上讲，没有攻不破的密码。随着密码科学的发展，也许会找到攻破某一种密码算法的途径。<br />
<br />
　　 评价Hash算法的一个最好方法是看敌手找到一对碰撞消息所花的代价有多高。一般地，假设攻击者知道Hash算法，攻击者的主要攻击目标是找到一对或更多对碰撞消息。目前已有一些攻击Hash算法和计算碰撞消息的方法。在这些方法中，有些是一般的方法，可用于攻击任何类型的Hash算法，比如&#8220;生日攻击&#8221;；而另一些是特殊的方法，只能用于攻击某些特殊的Hash算法，比如适合于攻击具有分组链结构Hash算法的&#8220;中间相遇攻击&#8221;，适用于攻击基于模运算的Hash函数的&#8220;修正分组攻击&#8221;。坚固的哈希函数可通过设计有效的碰撞处理机制，或增加数字摘要的位数来增加复杂度，以减少碰撞出现的概率，<br />
<br />
　　 2004年8月17日，在美国召开的国际密码学会议（Crypto&#8217; 2004）上，一些国家的密码学者作了破译Hash函数的新进展的报告，其中我国山东大学的王小云教授做了破译MD5、HAVAL-128、MD4、和RIPE MD算法的报告。<br />
<br />
　　 到2005年2月，据王小云教授的研究报告，他们已经研究出了搜索SHA-1碰撞的一系列新技术。他们的分析表明，SHA-1的碰撞能在小于2^69次Hash操作中找到。对完整的80轮SHA-1的攻击，这是第一次在小于2^80次Hash操作这个理论界限的情况下找到碰撞。根据他们的估计，对于缩减到70轮的SHA-1能够用现在的超级计算机找出&#8220;实碰撞&#8221;。他们的研究方法，能自然地运用到SHA-0和缩减轮数的SHA-1的破译分析上。<br />
<br />
　　 2005年3月6日，Arjen Lenstra，王小云，Benne de Weger 宣布，他们构造出一对基于MD5 Hash函数的X.509证书，产生了相同的签名。他们提出了一种构造X.509证书的方法，在他们所构造出的证书对中，由于使用了MD5算法，签名部分产生了碰撞。因此，当证书发布者使用MD5作为Hash函数时，发布者就会在证书中产生相同的签名，导致PKI的基础原理遭到可信性破坏。这意味着，从单独某个证书无法确定是否存在另一个不同证书有着相同的签名。由于第二个相同签名证书存在的可能性，证书发布机构无法验证私钥的&#8220;拥有证明&#8221;，即无法验证证书中的签名。因此，使用&#8220;基于MD5函数&#8221;公钥证书的任何一方都无法确保所谓的证书拥有者是否真实拥有相应的私钥。<br />
<br />
　　 他们也想构造一对基于SHA-1的X.509证书，产生相同的签名。然而，他们还做不到这一点。因为产生SHA-1碰撞还需要相当长一段时间的研究。<br />
<br />
　　 专家指出：A.Lenstra和王小云等人声称已经成功地构造了两张符合X．509证书数据结构，拥有同样签名而内容却不同的证书，但该构造方法对证书的部分域要有特殊安排，签名算法RSA的密钥也是按照特殊规律生成的，要用来攻击某个实际应用的电子签名系统仍需时日。而对于SHA-1算法，说其从理论上被破解都还为时过早，只能说其破解工作取得了重大突破，破解所需要运算次数已从原来设计时估算的2^80次降低为2^69次，这比穷举法快了２０４８倍，但2^69次运算需要６０００年左右的时间，在实际计算上仍然是不可行的。<br />
<br />
　　 除了运算方面的瓶颈外，哈希函数的不可逆性决定了攻击者无法轻易得手，没有人可以保证通过这个发现的每个碰撞都是&#8220;可用&#8221;的碰撞。在漫长的运算后，你得到的也许包含一些有价值的信息，也许就是理论上存在的单纯碰撞，运算瓶颈和信息匮乏都会使黑客们的种种努力成为徒劳&#8230;&#8230;据业内人士估计，在当前的技术条件下，２^50或２^ ６０次运算量的范围内的攻击方法才会为我们带来麻烦，即引发实际意义上的攻击行为。在新研究成果发布前的一段时间内，SHA-1 算法只能被称作不完美，但还是安全的。基于PKI技术进行电子签名的最终用户，目前还不用担心自己的签名被伪造或遭遇签名人抵赖。<br />
<br />
　　 另外，安全专家强调：一种算法被破译，和整个企业的安全系统被攻破，是两个不同的概念。因为随着攻击技术和能力的提高，算法也会&#8220;水涨船高&#8221;，向前发展进步。王教授所取得的成就提醒密码学家研究新的算法，提醒有关标准化机构要提前修改算法标准，也提醒有关CA和电子签名产品开发商支持新的算法。当然，有些完全基于摘要算法的密押系统和电子货币系统，还需要尽早考虑替换方案。<br />
<br />
　　 美国国家技术与标准局（NIST）曾经发表如下评论：&#8220;研究结果说明SHA-1的安全性暂时没有问题，但随着技术的发展，技术与标准局计划在2010年之前逐步淘汰SHA-1，换用其他更长更安全的算法（如：SHA-224, SHA-256, SHA-384和SHA-512）来代替。&#8221;<br />
<img src ="http://www.blogjava.net/masen/aggbug/180703.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/masen/" target="_blank">Masen</a> 2008-02-19 16:15 <a href="http://www.blogjava.net/masen/articles/180703.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>定制自己的WebLogic LDAP Authentication Provider</title><link>http://www.blogjava.net/masen/articles/179174.html</link><dc:creator>Masen</dc:creator><author>Masen</author><pubDate>Sun, 03 Feb 2008 07:17:00 GMT</pubDate><guid>http://www.blogjava.net/masen/articles/179174.html</guid><wfw:comment>http://www.blogjava.net/masen/comments/179174.html</wfw:comment><comments>http://www.blogjava.net/masen/articles/179174.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/masen/comments/commentRss/179174.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/masen/services/trackbacks/179174.html</trackback:ping><description><![CDATA[<span class="h1b">定制自己的WebLogic LDAP Authentication Provider</span><br />
<br />
<table cellspacing="0" cellpadding="0" width="100%" border="0">
    <tbody>
        <tr>
            <td height="64">时间：2005-04-20<br />
            作者：<a href="http://dev2dev.bea.com.cn/author/55.html">马晓强</a><br />
            浏览次数： <script language="JavaScript" src="/beadevcount.jsp?d_id=161704" type="text/JavaScript"></script>158322 <br />
            本文关键字：</td>
            <td>
            <table class="box_content" cellspacing="0" cellpadding="0" border="0">
                <tbody>
                    <tr>
                        <td><span class="h2b">文章工具</span><br />
                        <img height="10" alt="推荐给朋友" src="http://dev2dev.bea.com.cn/images/letter001.gif" width="19" align="absMiddle" />&nbsp;<a href="javascript:sendmail()">推荐给朋友</a><br />
                        <img height="18" alt="打印文章" src="http://dev2dev.bea.com.cn/images/print001.gif" width="19" align="absMiddle" />&nbsp;<a href="javascript:window.print()">打印文章</a></td>
                    </tr>
                </tbody>
            </table>
            </td>
        </tr>
    </tbody>
</table>
<!-- 提取技术文章 -->
<div class="beas"><img height="1" alt="" src="http://dev2dev.bea.com.cn/images/dot6B6B6B.gif" width="100%" /></div>
目录
<p><a href="http://dev2dev.bea.com.cn/techdoc/200504502.html#1">1 J2EE Security 和 LDAP Security</a><br />
<a href="http://dev2dev.bea.com.cn/techdoc/200504502.html#2">2 JAAS和WebLogic Security Framework</a><br />
<a href="http://dev2dev.bea.com.cn/techdoc/200504502.html#3">3 了解WebLogic LDAP Authentication Provider</a><br />
<a href="http://dev2dev.bea.com.cn/techdoc/200504502.html#4">4 定制自己的Custom LDAP Authentication Provider</a><br />
<a href="http://dev2dev.bea.com.cn/techdoc/200504502.html#5">5 部署中的注意事项</a><br />
<a href="http://dev2dev.bea.com.cn/techdoc/200504502.html#6">6 结束语</a><br />
<a href="http://dev2dev.bea.com.cn/techdoc/200504502.html#7">7 参考资料</a></p>
<p>　　从WebLogic Server 7.0开始，WebLogic Server的安全机制有了全面的改变，实现了一个更加规范的基于JAAS的Security Framework，以及提供了一系列设计良好的Security Service Provider Interface。这样我们可以根据自己的具体需求，通过Custom Security Authentication Provider来实现安全上的定制功能。</p>
<p>　　本文将以WebLogic（WebLogic Server 8.1） Security和 LDAP为基础，介绍Custom LDAP Authentication Provider如何给我们带来更多的灵活性，和系统安全设计上更多的空间；以及讨论如何实现一个Custom LDAP Authentication Provider和部署过程中的一些良好经验。</p>
<p>　　由于本文涉及到的范围太广，不可能一一详细讨论；为了使没有相关基础的读者也能够阅读理解本文，因此我将在文章前半部分，试图通过最简洁扼要的描述，来使大家对于J2EE Security，WebLogic Security Framework以及LDAP 等有一个初步的清晰认识；进而可以开发出自己的LDAP Authentication Provider。因此很多地方做了比较有限的描述或者介绍，更多详细的内容可以参考文后附带的参考资料或者文中给出的链接。<a id="1" name="1"></a><br />
<br />
<strong>1 J2EE Security 和 LDAP Security</strong><br />
　　Sun J2EE推出以来，其安全部分的规范就一直倍受关注。我们最常见到安全规范的两个方面分别是Servlet Security 和 EJB Security。目前绝大多数的Servlet容器，J2EE容器都能很好的支持这些安全规范。</p>
<p>　　WebLogic Server作为业界领先的J2EE服务器对J2EE Security的支持是非常优秀的。我们这里将结合WebLogic Security和使用越来越广泛的LDAP做一个简要的介绍，这些是设计开发Custom LDAP Authentication Provider的技术基础。</p>
<p><strong>1.1 Authentication 和Authorization<br />
</strong>　　这里需要大家先明确安全上的两个重要名词：一个是认证（Authentication），一个是授权（Authorization）。认证是回答这个人是谁的问题，即完成用户名和密码的匹配校验；授权是回答这个人能做什么的问题。我们讨论的J2EE Security包括Declarative Authorization和Programmatic Authorization，即一个是通过web.xml，ejb-jar.xml等部署描述符中的安全声明通过容器提供的服务来完成权限控制的；一个是通过HttpServletRequest.isUserInRole()和EJBContext.isCallerInRole()这样的编程接口在应用中自己完成权限控制的。</p>
<p><strong>1.2 资源（Resource）和Security Role<br />
　　</strong>资源原本只包括 Web Resource和EJB Resource，但在WebLogic Security中扩展到几乎任何一个WebLogic Platform中的资源，具体可以参考<a href="http://e-docs.bea.com/wls/docs81/secwlres/types.html#1213777" target="_blank">http://e-docs.bea.com/wls/docs81/secwlres/types.html#1213777</a>。授权就是针对资源的访问控制。</p>
<p>　　J2EE Security是基于Security Role的。我们可以将一组资源与一个Security Role进行关联来达到控制的目的——只有拥有该Role权限的用户才能够访问这些资源。简单的说，我们可以通过给用户分配不同的Security Role来完成权限的控制。复杂的情况下包括用户/用户组，以及Principal和Role的映射关系等等。下面是一个声明性安全在web application（war包中WEB-INF/web.xml）中的示例：</p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>&lt;web-app&gt;<br />
            　&lt;security-constraint&gt;<br />
            　&lt;web-resource-collection&gt;<br />
            　　&lt;web-resource-name&gt;Success&lt;/web-resource-name&gt; <br />
            　　&lt;url-pattern&gt;/welcome.jsp&lt;/url-pattern&gt; <br />
            　　　&lt;http-method&gt;GET&lt;/http-method&gt; <br />
            　　　&lt;http-method&gt;POST&lt;/http-method&gt; <br />
            　&lt;/web-resource-collection&gt; <br />
            　&lt;auth-constraint&gt; <br />
            　　&lt;role-name&gt;webuser&lt;/role-name&gt; <br />
            　&lt;/auth-constraint&gt; <br />
            　&lt;/security-constraint&gt; <br />
            　&lt;login-config&gt; <br />
            　　&lt;auth-method&gt;BASIC&lt;/auth-method&gt; <br />
            　　&lt;realm-name&gt;default&lt;/realm-name&gt; <br />
            　&lt;/login-config&gt; <br />
            　&lt;security-role&gt; <br />
            　　&lt;role-name&gt;webuser&lt;/role-name&gt; <br />
            　&lt;/security-role&gt;<br />
            &lt;/web-app&gt;</td>
        </tr>
    </tbody>
</table>
<p>　　只有拥有角色webuser的用户才能够访问welcome.jsp页面，否则容器会返回401无权访问的错误。更多信息请参考<a href="http://e-docs.bea.com/wls/docs81/security/index.html" target="_blank">http://e-docs.bea.com/wls/docs81/security/index.html</a>。</p>
<p>　　同时我们需要在weblogic.xml（war包中WEB-INF/weblogic.xml）中对security role和principal进行映射关系的配置：</p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>&lt;weblogic-web-app&gt; <br />
            　&lt;security-role-assignment&gt; <br />
            　　&lt;role-name&gt;PayrollAdmin&lt;/role-name&gt; <br />
            　　&lt;principal-name&gt;Tanya&lt;/principal-name&gt; <br />
            　&lt;/security-role-assignment&gt;<br />
            &lt;/weblogic-web-app&gt;</td>
        </tr>
    </tbody>
</table>
<p>　　这样拥有Principal &#8220;Tanya&#8221;的用户（Principal将封装到Subject中，用户将和Subject关联）将会拥有PayrollAdmin的权限。</p>
<p>　　<em>注意：一般情况下为了简化设计，本文中将假设security role即是principal name（如果不配置security-role-assignment，WebLogic会默认做此假设）。即上例中Principal-name也为PayrollAdmin。</em></p>
<p><strong>1.3 LDAP Security<br />
　　</strong>LDAP是轻量级目录服务（Lightweight Directory Access Protocol）。越来越多的应用开始采用LDAP作为后端用户存储。在安全上，LDAP Security是基于ACL（Access Control List）的，它通过给一个用户组分配LDAP 操作资源（比如对一个子树的查询，修改等）来最终完成权限的控制。因此在LDAP中，授权工作是以用户组为单位进行的。一个用户组一般来说是拥有如下一组属性的LDAP Entry：</p>
<p align="center"><img height="165" src="http://dev2dev.bea.com.cn/images/webser/image2005041901.gif" width="500"  alt="" /><br />
图1-3-1</p>
<p>　　其中objectclass可以为groupOfUniqueNames或者groupOfNames，它们对应的组成员属性分别是uniquemember和member。如果是动态组，objectclass为groupOfURLs。动态组一般应用在成员可以通过某种业务逻辑运算来决定的情况下。比如，经理为ZHANGSAN的全部员工。下面是一个典型的动态组，memberURL属性定义了哪些entry属于该组：</p>
<p align="center"><img height="130" src="http://dev2dev.bea.com.cn/images/webser/image2005041902.gif" width="550"  alt="" /><br />
图1-3-2</p>
<p><strong>　　</strong>从图1-3-1中我们可以看出，用户WANTXIAOMING，ZHANGSAN，LISI属于组HR Managers。这种组和成员的关系是通过属性uniquemember来决定的。同时LADP Group 支持嵌套，即一个组可以是另外一个组的成员，比如我们将Accounting Managers组分配给HR Managers组作为其成员：</p>
<p align="center"><img height="185" src="http://dev2dev.bea.com.cn/images/webser/image2005041903.gif" width="500"  alt="" /><br />
图1-3-3</p>
<p><strong>　　</strong>这样将表示Accounting Managers中的成员，同时也是组HR Managers的成员。通过这种层级关系可以使权限分配变的更加灵活。</p>
<p><strong>　　</strong>下面是一些名词的解释，希望大家对LDAP有更好的理解：<br />
<strong>　　</strong>a) Objectclass —— LDAP对象类，抽象上的概念类似与一般我们理解的class。根据不同的objectclass，我们可以判断这个entry是否属于某一个类型。比如我们需要找出LDAP中的全部用户：(objectclass=person)再比如我们需要查询全部的LDAP组：(objectclass=groupOfUniqueNames)<br />
<br />
<strong>　　</strong>b) Entry —— entry可以被称为条目，或者节点，是LDAP中一个基本的存储单元；可以被看作是一个DN和一组属性的集合。 属性可以定义为多值或者单值。<br />
<br />
<strong>　　</strong>c) DN —— Distinguished Name，LDAP中entry的唯一辨别名，一般有如下的形式：uid=ZHANGSAN, ou=staff, ou=people, o=examples。LDAP中的entry只有DN是由LDAP Server来保证唯一的。</p>
<p><strong>　　</strong>d) LDAP Search filter ——使用filter对LDAP进行搜索。 Filter一般由 (attribute=value) 这样的单元组成，比如：(&amp;(uid=ZHANGSAN)(objectclass=person)) 表示搜索用户中，uid为ZHANGSAN的LDAP Entry．再比如：(&amp;(|(uid= ZHANGSAN)(uid=LISI))(objectclass=person))，表示搜索uid为ZHANGSAN, 或者LISI的用户；也可以使用*来表示任意一个值， 比如(uid=ZHANG*SAN)，搜索uid值以 ZHANG开头SAN结尾的Entry。更进一步，根据不同的LDAP属性匹配规则，可以有如下的Filter： (&amp;（createtimestamp&gt;=20050301000000）(createtimestamp&lt;=20050302000000))，表示搜索创建时间在20050301000000和20050302000000之间的entry。<br />
<strong>　　</strong>Filter中 &#8220;&amp;&#8221; 表示&#8220;与&#8221;；&#8220;!&#8221;表示&#8220;非&#8221;；&#8220;|&#8221;表示&#8220;或&#8221;。根据不同的匹配规则，我们可以使用&#8220;=&#8221;，&#8220;~=&#8221;，&#8220;&gt;=&#8221;以及&#8220;&lt;=&#8221;，更多关于LDAP Filter读者可以参考LDAP相关协议：<a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">http://www.ietf.org/rfc/rfc2254.txt</a>。</p>
<p><strong>　　</strong>e) Base DN —— 执行LDAP Search时一般要指定basedn，由于LDAP是树状数据结构，指定basedn后，搜索将从BaseDN开始，我们可以指定Search Scope为：只搜索basedn（base），basedn直接下级（one level），和basedn全部下级（sub tree level）。</p>
<p><strong>　　</strong>下面是一个典型的LDAP Tree结构，右侧显示Entry uid=ZHANGSAN, ou=staff, ou=people, o=examples的属性，该entry代表了一个名字叫张三的用户：</p>
<p align="center"><img height="328" src="http://dev2dev.bea.com.cn/images/webser/image2005041904.gif" width="600"  alt="" /><br />
图1-3-4<a id="2" name="2"></a></p>
<p><strong>2 JAAS和WebLogic Security Framework</strong><br />
<strong>　　</strong>现在越来越多的人开始了解JAAS，使用JAAS。WebLogic Security Framework就是基于JAAS的。因此我们需要对此有一个非常准确的理解才能够设计开发Custom Authentication Provider。</p>
<p><strong>　　</strong>下面我们从几个名词入手，了解JAAS和 WebLogic Security Framework的关键之处：</p>
<p><strong>2.1 Principal，Subject和LoginModule<br />
　　</strong>a) Principal <br />
<strong>　　</strong>当用户成功验证后，系统将会生成与该用户关联的各种Principal。我们这里将Principal进行简化的设计，认为一个Principal就是用户登录帐号和它所属于的组（LDAP Group）。这样当用户登录成功后，我们将会在LDAP中执行搜索，找出用户属于哪些组，并将这些组的名字，或者其标识作为Principal返回。这样，当用户在LDAP中属于某一个组，并且这个组的名字对应到 web.xml （或者ejb-jar.xml）中的Security role，那么这个用户就可以看作拥有访问这个Security Role定义的资源的权限。<br />
<strong>　　</strong>在WebLogic Security Framework中，这个LDAP Group的名字（Principal）和Security Role的映射关系，可以通过一个 Role Mapping Provider来实现动态的匹配，即用户的动态权限控制。比如在运行时根据某一个业务逻辑来决定用户是否拥有某一个权限。关于Role Mapping Provider，读者可以参考下面链接的内容：<a href="http://e-docs.bea.com/wls/docs81/dvspisec/rm.html#1145542" target="_blank">http://e-docs.bea.com/wls/docs81/dvspisec/rm.html#1145542</a>。</p>
<p><strong>　　</strong>b) Subject<br />
<strong>　　</strong>JAAS规定由Subject封装用户以及用户认证信息，其中包括Principals。下面是WebLogic Security Framework中Subject的组成图示：</p>
<p align="center"><img height="385" src="http://dev2dev.bea.com.cn/images/webser/image2005041905.gif" width="500"  alt="" /><br />
图2-1-1</p>
<p><strong>　　</strong>这样当用户试图访问一个受限的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的访问权限。</p>
<p><strong>　　</strong>c) LoginModule<br />
<strong>　　</strong>JAAS LoginModule是一个Authentication Provider必须的组成部分。LoginModule是认证的核心引擎，它负责对用户身份进行验证，同时将返回与用户关联的Principals（用户登录帐号，以及LDAP Groups），然后放入Subject中，供后续的访问控制使用。<br />
<strong>　　</strong>我们将在LoginModules中完成LDAP的相关认证，查询操作，将用户在LDAP中所属于的组搜索出来，作为认证后的结果封装到Subject中返回。</p>
<p><strong>2.2 WebLogic Authentication认证过程</strong><br />
<strong>　　</strong>下面我们了解一下WebLogic的认证过程。以下图片来自<a href="http://e-docs.bea.com/wls/docs81/dvspisec/atn.html" target="_blank">http://e-docs.bea.com/wls/docs81/dvspisec/atn.html</a> 我将其中主要部分进行说明。</p>
<p align="center"><img height="452" src="http://dev2dev.bea.com.cn/images/webser/image2005041906.gif" width="600"  alt="" /><br />
图2-2-1</p>
<p><strong>　　</strong>Security Framework在WebLogic Server启动时初始化Authentication Provider（5）。当有认证请求进入时，Security Framework首先将通过AuthenticationProvider.getLoginModuleConfiguration()来获取一个AppConfigurationEntry对象。通过AppConfigurationEntry（详见<a href="http://java.sun.com/security/jaas/apidoc/javax/security/auth/login/AppConfigurationEntry.html" target="_blank">http://java.sun.com/security/jaas/apidoc/javax/security/auth/login/AppConfigurationEntry.html</a> ）可以初始化一个LoginModule。初始化LoginModule的方法为：public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options)，可以看到里面有Subject, CallbackHandler等等重要参数。<br />
<strong>　　</strong>被实例化的JAAS LoginModule将完成用户的一系列验证任务。验证完成后，在（6a）中principals将被Principal Validation Provider签名，在（ 6b）中存放到与用户关联的Subject里面。Security Framework最后将拿着该用户的Subject去完成其他权限控制等任务。<a id="3" name="3"></a><br />
<br />
<strong>3 了解WebLogic LDAP Authentication Provider</strong><br />
<strong>　　</strong>现在我们有信心了解一下WebLogic LDAP Authentication Provider的工作原理了。这里将以WebLogic提供的iPlanet Authentication Provider的配置为例进行说明。在这里也需要明确说明一下，为了方便进行描述，我们将实际属于LoginModule的行为也一并归结到Provider中。没有单独将两个的行为分开，目的是为了突出整个完整的过程。</p>
<p><strong>3.1 iPlanet Authentication Provider配置</strong></p>
<p align="center"><img height="454" src="http://dev2dev.bea.com.cn/images/webser/image2005041907.gif" width="600"  alt="" /><br />
图3-1-1</p>
<p><strong>　　</strong>从上图可以看出我们需要指定LDAP服务器的地址，端口，连接LDAP使用的Principal（不同于前面讨论的Principal，这个Principal实际是一个连接LDAP的用户，也就是一个LDAP 中用户Entry的 DN，它必须要有相关的LDAP 搜索等权限）和Credential（一般来说就是口令）。<br />
<strong>　　</strong>再看下面关于Users的配置：</p>
<p align="center"><img height="454" src="http://dev2dev.bea.com.cn/images/webser/image2005041908.gif" width="600"  alt="" /><br />
图3-1-2</p>
<p><strong>3.1.1 User Object Class </strong>—— 前面已经对objectclass进行过说明，指明LDAP Entry属于哪一类<br />
<strong>3.1.2 User Name Attribute </strong>—— 用户登录帐号在LDAP Entry中的属性，一般为UID或者cn<br />
<strong>3.1.3 User Base DN</strong> —— 所有的用户将会放置到这个子树下面，因此在Provider中对用户进行的搜索将会从这个basedn开始<br />
<strong>3.1.4 User Search Scope </strong>—— 指定搜索范围为Basedn的直接一级或者全部下级<br />
<strong>3.1.5 User From Name Filter</strong> —— 使用这个filter可以搜索出用户信息。其中%u将会被用户输入的登录帐号替换，从而查询中LDAP中的用户信息</p>
<p align="center"><img height="452" src="http://dev2dev.bea.com.cn/images/webser/image2005041909.gif" width="600"  alt="" /><br />
图3-1-3</p>
<p><strong>3.1.6 Group Base DN </strong>—— 从该Base DN开始搜索用户组<br />
<strong>3.1.7 Group From Name Filter</strong> —— %g将会被组的名字替换，通过该filter可以搜索出符合条件的LDAP Group<br />
<strong>3.1.8 Static group name attribute</strong> —— 组名字的属性，属性cn对应的值就是组的名字</p>
<p align="center"><img height="469" src="http://dev2dev.bea.com.cn/images/webser/image2005041910.gif" width="600"  alt="" /><br />
图3-1-4</p>
<p><strong>3.1.9 Static Member DN Attribute</strong> —— 静态成员属性，通过该属性可以判断一个Entry是否属于一个组<br />
<strong>3.1.10 Static Group DNs From Member DN Filter</strong> —— 通过该filter可以找出用户属于哪些组<br />
<strong>3.1.11 Dynamic Group</strong> —— 动态组是在运行时根据某种业务逻辑，来决定成员隶属关系的LDAP Group</p>
<p><strong>3.2 iPlanet Authentication Provider的工作原理</strong><br />
<strong>　　</strong>从上面配置的介绍中可以看出，后端存储为LDAP的情况下，在Console中我们需要配置的参数已经清楚的表明它所要完成工作的内容。<br />
<strong>　　</strong>首先，它使用配置的User BaseDN和Filter，来根据用户输入的登录帐号进行搜索，找出存放在LDAP中的用户Entry。如果找到一个用户，那么Provider就使用该用户的DN和用户输入的口令，进行验证。验证可以使用LDAP Bind和LDAP Compare，这需要根据不同LDAP的特点来进行选择。<br />
<br />
<strong><em>　　</em></strong><em>A. LDAP Bind —— 将当前的LDAP Connection绑定到一个用户身份上。这样后续的使用该Connection的LDAP Operation都将以该身份进行。LDAP Bind需要两个重要参数，一个是用户Entry的DN，一个是该用户的口令。</em></p>
<p><em><strong>　　</strong>B. LDAP Compare —— LDAP Compare是一个为兼容X.500的古老操作，它用于检查一个属性值是否包含在指定Entry中的属性里。这样我们可以在知道用户password存放在哪个属性的前提下，对该属性进行compare。如果成功，表明口令正确。如果属性值为散列后的口令（绝大多数情况），有的LDAP Server支持这样的验证，有的不支持，比如iPlanet LDAP Server 5。</em></p>
<p><strong>　　</strong>验证成功后，Provider将使用Console中关于Groups和Memberships中的配置，查找用户属于哪些LDAP Group，而且由于这些组本身可能会有一些嵌套，因此对于搜索到的组还需要进行查询。即使用filter： (&amp;（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可以自动检测出，有的需要我们程序来判断）。<br />
<br />
<strong>　　</strong>然后将用户登录的帐号，用户所属组的名字（属性CN的值或其他），放入Subject中。最后调用Principal Validator Provider，对Subject中的principals进行签名，来表明该Subject是这个Provider生成的。这样防止其他攻击者伪造Subject以及Principal进行欺骗。此处也可以解释为何在不同的WLS Domain间不能够传递Subject，我们可以通过设置域信任来完成这种Subject的传递。设置域信任使用的Credential就是签名用的Key。<br />
<br />
<strong>　　</strong>图3-1-5表明了如何设置WebLogic Domain Credential，默认情况下WebLogic Server会在启动的时候随即生成一个Credentials（在WLS6.1时，这个值就是system用户的口令）：</p>
<p align="center"><img height="307" src="http://dev2dev.bea.com.cn/images/webser/image2005041911.gif" width="600"  alt="" /></p>
<p><strong>　　</strong>可以想见如果我们实现自己的Principal Validator Provider，让它去一个集中的验证服务器中对Subject进行签名，或者验证Subject，这样就可以实现域信任，进而完成Application（EJB Tier）层的SSO。</p>
<p><strong>　　</strong>通过以上的讨论，我们对于实现自己的LDAP Authentication Provider是不是又增加了一份信心？<a id="4" name="4"></a><br />
<strong><br />
4 定制自己的Custom LDAP Authentication Provider</strong><br />
<strong>　　</strong>为何要定制自己的Authentication Provider? 由于WebLogic Server已经提供了很多默认的Authentication Provider在一般情况下我们确实没有必要实现自己的Provider。但是面对某些针对安全方面的复杂需求时，WebLogic Server提供的Provider很有可能不满足这些需求，此时就需要我们定制自己的Provider。</p>
<p><strong>　　</strong>在这一章的开头部分中我需要简要讨论关于WebLogic MBean Types，以及WebLogic Console扩展等内容，目的在于让读者了解到我们通过WebLogic Console可以完成对Custom Security Provider的配置和部署，我将以WebLogic 提供的Sample Security Provider为示例进行说明。详细的信息可以参考以下的一些资源：</p>
<p><a href="http://e-docs.bea.com/wls/docs81/dvspisec/atn.html#1106272" target="_blank">http://e-docs.bea.com/wls/docs81/dvspisec/atn.html#1106272</a><br />
<a href="http://e-docs.bea.com/wls/docs81/dvspisec/atn.html#1106241" target="_blank">http://e-docs.bea.com/wls/docs81/dvspisec/atn.html#1106241</a></p>
<p><strong>　　</strong>上面两个链接描述了如何创建MBean Types以及在控制台上配置Custom Authentication Provider.下面这个链接中专门介绍了WebLogic Console的扩展：</p>
<p><a href="http://dev2dev.bea.com.cn/techdoc/2005012102.html" target="_blank">/techdoc/2005012102.html</a></p>
<p><strong>　　</strong>读者可以从<a href="http://dev2dev.bea.com/codelibrary/code/security_prov81.jsp" target="_blank">http://dev2dev.bea.com/codelibrary/code/security_prov81.jsp</a>下载WLS提供的Sample Security Provider。</p>
<p><strong>4.1 MBean Types和WebLogic Console</strong><br />
<strong>　　</strong>一般情况下，人们可能更习惯通过WebLogic Console对Security Provider进行配置。这里我将简要描述这个过程，以及可以到达的一个效果。限于篇幅就不详细讨论了。</p>
<p><strong>　　</strong>从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：</p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>&lt;MBeanAttribute<br />
            <strong>　　</strong>Name = "LDAPServerIP" <br />
            <strong>　　</strong>Type = "java.lang.String" <br />
            <strong>　　</strong>Writeable = "true" <br />
            <strong>　　</strong>Default = "&amp;quot;127.0.0.1&amp;quot;" <br />
            /&gt;</td>
        </tr>
    </tbody>
</table>
<p><strong>　　</strong>这样在Provider中我们将通过MbeanMaker(WLS提供)生成的SimpleSampleAuthenticatorMBean中取到这个属性：SimpleSampleAuthenticatorMBean.getLDAPServerIP()。MBean将在初始化Provider的时候作为参数传入。这样我们就可以通过MBean中的参数控制Provider的行为。</p>
<p><strong>　　</strong>当然这个参数是可以在WebLogic Console中设置的，通过对MBean Types的扩展，在WebLogic Console上看到的画面如下：</p>
<p align="center"><img height="210" src="http://dev2dev.bea.com.cn/images/webser/image2005041912.gif" width="600"  alt="" /><br />
图4-1-1</p>
<p><strong>　　</strong>这样我们可以通过Console修改配置参数（修改的Security Provider参数将保存在config.xml中，默认的值将保存在MBean Jar File中）。</p>
<p><strong>4.2 为何定制LDAP Authentication Provider<br />
　　</strong>当我们面临越来越复杂的安全方面的业务需求时，或者面临较高的性能要求，需要根据目标LDAP做针对性的优化时，或者需要将我们已有的认证，或授权模块集成到WebLogic平台时，WebLogic提供的现成的Provider往往不能满足我们的需求。</p>
<p><strong>4.2.1 复杂的业务需求<br />
</strong><strong>　　</strong>当系统要求用户不仅仅输入用户名（j_username），口令（j_password），还需要输入其他信息，比如登录的地点，系统的名字，用户的类型等等。如果是采用基于J2EE Form的验证方式， 登录信息需要提交到j_security_check（Servlet规范定义由容器负责实现的Servlet），导致我们没法处理更多的信息。</p>
<p><strong>　　</strong>这个时候，如果能够实现我们自己的 Authentication Provider，那么我们就可以通过TextInputCallback来获取登录表单中更多的信息了；进而通过这些信息在Provider中完成符合我们需要的处理。</p>
<p><strong>　　</strong>比如搜狐的登录页面上需要选择用户的类型：</p>
<p align="center"><img height="176" src="http://dev2dev.bea.com.cn/images/webser/image2005041913.gif" width="158"  alt="" /><br />
图4-2-1</p>
<p><strong>4.2.2 性能需求或者调优</strong><br />
　　有时，有的用户会比较困惑为何WebLogic LDAP Security Provider在压力测试中的表现不很理想，用户需要较长时间的等待，才能够登录到系统中。由于这些Provider是 WebLogic产品的一部分，因此缺乏对不同目标LDAP Server的有针对性的优化。这样就使得我们无法充分发挥具体LDAP Server的性能调优。</p>
<p>　　比如，有的LDAP Server支持动态组（LDAP Dynamic Group，成员关系是运行时根据ldap server，basedn，filter等动态决定的），可以使用如下的Filter查询用户属于哪些动态组：</p>
<p>　　(uniquemember=uid=MAXQ,ou=staff,ou=people,o=examples)</p>
<p>　　有的LDAP Server虽然支持动态组，但是支持的有限，不能使用上面的Filter获取用户属于哪些动态组。在WebLogic iPlanet Authentication Provider的实现中，它先是搜索出全部的动态组，然后再遍历这些动态组，依次去LDAP中检查用户是否属于一个组；很明显，这样虽然最大程度的满足了不同LDAP Server的要求（从产品的角度讲可能是必须的），但是与LDAP交互的次数大增，并发用户量一大性能下降的比较明显。</p>
<p>　　此时，如果系统中的LDAP支持上面的Filter或者有更好的搜索方式，那么完全可以通过定制Provider完成对性能的优化。</p>
<p><strong>4.2.3 已有权限控制的集成</strong><br />
　　如果系统中已经存在了现成的满足需求的认证模块，并且已经很好的工作；在系统转向J2EE架构，并使用WebLogic Server做J2EE容器时，我们可能会更愿意直接在Provider中加入这个认证模块。</p>
<p>　　综上，我只是列举了一些可以驱动我们开发自己Provider的需求，相信在读者实际工作中可能会面临更复杂的情况，开发自己的Provider将是一个非常好的选择。</p>
<p><strong>4.3 LDAP Authentication Provider实现</strong><br />
　　本文之前为了表述的方便没有单独提到LoginModule，认为LoginModule的行为就是LDAP Authentication Provider的行为。到了目前的具体实现阶段，我们必须分开Authentication Provider和JAAS LoginModule。最终部署到WebLogic上的实际只是LDAP AuthenticationProvider Implements。</p>
<p>　　WebLogic SecurityFramework通过Authentication Provider获取具体的JAAS LoginModule。通过LoginModule完成最终登录的工作。因此我们必须先实现一个AuthenticationProvider。</p>
<p>　　我们一般通过weblogic.security.spi.AuthenticationProvider 来实现自己的AuthenticationProvider。这里介绍其中的几个重要方法：</p>
<p>　　a) public void initialize(ProviderMBean mbean, SecurityServices services)<br />
初始化一个Provider。通过参数MBean我们可以获取到在WebLogic Console中配置的各项参数。进而初始化我们的Provider，然后通过Provider传递到LoginModule中。</p>
<p>　　b) public void shutdown()<br />
释放一些与Provider，LoginModule等相关的资源。</p>
<p>　　c) public AppConfigurationEntry getLoginModuleConfiguration()<br />
这个方法非常重要，通过该方法，WebLogic Security Framework可以获取用于初始化LoginModule的AppConfigurationEntry。AppConfigurationEntry中存放了LoginModule的类名等信息，比如使用如下代码返回一个AppConfigurationEntry:</p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>　　return new AppConfigurationEntry(<br />
            　　　　"examples.security.providers.authentication.SampleLoginModuleImpl",<br />
            　　　　controlFlag,<br />
            　　options);</td>
        </tr>
    </tbody>
</table>
<p>　　其中LoginModule Name就</p>
<p>是"examples.security.providers.authentication.SampleLoginModuleImpl"，我们通过它就可以实例化一个LoginModule并通过LoginModule.initialize()方法进行初始化。</p>
<p>　　d) public AppConfigurationEntry getAssertionModuleConfiguration()<br />
该方法将返回一个与Identity Assertion Provider关联的LoginModule。这个Assertion LoginModule，将只会验证用户是否存在，以及如果存在返回用户的Principals。 该方法也比较重要，需要正确实现，比如我们使用CLIENT-CERT这种WEB认证方式，该方法就会被调用。</p>
<p>　　Provider的实现比较简单，读者可以在<a href="http://dev2dev.bea.com/codelibrary/code/security_prov81.jsp" target="_blank">http://dev2dev.bea.com/codelibrary/code/security_prov81.jsp</a>下载WebLogic提供的Samples，查看SampleAuthenticationProviderImpl的代码。</p>
<p>&nbsp;</p>
<p><strong>4.4 LDAP LoginModule 逻辑流程<br />
</strong>　　实现了Provider后，必须拥有我们自己的LDAP LoginModule。下面是一个简单的用于演示的验证逻辑流程图。实际的一个LoginModule由于不同的业务需求，情况可能会复杂得多。这里只是描述了最核心最基本的逻辑，使读者能有一个清晰的思路。后面我将以这个流程为例进行实现。</p>
<p align="center"><img height="743" src="http://dev2dev.bea.com.cn/images/webser/image2005041914.gif" width="356"  alt="" /></p>
<p><strong>4.5 LoginModule代码示例和讲解<br />
</strong>　　这里我将使用Netscape LDAP SDK for java作为开发工具实现LDAP相关的操作，读者可以到<a href="http://docs.sun.com/db/doc/816-6402-10" target="_blank">http://docs.sun.com/db/doc/816-6402-10</a> 下载开发手册，从<a href="http://www.mozilla.org/directory/" target="_blank">http://www.mozilla.org/directory/</a> 下载SDK 包。一般来说还可以通过JNDI来操作LDAP，我个人认为Sun LDAP JNDI Provider中关于Connection Pool的实现非常优秀。但不管使用哪种SDK，对LDAP的编程原理上基本都是相同的（因为基于同样的LDAP协议），不同的可能仅仅是接口，类的名字而已。</p>
<p><strong>4.5.1 初始化Connection Pool</strong><br />
　　为了有效使用宝贵，并且有限的LDAP连接，必须使用连接池。下面的代码初始化了一个LDAP连接池：</p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>/** <br />
            * <br />
            */ <br />
            static ConnectionPool pool; <br />
            /** * * @throw LDAPException <br />
            */ public LdapClient() <br />
            　　throws LDAPException <br />
            　　　　{ <br />
            　　　　　　pool= new ConnectionPool( <br />
            　　　　　　　　1, 150, "127.0.0.1", 389, "cn=Directory Manager", "88888888"); <br />
            }</td>
        </tr>
    </tbody>
</table>
<p>　　Sun JNDI LDAP Service Provider（JDK1.4）中可以通过在环境变量中设置具体的参数来启用连接池，达到复用连接的目的。具体可以参考链接：<a href="http://java.sun.com/products/jndi/tutorial/ldap/connect/index.html" target="_blank">http://java.sun.com/products/jndi/tutorial/ldap/connect/index.html</a>，下面是示例代码：<br />
<br />
Hashtable env= new Hashtable();<br />
&#8230;<br />
env.put("com.sun.jndi.ldap.connect.pool", "true");<br />
DirContext ctx= new InitialDirContext( env);</p>
<p><strong>4.5.2 根据用户输入的登录帐号，搜索用户Entry<br />
</strong>　　下面这个方法实现了从LDAP中搜索用户Entry DN的最简单的过程。实际上我们可以在其中实现很多定制的功能。比如允许用户使用多于一个的帐号登录，只要这些帐号能够通过LDAP Searche最终返回一个唯一的用户DN即可。</p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>&nbsp; * @return<br />
            &nbsp;&nbsp; * @throws LDAPException <br />
            &nbsp;&nbsp; */<br />
            &nbsp; public String getUserDN( String uid) throws LDAPException{ <br />
            &nbsp;&nbsp;&nbsp; LDAPConnection conn= pool.getConnection();<br />
            &nbsp;&nbsp;&nbsp; try {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; String[] attrs= new String[]<br />
            {}; <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LDAPSearchResults sr= conn.search("o=examples", 2, "(uid="+ uid +")", attrs, false);<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ( sr.hasMoreElements()) { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LDAPEntry entry= sr.next();<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return entry.getDN();<br />
            }<br />
            &nbsp;&nbsp;&nbsp; throw new LDAPException("No Such Object:"+ uid, <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LDAPException.NO_SUCH_OBJECT);<br />
            &nbsp;&nbsp;&nbsp; }catch ( LDAPException ex) { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; throw ex;<br />
            &nbsp;&nbsp;&nbsp; }finally {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; try {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (conn!= null) pool.close(conn);<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }catch ( Exception ex)<br />
            {}<br />
            &nbsp;&nbsp;&nbsp; }<br />
            &nbsp; }</td>
        </tr>
    </tbody>
</table>
<p>　　首先需要从池中获取一个LDAP连接，然后使用LDAPConnection.search方法进行搜索。我这里以一个典型的LDAP Search接口为例进行说明，其他API比如JNDI等，基于同样的LDAP协议，接口中同样参数的含义都是相同的。</p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>public LDAPSearchResults search(java.lang.String base,<br />
            　　int scope,<br />
            　　java.lang.String filter,<br />
            　　java.lang.String[] attrs,<br />
            　　boolean attrsOnly)</td>
        </tr>
    </tbody>
</table>
<p>　　1) Base: 表明从该Basedn开始搜索，可以通过MBean获取</p>
<p>　　2) Scope: 搜索的范围：</p>
<p>　　　　a) LDAPv2.SCOPE_BASE， 只搜索basedn指定的entry<br />
　　　　b) LDAPv2.SCOPE_ONE, 在basedn的下一级entry中搜索，不包括basedn<br />
　　　　c) LDAPv3.SCOPE_SUB，在basedn的全部下级entry中搜索，包括basedn</p>
<p>　　3) Filter：过虑条件。比如uid=ZHANGSAN，搜索UID为ZHANGSAN的用户；再比如uid=ZHANG*，搜索 ZHANG开头的帐号；或者uid=Z*S，搜索以Z开头S结尾的帐号。前面已经介绍过这里就不多说了。</p>
<p>　　4) attrs：返回该attrs数组中指定的属性，比如new String[]{&#8220;uid&#8221;}，只返回属性uid，其他属性将会不在结果中返回。 一般来说我们会要求开发人员只将需要的属性返回，这样避免返回无用的属性，降低网络和Server等方面的资源开销；而且如果存在一个有较大属性集合的Entry，并且你并不使用到这个较大的属性集合。举个实际例子来说，比如你的系统中拥有很多成员数目超过2万或者更多的LDAP Groups，并且你希望通过LDAP Search找出某一个用户属于的组CN，那么搜索结果只返回组的CN已经可以满足你的要求了，这时就没有必要将全部member属性也返回了。这在后面我会有代码来说明。</p>
<p>　　这里还有一点很重要的细节，相信对读者会有帮助。比如，你希望搜索到modifytimestamp等Operational Attributes。这些属性（LDAP Server自己负责维护的，用户无法修改的）必须要在参数attrs中指定，LDAP Server才会返回给客户端。如果用户希望返回全部User Attributes的同时，返回指定的 Operational Attributes那该怎么办？不在attrs列表中的属性将不会被返回，一旦我指定了attrs，是否需要将全部的属性都列在attrs参数中？实际上此时只需要传入 new String[]{ &#8220;*&#8221;, &#8220;createtimestamp&#8221;}这样的参数即可；&#8220;*&#8221;号即代表了全部的User Attributes。在Sun JNDI LDAP Service Provider中也是这样，尽管找遍Sun关于JNDI的文档也找不到这样的说明。LDAP协议对此的说明在 <a href="http://www.ietf.org/rfc/rfc2251.txt" target="_blank">http://www.ietf.org/rfc/rfc2251.txt</a> 第28页。</p>
<p>　　5) attrsOnly：只返回属性名称，不包含值。我们一般设置为false。</p>
<p>　　我们下面看一下Sun JNDI中关于LDAP Search的接口：</p>
<p>public NamingEnumeration search(String name,<br />
　　String filter,<br />
　　SearchControls cons)<br />
　　throws NamingException<br />
name参数就是上面的basedn，SearchControls中封装更多的设置，比如：SearchControls. setSearchScope(int scope)其中的scope对应上面的2)； SearchControls.setReturningAttributes(String[] attrs)，设置指定返回的属性，对应上面的4)。</p>
<p>　　其他的参数还包括Size Limit，即限制搜索结果的数目（LDAP返回的Entry一般情况下是没有排序的，除非使用一些Sort Control），当你的搜索可能会返回成千上万的Entry时，限制搜索的数目是非常明智也是必须的。关于这些，读者可以参考API文档或者LDAP协议。</p>
<p><strong>4.5.3 根据用户的Entry DN，和用户输入的口令完成身份验证<br />
　　</strong>下面的方法实现了到LDAP的身份验证。LDAP中的用户实际上就是一个LDAP Entry，一次验证实际是将Connection与这个Entry进行绑定，因此验证时我们需要两个必须的参数，一个是Entry的DN，一个是口令。LDAP的身份验证方式有多种，包括SASL以及基于证书的验证等，我们这里只介绍Simple Authentication。关于SASL更多信息读者可以参考：<a href="http://www.ietf.org/rfc/rfc2222.txt" target="_blank">http://www.ietf.org/rfc/rfc2222.txt</a>。</p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>&nbsp; /**<br />
            &nbsp;&nbsp; * <br />
            &nbsp;&nbsp; * @param dn<br />
            &nbsp;&nbsp; * @param pwd<br />
            &nbsp;&nbsp; * @return<br />
            &nbsp;&nbsp; * @throws LDAPException<br />
            &nbsp;&nbsp; */<br />
            &nbsp; public boolean authentication( String dn, String pwd) throws LDAPException {<br />
            &nbsp;&nbsp;&nbsp; LDAPConnection conn= pool.getConnection();<br />
            &nbsp;&nbsp;&nbsp; try { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; conn.authenticate( dn, pwd);<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return true;<br />
            &nbsp;&nbsp;&nbsp; }catch ( LDAPException ex) { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ( ex.getLDAPResultCode()== LDAPException.INVALID_CREDENTIALS) { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return false; // 用户口令错误 }<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ( ex.getLDAPResultCode()== LDAPException.NO_SUCH_OBJECT) { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return false; // 用户不存在 }<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; throw ex; <br />
            &nbsp;&nbsp;&nbsp; }finally { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; try {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ( conn!= null) pool.close(conn);<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }catch ( Exception ex) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } <br />
            &nbsp;&nbsp;&nbsp; }<br />
            &nbsp; } </td>
        </tr>
    </tbody>
</table>
<p><br />
<strong>　　</strong>我们这里使用 LDAP Bind操作完成对用户的身份验证。本文前面曾提到也可以使用LDAP Compare，通过比较口令属性（userpassword）中的值来完成验证。我们需要根据不同的LDAP Server选择合适的验证方法。</p>
<p><strong>　　</strong>比如iPlanet LDAP Server 5，只能通过Bind完成认证；而Oracle Internet Directory可以通过LDAP Compare完成验证，而且还支持口令策略。</p>
<p><strong>　　</strong>如果我们使用了连接池，连接池中的连接一般都是使用权限较大的用户初始化的，这样这些连接才可以完成对LDAP的搜索操作；而当通过这些连接对普通用户进行身份验证时，如果通过验证，连接的身份将被改变为普通的用户（或称为与普通用户的身份关联）。普通用户很可能没有除了bind以外的任何权限，所以在连接被放入池中前，我们必须要恢复连接的身份。</p>
<p><strong>　　</strong>这样我们必须执行两次LDAP Bind，一次用于对普通用户验证身份；一次用于恢复连接的较大权限的用户身份。我们看到这样效率是比较低的，可能你在LDAP Server端统计有2万次bind请求，实际上只有1万人次登录。</p>
<p><strong>　　</strong>对于特定的LDAP Server，比如 Oracle Internet Directory，可以通过LDAP Compare对用户身份进行验证，并且不会改变连接关联的用户身份。这样我在使用池的情况下只需要一次LDAP Compare即可。效率有很大提高。如果不通过定制LDAP Authentication Provider，这样的调优是没法实现的。</p>
<p><strong>　　</strong>Netscape LDAP SDK的ConnectionPool的实现中，在连接放入池中前会检查连接的身份，如果身份被改变，那么会重新进行bind。所以我们没有必要在代码中再做bind。</p>
<p><strong>4.5.4 根据用户的DN搜索用户属于的组列表<br />
　　</strong>有了上面的基础后，这个方法就很容易理解了。下面我们来看看如何返回用户所属的组。</p>
<p>&nbsp; </p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>/**<br />
            &nbsp;&nbsp; * <br />
            &nbsp;&nbsp; * @param groupbasedn<br />
            &nbsp;&nbsp; * @param memberDn<br />
            &nbsp;&nbsp; * @return<br />
            &nbsp;&nbsp; * @throws LDAPException<br />
            &nbsp;&nbsp; */<br />
            &nbsp; public List getGroupMembership( String groupbasedn, String memberDn) throws LDAPException {<br />
            &nbsp;&nbsp;&nbsp; LDAPConnection conn= pool.getConnection(); <br />
            &nbsp;&nbsp;&nbsp; try { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LDAPSearchResults sr= conn.search(<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; groupbasedn,<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 2,<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; "(uniquemember="+ memberDn +")", <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; new String[] {"cn"},<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; false);<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; List groups= new java.util.ArrayList();<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; while ( sr.hasMoreElements()) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LDAPEntry entry= sr.next();<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LDAPAttribute attr= entry.getAttribute("cn");<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ( attr!= null) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; String[] values= attr.getStringValueArray(); <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ( values!= null &amp;&amp; values.length&gt;0) groups.add( values[0]);<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return groups;<br />
            &nbsp;&nbsp;&nbsp; }catch ( LDAPException ex) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; throw ex;<br />
            &nbsp;&nbsp;&nbsp; }finally { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; try { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ( conn!= null) pool.close(conn);<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }catch ( Exception ex) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
            &nbsp;&nbsp;&nbsp; }<br />
            &nbsp; } </td>
        </tr>
    </tbody>
</table>
<p><strong>　　</strong>成员和组的membership主要是通过uniquemember或者 member属性来定义的。成员不仅仅可以使用用户，也可以是组，因为组可以嵌套。<br />
<br />
<strong>　　</strong>a) 上面的方法中只实现了一个层次的搜索，即用户——组的搜索，而组间的嵌套搜索没有实现。读者可以根据系统内的具体情况，在此处也可以做一些优化。</p>
<p><strong>　　</strong>b) 组成员的数量可能比较大，为了避免不必要的开销，我们指定只返回组的cn属性。</p>
<p><strong>　　</strong>c) 动态组的优化。在WebLogic默认的实现中，Provider（实际为LoginModule）会不断的拿动态组中定义的URL中的filter和用户的DN去LDAP做搜索，来看用户是否属于该组。本文前面也讨论了，这样效率很低，完全可以放在Provider本地实现URL和Entry的匹配。</p>
<p><strong>4.5.5 LoginModule中的login()方法实现<br />
　　</strong>一切准备就绪后，我们就可以完成LoginModule.login()这个最核心的方法了。下面我根据代码中的注释逐条说明。</p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>&nbsp; // <strong>A</strong><br />
            &nbsp; boolean loginSucceeded;<br />
            &nbsp; // <strong>B</strong><br />
            &nbsp; List principals= new java.util.ArrayList();<br />
            &nbsp; /**<br />
            &nbsp;&nbsp; *<br />
            &nbsp;&nbsp; * @return<br />
            &nbsp;&nbsp; * @throws LoginException <br />
            &nbsp;&nbsp; */<br />
            &nbsp; public boolean login() throws LoginException { <br />
            &nbsp;&nbsp;&nbsp; // <strong>C</strong><br />
            &nbsp;&nbsp;&nbsp; Callback[] callbacks= new Callback[] { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; new NameCallback("username: "),<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; new PasswordCallback("password: ",false)};<br />
            &nbsp;&nbsp;&nbsp; try { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; callbackHandler.handle( callbacks);<br />
            &nbsp;&nbsp;&nbsp; }catch (IOException e) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; throw new LoginException(e.toString());<br />
            &nbsp;&nbsp;&nbsp; }catch (UnsupportedCallbackException e) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; throw new LoginException(e.toString() + " " +e.getCallback().toString()); &nbsp;&nbsp;&nbsp; }<br />
            &nbsp;&nbsp;&nbsp; //<br />
            &nbsp;&nbsp;&nbsp; String userName = ((NameCallback)callbacks[0]).getName(); <br />
            &nbsp;&nbsp;&nbsp; if ( userName== null || userName.length()== 0) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; throw new LoginException("User login name is empty!");<br />
            &nbsp;&nbsp;&nbsp; }<br />
            &nbsp;&nbsp;&nbsp; // <br />
            &nbsp;&nbsp;&nbsp; PasswordCallback passwordCallback= (PasswordCallback)callbacks[1]; &nbsp;&nbsp;&nbsp; char[] password = passwordCallback.getPassword();<br />
            &nbsp;&nbsp;&nbsp; passwordCallback.clearPassword();<br />
            &nbsp;&nbsp;&nbsp; if ( password== null || password.length== 0) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; throw new LoginException("User password is empty!");<br />
            &nbsp;&nbsp;&nbsp; }<br />
            &nbsp;&nbsp;&nbsp; try {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // <strong>D</strong><br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; String dn= this.getUserDN( userName);<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ( dn== null) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; throw new LoginException("User "+ userName +" doesn't exist."); <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // <strong>E</strong><br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; boolean authResult= this.authentication( dn, String.valueOf( password)); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ( authResult== false) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; throw new FailedLoginException("User login failed.");<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // <strong>F</strong><br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; principals.add( new WLSUserImpl( userName));<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // <strong>G</strong><br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; List groups= this.getGroupMembership( "ou=groups,o=examples", dn); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for ( int i=0, n=groups.size(); i&lt;n; i++) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;String cn= String.valueOf(groups.get(i));<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // <strong>H</strong> <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; principals.add( new WLSGroupImpl(cn)); <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
            &nbsp;&nbsp;&nbsp; }catch ( LDAPException ex) { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; java.io.StringWriter sw = new java.io.StringWriter(); &nbsp;<br />
            &nbsp;&nbsp;&nbsp;&nbsp; ex.printStackTrace(new java.io.PrintWriter(sw));<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sw.flush();<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; throw new LoginException( sw.toString());<br />
            &nbsp;&nbsp;&nbsp; }&nbsp;<br />
            &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; return loginSucceeded= true;<br />
            &nbsp; } </td>
        </tr>
    </tbody>
</table>
<p><strong>　　</strong>a) 用于保存验证结果，在后续方法（commit等）中使用<br />
<br />
<strong>　　</strong>b) 用于保存和用户关联的Principal，在后续方法（commit等）中使用<br />
<br />
<strong>　　</strong>c) 在JAAS中通过CallbackHandler来完成用户和底层安全认证系统间信息的交换，这里我们通过CallbackHandler获取用户输入的登录帐号和口令。CallbackHandler将在初始化LoginModule时由WebLogic Security Framework传入。读者可能会问，我是否可以在此要求WebLogic Security Framework传入我自己实现的CallbackHandler，进而获取更多的信息，支持更多的Callback？很遗憾，不可以。只有在一种情况下（本文前面也提到）有一个变通，就是在Web应用中，通过Form Based这种认证方式进行用户身份验证时，可以获取更多的登录表单中提交的cgi变量。</p>
<p><strong>　　</strong>下面的代码将演示这种技术：</p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>&nbsp; &nbsp; Callback[] callbacks= new Callback[] { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; new NameCallback("username: "),<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; new PasswordCallback("password: ",false),<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; new TextInputCallback("my_hidden_field")};<br />
            &nbsp;&nbsp;&nbsp; try { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; callbackHandler.handle( callbacks); <br />
            &nbsp;&nbsp;&nbsp; }catch (IOException e) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; throw new LoginException(e.toString());<br />
            &nbsp;&nbsp;&nbsp; }catch (UnsupportedCallbackException e) {<br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; throw new LoginException(e.toString() + " " +e.getCallback().toString()); &nbsp;&nbsp;&nbsp; } <br />
            &nbsp;&nbsp;&nbsp; String value= ((TextInputCallback)callbacks[2]).getText(); </td>
        </tr>
    </tbody>
</table>
<p><strong>　　</strong>这样我们通过((TextInputCallback)callbacks[2]).getText()来获取表单中更多的信息。其中TextInputCallback的Prompt必须要和表单中对应域的名字一致，否则取不到信息。如果有多个域，则需要传入多个TextInputCallback。</p>
<p><strong>　　</strong>同时如果登录表单中没有这些对应TextInputCallback的域，或者使用JNDI方式验证，那么会抛出UnsupportedCallbackException。</p>
<p><strong>　　</strong>下面是一个符合Servlet安全规范的登录表单，其中域my_hidden_field的值将通过CallbackHandler传递到的TextInputCallback中：</p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>&lt;html&gt;<br />
            <strong>　　</strong>&lt;form method=&#8221;POST&#8221; action=&#8221;j_security_check&#8221;&gt;<br />
            <strong>　　</strong><strong>　　</strong>&lt;input type=&#8221;text&#8221; name=&#8221;j_username&#8221;&gt;<br />
            <strong>　　</strong><strong>　　</strong>&lt;input type=&#8221;password&#8221; name=&#8221;j_password&#8221;&gt;<br />
            <strong>　　</strong><strong>　　</strong>&lt;input type=&#8221;hidden&#8221; name=&#8221;my_hidden_field&#8221;value=&#8221;Hello Callback!&#8221;&gt;<br />
            <strong>　　</strong>&lt;/form&gt;<br />
            &lt;/html&gt;</td>
        </tr>
    </tbody>
</table>
<p><strong>　　</strong>d) 生成一个用于存放Principal的集合对象<br />
<strong>　　</strong>e) 通过登录帐号获取LDAP中的Entry DN<br />
<strong>　　</strong>f) 通过DN和用户口令进行身份验证<br />
<strong>　　</strong>g) 将通过验证的用户名加入到Principal列表中，这些以后都将代表用户的一定权限<br />
<strong>　　</strong>h) 查找用户属于哪些组<br />
<strong>　　</strong>i) 将这些组的名字加入到Principal列表中</p>
<p><strong>4.5.6 LoginModule中的commit()和abort()方法实现<br />
　　</strong>完成了以上的验证后，我们需要通过LoginModule.commit()方法提交验证结果；或者abort()方法取消验证结果。</p>
<table cellspacing="0" cellpadding="4" width="98%" align="center" bgcolor="#e8e8e8" border="0">
    <tbody>
        <tr>
            <td>&nbsp; // <strong>A</strong><br />
            &nbsp; private Subject subject;<br />
            &nbsp; &nbsp; /**<br />
            &nbsp;&nbsp; *<br />
            &nbsp;&nbsp; * @return<br />
            &nbsp;&nbsp; * @throws LoginException<br />
            &nbsp;&nbsp; */<br />
            &nbsp; public boolean commit() throws LoginException { if (loginSucceeded) { <br />
            &nbsp; // <strong>B</strong><br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; subject.getPrincipals().addAll(principals);<br />
            &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;return true;<br />
            &nbsp;&nbsp;&nbsp; }else { <br />
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return false;<br />
            &nbsp;&nbsp;&nbsp; } <br />
            &nbsp; }<br />
            &nbsp; &nbsp; /**<br />
            &nbsp;&nbsp; * <br />
            &nbsp;&nbsp; * @return<br />
            &nbsp;&nbsp; * @throws LoginException<br />
            &nbsp;&nbsp; */<br />
            &nbsp; &nbsp; public boolean abort() throws LoginException {<br />
            &nbsp;&nbsp;&nbsp; // <strong>C</strong> <br />
            &nbsp;&nbsp;&nbsp; subject.getPrincipals().removeAll(principals); <br />
            &nbsp;&nbsp;&nbsp; return true;<br />
            &nbsp; } &nbsp; &nbsp;</td>
        </tr>
    </tbody>
</table>
<p>&nbsp;</p>
<p><strong>　　</strong>我这里仍然通过代码中的注释进行相应的说明：</p>
<p><strong>　　</strong>a) JAAS Subject，在LoginModule初始化时，由WebLogic Security Framework负责传入，用户的权限信息（Principals等）都将封装到该Subject中。同时Principal Validator会对Subject中的Principals进行签名，防止被黑客纂改。</p>
<p><strong>　　</strong>b) 用户认证成功的情况下，将用户关联的Principals加入到Subject中。这样用户在进行后续的访问时，WebLogic Security Framework会检查与用户关联的Subject中是否有可以访问受限资源的Principal。</p>
<p><strong>　　</strong>c) 用户登录失败的情况下（有可能是其他的LoginModule导致的整体登录失败，具体参考JAAS Control Flag），我们在commit中添加的Principals必须从Subject中删除。</p>
<p><br />
<strong>　　</strong>通过上面的描述和讨论，相信读者已经对如何实现一个LDAP AuthenticationProvider/LoginModule有了比较清楚的了解。限于篇幅省略了很多细节，对此有需要的读者可以根据文中给出的相关链接做进一步的了解；或者与我交流。从这里我们也可以看到核心的逻辑还是比较简单的。其中将LDAP Group作为用户的Principal，是J2EE Security与LDAP Security结合的非常重要的一步。通过这种结合，我们可以将LDAP作为后端，设计出基于J2EE，JAAS的用户身份验证，权限管理等系统。<a id="5" name="5"></a><br />
<br />
<strong>5 部署中的注意事项</strong><br />
<strong>　　</strong>开发完成LDAP Authentication Provider后，需要将通过WebLogic MBean Maker生成的MBean Jar File放到${WL_HOME}/server/lib/mbeantypes目录下。由于WebLogic Server在启动时会加载这些Provider，因此在一个分布式的环境中，WebLogic Domain内的全部WLServer（包括Managed Server）均需要部署该Jar包。</p>
<p><strong>　　</strong>同时由于使用了Provider MBean，因此，当你删除了MBean Types中定义的，并且通过修改保存在config.xml（WebLogic Domain配置文件）中的属性时，重新部署的Jar包会导致WebLogic Server无法正常启动，因为它没有办法按照config.xml中配置的属性初始化MBean。此时需要手工修改config.xml，删除相关的属性即可。</p>
<p>&lt;examples.security.providers.authentication.simple.SimpleSampleAuthenticator<br />
<strong>　　</strong>ControlFlag="OPTIONAL"<br />
<strong>　　</strong>Name="Security:Name=myrealmSimpleSampleAuthenticator"<br />
<strong>　　</strong>UserBaseDN="ou=people, o=examples" Realm="Security:Name=myrealm"/&gt;</p>
<p><strong>　　</strong>比如UserBaseDN已经不需要通过MBean来管理，并且在MBean Types中已经删除，那么直接删除这里的UserBaseDN="ou=people, o=examples" 就可以了。<a id="6" name="6"></a><br />
<br />
<strong>6 结束语</strong><br />
<strong>　　</strong>本文所讨论的内容有些过于广泛，从我个人角度讲，写起来也比较困难，很多技术问题没有进行深入的讨论。其实只是希望通过本文，能够帮助读者对WebLogic Security，J2EE Security，LDAP Security以及JAAS有一个初步的认识；能够给希望实现LDAP Authentication Provider，或者被WebLogic LDAP Authentication Provider困惑的读者一些启示。只要能够对读者有所帮助，本文的目的就算达到了。</p>
<p><strong>　　</strong>限于篇幅，本文很多地方的表述可能并不清晰，也可能会有一些错误，如果在阅读过程中产生任何疑问或者有任何看法都可以通过E-mail来与我交流，我也非常希望能和大家交流。<a id="7" name="7"></a><br />
<br />
<strong>7 参考资料</strong><br />
a) <a href="http://e-docs.bea.com/wls/docs81/secwlres/types.html#1213777" target="_blank">http://e-docs.bea.com/wls/docs81/secwlres/types.html#1213777</a> 关于WebLogic Resource的更多说明<br />
b) <a href="http://e-docs.bea.com/wls/docs81/security/index.html" target="_blank">http://e-docs.bea.com/wls/docs81/security/index.html</a> 关于WebLogic Security的全部内容<br />
c) <a href="http://e-docs.bea.com/wls/docs81/dvspisec/rm.html#1145542" target="_blank">http://e-docs.bea.com/wls/docs81/dvspisec/rm.html#1145542</a> 关于Role Mapping Provider的更多内容<br />
d) <a href="http://dev2dev.bea.com.cn/techdoc/2005012102.html" target="_blank">http://dev2dev.bea.com.cn/techdoc/2005012102.html</a> 关于WebLogic Console的扩展<br />
e) <a href="http://dev2dev.bea.com/codelibrary/code/security_prov81.jsp" target="_blank">http://dev2dev.bea.com/codelibrary/code/security_prov81.jsp</a> Custom Seuciryt Provider的Samples<br />
f) <a href="http://java.sun.com/products/JavaManagement/wp/" target="_blank">http://java.sun.com/products/JavaManagement/wp/</a> 更多关于Sun JMX<br />
g) <a href="http://java.sun.com/products/jndi/tutorial/ldap/connect/index.html" target="_blank">http://java.sun.com/products/jndi/tutorial/ldap/connect/index.html</a> 关于如何使用Sun JNDI LDAP Service Provider中提供的连接池<br />
h) <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">http://www.ietf.org/rfc/rfc2254.txt</a> 更多关于LDAP Filter<br />
i) <a href="http://www.ietf.org/rfc/rfc2251.txt" target="_blank">http://www.ietf.org/rfc/rfc2251.txt</a> LDAP V3协议<br />
j) <a href="http://www.ietf.org/rfc/rfc2222.txt" target="_blank">http://www.ietf.org/rfc/rfc2222.txt</a> 更多关于SASL</p>
<img src ="http://www.blogjava.net/masen/aggbug/179174.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/masen/" target="_blank">Masen</a> 2008-02-03 15:17 <a href="http://www.blogjava.net/masen/articles/179174.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>