<![CDATA[wston.bokee.com]]> zh_cn Thu,08 Nov 2007 09:26:20 CST Fri,04 Jul 2008 10:10:22 CST http://www.bokee.com http://reg.bokee.com/account/web/img/logo.gif 博客网 http://www.bokee.com 您好,欢迎访问yunle110.bokee.com <![CDATA[随 缘]]> .html

 

/ 淡淡云中月

 

禅语: 万事皆有缘,人生当随缘。随不是跟随,是顺其自然 , 不怨忧, 不躁进,  不强求。 随不是随便 ,是把握机缘,不悲观,不刻板,不慌乱。人生随缘,即是“枯萎的随它枯萎,繁荣的任它去繁荣”。随顺自然,毫不执着。

 

 

随缘,两字常见,却从未去深究,理解也停留在表义,这些天看一些关于人生禅悟的书,才发现其实自己一直崇尚的顺其自然的人生处世态度即是禅语的“随缘”二字;但真正要做到禅语的“随缘”,却需要在人生的路途中更深刻去领会,去禅悟,才能真正生活得更豁达,洒脱。

 

凡事顺其自然,是自己常说的话,但真正遇事时,却又未必能真正做到。明明知道,世间许多事情比如事业亦好,情感也罢,皆应顺其自然,不可强求,但铆足干劲、紧盯预目标追求成功时,不自觉地就会被人“欲”所左右,“欲”多数时候是占上峰且无限的,当在自己的努力下有所收获时,“欲”便会丛涌追求更大、更全、更高的目标,甚至超越自己的实际能力,将自己引入“欲”的恶性循环里,即“欲”一旦得到满足,又会有更多“欲求”,人不知不觉便会被自己太多的“欲求”困扰,生活得不堪负重,一边忽略了人生中许多垂手可得的快乐和幸福,却又一边感叹生活的无趣,人生的压力。当然,人生不可能无求无欲,事业目标的追求更如此,只是以随缘的心态去面对,淡看人生的“欲”,便会在努力追求事业目标的同时,用心去体验酸甜苦辣的过程,顺其自然中懂得把握时机,不消极,不躁进,不慌乱,不过于注重结果,有“果”是缘,无“果”也是缘,有时真的如愿修成了“正果”,回过头来,值得回味的却是过程,所以只要努力过、奋斗过,争取过,有无“果”也就随缘了。在顺其自然的情况下,已经“无果”了,却在“欲”的驱使下,不甘心“无果”,盲目执着,沦为“欲”的奴隶,执着于不是自己能力所能达到的,甚至超越自己能力才能达到目标,整日处于“患得患失”的心态,便注定生活在不安、烦燥的状态,注定无法去体验种种追求目标路程中带来的快乐!学会自嘲一些吧,事业有成,目标得以实现,是顺其自然,水到渠成;努力了事业无成,目标无法实现,也是顺其自然,说明自己方方面面尚不足,懂得为自己解脱,正视现实,跳出“欲”的陷井,我们便会在冬日的萧条中看到春日的万物复苏,夏季的生机盎然,秋季的硕果累累;世间是轮回的,有所得必有所失,有所失又会有所得,万事随缘,保持一颗平常心,淡看繁华与落寞,颠峰与低谷;人生路上的坎坎与坷坷,幸福与快乐,皆是人世走一遭得到的馈赠,不可拒绝,就坦然面对。

 

凡事皆随缘,人生除了工作事业外,情感更应随缘。是你的,终归是你,不是你的,即使强求得到的也未必幸福。禅语说得好“枯萎的随它枯萎,繁荣的随它繁荣”,万事随缘,当不可预知的缘份将一份情感摆在面前时,能做的也许就是“随缘”,顺其自然,值得珍惜和拥有的,就该把握机缘,积极争取,哪怕历尽磨难,艰辛万苦,即使最终无缘拥有、伤痛满身,那也是人生情感经历的路程。有幸得到这份情感的,更要顺其自然,懂得珍惜,懂得用心去经营,懂得享受随之带来的幸福快乐。但是世间万物不是一成不变的,任何事物都是在变化中发展,在发展中前进,情感亦如此,天长地久,海枯石烂终不悔,永远的永远等等这些男女间美妙爱情滋生的情话,亦是顺乎于情,情至深处时的至高至美至纯的美好愿望,是每一个心怀着爱的人所祈求的结果,但在追求天长地久或永远的情感结果过程中,情感会遭受各种各样因素的影响,不可能一成不变,或许生活会让情感变得更加情意笃深,直至牵手白头;或许生活又会颠覆当初视若无价的情感,何谓真爱让人迷惑。无论哪一种结果,其实都该随缘,两情相悦,情深意浓时,尽情地渲染,尽情地享受,尽情地相爱吧!让所有的人都善意地嫉妒;情已逝,爱已走时,亦请尽情地发泄,尽情地流泪,尽情地痛哭吧!被人同情与嘲笑又如何?自嘲地为自己开脱:自己失去的是不爱自己和不珍惜自己的人,而那个人失去的是一个最爱和最珍惜他(她)的人,相比之下自己的损失更小些。随缘吧!失去也许是为了让自己遇到更好的缘!顺其自然,懂得珍惜与拥有,更要懂得舍得与放弃。情感中的“情”和“爱”是博大的,除了男女间的情爱,还有亲情、儿女情、手足情、朋友情、同事情等等,许多时候要懂得跳出情感的“小我”,寻到“大我”,不将情感局限于“小我”世界,一颗心不仅能饱受幸福快乐,更能承受得起磨砺,安然地看云卷云舒,看山花烂漫,看天苍苍野茫茫,看天地的礴大,感知人的渺小;人的一生何必在意太多的得得失失,真实地过好每一天,开心每一天,展望美好的每一天吧!

 

随缘,一直崇尚的一种做人的态度,用心去感悟,才领会“随缘”更是一种待人处事的思维方式,生活是在随缘中实现,心智也在随缘中成长,岁月本无疆,人生当随缘!

 

]]>
Fri,04 Jul 2008 10:10:22 CST 99
<![CDATA[有一楼主伤心发贴,竟然引来一批网友安慰]]> .html 某论坛有一楼主伤心发贴,竟然引来一批网友安慰,然后形成如此结果…… 

原帖如下: 

我和我老婆是大学里认识的,大2的时候,我们在同一个学院,故事很平常,朋友租房子我去帮忙抬东西,朋友的朋友也去帮忙,这样我就认识了朋友的朋友,也就是老婆。 

大学的生活很快,日子过去。我是个穷人,家里没有钱,父母以前是工人,现在下岗了在外打工,虽然穷,但是日子过得很快乐,我和老婆也是一样,最让她感动的是 2003年我节约钱给她买了她很喜欢的那双 NIKE的鞋,她很感动,那时候我也觉得自己很幸福。 

她的家庭情况也一般,她说以后想考公务员,而我刚上大四时去了一个房屋公司兼职做了个小职员,一个月1500。一切看来都很好。我是成都人,老婆重庆的。我在重庆读大学自然就在重庆的工作了,公司在两路口,那时候我和老婆计划着以后奋斗的生活。 

风云突变,就在大四的下学期,在毕业来临的日子里,我们和中国农业大学进行友好学生活动。她认识了里面一个大四的上海人。那天活动我也去了,我见到了那个上海人,带着个傻忽忽的眼睛,可是一看那小样就有点钱。二天后,老婆就搬出了我们租的房子。只是匆匆的来了个电话和一封信,大概意思是对我道歉,还叫我努力,鼓励我去面对未来美好的生活。 

那一时刻我的头都炸了,没有伤心的感觉,只是觉得脑袋空空的,后来她的朋友来告诉我,那个上海人很有钱,有车有房子,家里还有船。那天上海人陪她坐车出去到了,大概是到瓷器口方向有一段路不好,那上海人就出了钱找几个民工买材料把那段路修好,就这一举动彻底征服了我老婆。 

在我看来这纯粹的无聊花钱行为中,老婆没有了,毕业典礼的时候她已经在和那个上海人软语开心的聊天了。我觉得自己很失败,那天晚上我坐在通往到解放杯的公交车上,在最后一排我终于忍不住哭了。 

事后想想也不怪她,人人都有追求幸福的权利,但作为一个男人只有提高自身的含金量才是正途。有时我常常笑我自己是井底之蛙,1500元要多少年才能买车买房,没有钱哪来的浪漫?让人跟你受穷?人家上海人有钱就该娶美女,我这种穷鬼要是找到了美女那不是阻碍了先进生产力的发展方向?还怎么构建和谐社会?每当有人提起她,我的心理都这样想着。 



以下为其他网友安慰楼主的回帖,请欣赏: 

某网友回复: 

我是上海人,遭遇和 LZ一样.上个月我认识半年的女友和一个香港人跑了. 我收入还可以有房有车,但那HK人不但有房还有辆三菱的跑车,月薪有七八万,够我做大半年的了.最可气的是 ,我1.86她1.70那个矮子才1.60. 事后想想也不怪她,人人都有追求幸福的权利,但作为一个男人只有提高自身的含金量才是正途,不需要自怨自哀. 希望能与LZ共勉 



某网友回复: 

我是香港人,遭遇和LZ一样.上个礼拜我认识才半个月的女友和一个曰本鬼子跑了.我收入还可以,有房有车,还是三菱的跑车,月薪也有七八万,够 LZ做好几年的了.最可气的是,我1.60那个曰本鬼子才1.55。事后想想也不怪她,人人都有追求幸福的权利,但作为一个男人只有提高自身的含金量才是正途,不需要自怨自哀.希望能与LZ 共勉. 



某网友回复: 

我是火星人,遭遇和LZ一样.上个礼拜我认识才半个月的女友和哈雷慧星人跑了.我收入还可以,有房有车有飞碟,还有宇宙飞船空间站,月薪也有七八个兆。可是那哈雷慧星人开的是激光束啊!最可气的是,我0.55那个哈雷慧星人才0.5,事后想想也不怪她,人人都有追求幸福的权利 ,但作为一个男人只有提高自身的含金量才是正途,不需要自怨自哀.希望能与LZ共勉. 



某网友回复: 

我是哈雷慧星人,遭遇和LZ一样上个礼拜我认识才半个月的女友和土星人跑了.我收入还可以,有房有车有飞碟,还有激光束,月薪也有七八个亿兆。可是那土星人开的是土星光环啊!最可气的是,我0.5那个土星人才0.05,事后想想也不怪她,人人都有追求幸福的权利,但作为一个男人只有提高自身的含金量才是正途,不需要自怨自哀.希望能与LZ共勉. 



某网友回复: 

我是土星人,遭遇和LZ一样.上个礼拜我认识才半个月的女友和M78星云的奥特曼跑了.我收入还可以,有房有车有飞碟,有激光束,我还有土星光环作交通工具,月薪也有七八个万亿兆。可是那M78星云的奥特曼不用坐什么自己就能飞!最可气的是,我0.05那个奥特曼有400多米,事后想想也不怪她,人人都有追求幸福的权利,但作为一个男人只有提高自身的含金量才是正途,不需要自怨自哀.希望能与LZ共勉. 



某网友回复: 

我是奥特曼,遭遇和LZ一样 .上个礼拜我认识才半个月的女友和一个重庆人跑了。我收入还可以,有房有车有星球,不用交通工具自己就会飞,翻个身就十万八千里,没有月薪,自己印钞。没办法,女友说我不是人

]]>
Thu,07 Aug 2008 10:45:51 CST 0
<![CDATA[我的初恋女友和现任女友(转自互联网)]]> .html  

我的初恋女友初恋时21岁;
我的现任女友初恋时16岁。

我的初恋女友是我的大学同学;
我的现任女友是我在泡吧时认识的。

我连哄带骗一年半以后才与我初恋女友发生了关系;
我与现任女友认识的第二天就睡在了一起。

我和初恋女友发生关系的地点在我们宿舍;
我和现任女友在四星酒店开的房。

送初恋女友一个“史卢比”她高兴好几天,不停地向她室友炫耀;
送现任女友一个铂金戒子.她看了两眼,放近抽屉.原来是嫌它太小。

初恋女友买衣服时,逛的是大型批发市场;
现任女友买衣服时,逛的是品牌专卖店。

与初恋女友吵架,她边抽泣边小声问道:“难道你不再爱我了吗?”;
与现任女友吵架,她坐在沙发上指着我骂道:“你们男人请文明用语没一个好东西!!”

与初恋女友在一起时,她把家里每个月寄给她的生活费存到我的食堂饭卡上;
与现任女友在一起时,我每个月的工资存到她的存折上。

与初恋女友在一起,早上我醒来时候,她已经买好了早餐等我起来吃;
与现任女友在一起,她躺在床上对我说:“老公,我饿!去给我买早餐”。


初恋女友下课后在我教室门口等我一起去食堂吃饭.甚至有时打了饭送来给我吃;
现在我经常下班了回来买菜做饭等现任女友回来吃.甚至有时还得给她送过去。

初恋女友经常坐在我身边陪我上网到天亮,最后她伏在桌子上睡了;
现任女友经常打麻将打天亮 ,我坐在旁边看着,最后我坐在凳子上睡着了。

初恋女友听见我和死党们说黄色笑话会脸红;
现任女友经常把她手机里的黄色短信转发给我。

初恋女友看见这篇文章,一定会感慨万千;
现任女友看见这篇文章,一定会怨声连连。

初恋女友等我老了,我还依然记得她;
现任女友等我老了,我不知道我能否还记得她。

第一次和初恋女友约会,吃的是二块钱一碗的刀削面。她说吃不了还夹了一大半给我;
第一次和现任女友约会,吃的是八十八元一份的西式牛排。完了她还要了一份水果沙拉。

第一次走路送初恋女友回家,她神采飞扬.笑个不停;
第一次走路送现任女友回家,她说我小气,怎么不打的?

第一次牵我初恋女友的手,她的手心在冒汗,呆呆的她任由我牵着(准确说应该是任由我拖着); 


第一次牵我现任女友的手,她自然的把手指反扣过来,牵着我。

第一次与初恋女友谈论爱情的时候,她坚定地说她相信爱情能够天长地久。闻之我开心了一个礼拜;
第一次与现任女友谈论爱情的时候,她奚落地说相信爱情会天长地久的人是傻冒 幼稚,是不成熟的表现。闻之我郁闷了一个礼拜。

第一次与初恋女友接吻,她全身发麻。傻傻站着,发不出声音;
第一次与现任女友接吻,她把自己的舌头伸向我的口中。

第一次隔着衣服摸初恋女友的胸脯,她啊的一声大叫,跳着跺开吓的全身出汗;
第一次摸现任女友的胸脯,发现她乳头已经硬了起来。

第一次与初恋女友发生关系时,她迟迟不肯脱衣上床;
第一次与现任女友发生关系时,我们一起洗的“鸳鸯浴”。

第一次与初恋女友**时,她傻傻地躺在床上,嘴里低语着:“轻点......我怕”;
第一次与现任女友**时,她双手抱着我的背,双腿夹着我的腰,嘴里叫着:“恩......快点儿,再快点儿.....用力~~~~~!!!”。

第一次与初恋女友**完事后,她偎依在我怀里,喃喃地说:“人家现在是你的人了,你这一辈子都要对人家好哦.....”,我感动;

第一次与现任女友**完事后,她瘪着嘴抱怨:“叫你刚才用力嘛,真是没用....”,我尴尬。


第一次与初恋女友**时,我们都觉得这是心灵上的交流;
第一次与现任女友**时,我们都觉得这只是器官上的摩擦。

第一次与初恋女友**时,她出的是血;
第一次与现任女友**时,她出的是水.

...............

]]>
Tue,08 Jul 2008 17:11:47 CST 0
<![CDATA[追MM与设计模式]]> .html 创建型模式

1、FACTORY—追MM少不了请吃饭了,麦当劳的鸡翅和肯德基的鸡翅都是MM爱吃的东西,虽然口味有所不同,但不管你带MM去麦当劳或肯德基,只管向服务员说“来四个鸡翅”就行了。麦当劳和肯德基就是生产鸡翅的Factory

  工厂模式:客户类和工厂类分开。消费者任何时候需要某种产品,只需向工厂请求即可。消费者无须修改就可以接纳新产品。缺点是当产品修改时,工厂类也要做相应的修改。如:如何创建及如何向客户端提供。

2、BUILDER—MM最爱听的就是“我爱你”这句话了,见到不同地方的MM,要能够用她们的方言跟她说这句话哦,我有一个多种语言翻译机,上面每种语言都有一个按键,见到MM我只要按对应的键,它就能够用相应的语言说出“我爱你”这句话了,国外的MM也可以轻松搞掂,这就是我的“我爱你”builder。(这一定比美军在伊拉克用的翻译机好卖)

  建造模式:将产品的内部表象和产品的生成过程分割开来,从而使一个建造过程生成具有不同的内部表象的产品对象。建造模式使得产品内部表象可以独立的变化,客户不必知道产品内部组成的细节。建造模式可以强制实行一种分步骤进行的建造过程。

3、FACTORY METHOD—请MM去麦当劳吃汉堡,不同的MM有不同的口味,要每个都记住是一件烦人的事情,我一般采用Factory Method模式,带着MM到服务员那儿,说“要一个汉堡”,具体要什么样的汉堡呢,让MM直接跟服务员说就行了。

  工厂方法模式:核心工厂类不再负责所有产品的创建,而是将具体创建的工作交给子类去做,成为一个抽象工厂角色,仅负责给出具体工厂类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。

4、PROTOTYPE—跟MM用QQ聊天,一定要说些深情的话语了,我搜集了好多肉麻的情话,需要时只要copy出来放到QQ里面就行了,这就是我的情话prototype了。(100块钱一份,你要不要)

  原始模型模式:通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的方法创建出更多同类型的对象。原始模型模式允许动态的增加或减少产品类,产品类不需要非得有任何事先确定的等级结构,原始模型模式适用于任何的等级结构。缺点是每一个类都必须配备一个克隆方法。

5、SINGLETON—俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公Sigleton,她们只要说道“老公”,都是指的同一个人,那就是我(刚才做了个梦啦,哪有这么好的事)

  单例模式:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例单例模式。单例模式只应在有真正的“单一实例”的需求时才可使用。


结构型模式

6、ADAPTER—在朋友聚会上碰到了一个美女Sarah,从香港来的,可我不会说粤语,她不会说普通话,只好求助于我的朋友kent了,他作为我和Sarah之间的Adapter,让我和Sarah可以相互交谈了(也不知道他会不会耍我)

  适配器(变压器)模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端。

7、BRIDGE—早上碰到MM,要说早上好,晚上碰到MM,要说晚上好;碰到MM穿了件新衣服,要说你的衣服好漂亮哦,碰到MM新做的发型,要说你的头发好漂亮哦。不要问我“早上碰到MM新做了个发型怎么说”这种问题,自己用BRIDGE组合一下不就行了

  桥梁模式:将抽象化与实现化脱耦,使得二者可以独立的变化,也就是说将他们之间的强关联变成弱关联,也就是指在一个软件系统的抽象化和实现化之间使用组合/聚合关系而不是继承关系,从而使两者可以独立的变化。

8、COMPOSITE—Mary今天过生日。“我过生日,你要送我一件礼物。”“嗯,好吧,去商店,你自己挑。”“这件T恤挺漂亮,买,这条裙子好看,买,这个包也不错,买。”“喂,买了三件了呀,我只答应送一件礼物的哦。”“什么呀,T恤加裙子加包包,正好配成一套呀,小姐,麻烦你包起来。”“……”,MM都会用Composite模式了,你会了没有?

  合成模式:合成模式将对象组织到树结构中,可以用来描述整体与部分的关系。合成模式就是一个处理对象的树结构的模式。合成模式把部分与整体的关系用树结构表示出来。合成模式使得客户端把一个个单独的成分对象和由他们复合而成的合成对象同等看待。

9、DECORATOR—Mary过完轮到Sarly过生日,还是不要叫她自己挑了,不然这个月伙食费肯定玩完,拿出我去年在华山顶上照的照片,在背面写上“最好的的礼物,就是爱你的Fita”,再到街上礼品店买了个像框(卖礼品的MM也很漂亮哦),再找隔壁搞美术设计的Mike设计了一个漂亮的盒子装起来……,我们都是Decorator,最终都在修饰我这个人呀,怎么样,看懂了吗?

  装饰模式:装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案,提供比继承更多的灵活性。动态给一个对象增加功能,这些功能可以再动态的撤消。增加由一些基本功能的排列组合而产生的非常大量的功能。

10、FACADE—我有一个专业的Nikon相机,我就喜欢自己手动调光圈、快门,这样照出来的照片才专业,但MM可不懂这些,教了半天也不会。幸好相机有Facade设计模式,把相机调整到自动档,只要对准目标按快门就行了,一切由相机自动调整,这样MM也可以用这个相机给我拍张照片了。

  门面模式:外部与一个子系统的通信必须通过一个统一的门面对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。每一个子系统只有一个门面类,而且此门面类只有一个实例,也就是说它是一个单例模式。但整个系统可以有多个门面类。

11、FLYWEIGHT—每天跟MM发短信,手指都累死了,最近买了个新手机,可以把一些常用的句子存在手机里,要用的时候,直接拿出来,在前面加上MM的名字就可以发送了,再不用一个字一个字敲了。共享的句子就是Flyweight,MM的名字就是提取出来的外部特征,根据上下文情况使用。

  享元模式:FLYWEIGHT在拳击比赛中指最轻量级。享元模式以共享的方式高效的支持大量的细粒度对象。享元模式能做到共享的关键是区分内蕴状态和外蕴状态。内蕴状态存储在享元内部,不会随环境的改变而有所不同。外蕴状态是随环境的改变而改变的。外蕴状态不能影响内蕴状态,它们是相互独立的。将可以共享的状态和不可以共享的状态从常规类中区分开来,将不可以共享的状态从类里剔除出去。客户端不可以直接创建被共享的对象,而应当使用一个工厂对象负责创建被共享的对象。享元模式大幅度的降低内存中对象的数量。

12、PROXY—跟MM在网上聊天,一开头总是“hi,你好”,“你从哪儿来呀?”“你多大了?”“身高多少呀?”这些话,真烦人,写个程序做为我的Proxy吧,凡是接收到这些话都设置好了自动的回答,接收到其他的话时再通知我回答,怎么样,酷吧。

  代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下,客户不想或者不能够直接引用一个对象,代理对象可以在客户和目标对象直接起到中介的作用。客户端分辨不出代理主题对象与真实主题对象。代理模式可以并不知道真正的被代理对象,而仅仅持有一个被代理对象的接口,这时候代理对象不能够创建被代理对象,被代理对象必须有系统的其他角色代为创建并传入。


行为模式

13、CHAIN OF RESPONSIBLEITY—晚上去上英语课,为了好开溜坐到了最后一排,哇,前面坐了好几个漂亮的MM哎,找张纸条,写上“Hi,可以做我的女朋友吗?如果不愿意请向前传”,纸条就一个接一个的传上去了,糟糕,传到第一排的MM把纸条传给老师了,听说是个老处女呀,快跑!

  责任链模式:在责任链模式中,很多对象由每一个对象对其下家的引用而接 起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的情况下动态的重新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象所接受。

14、COMMAND—俺有一个MM家里管得特别严,没法见面,只好借助于她弟弟在我们俩之间传送信息,她对我有什么指示,就写一张纸条让她弟弟带给我。这不,她弟弟又传送过来一个COMMAND,为了感谢他,我请他吃了碗杂酱面,哪知道他说:“我同时给我姐姐三个男朋友送COMMAND,就数你最小气,才请我吃面。”,:-(

  命令模式:命令模式把一个请求或者操作封装到一个对象中。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。命令模式允许请求的一方和发送的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否执行,何时被执行以及是怎么被执行的。系统支持命令的撤消。

15、INTERPRETER—俺有一个《泡MM真经》,上面有各种泡MM的攻略,比如说去吃西餐的步骤、去看电影的方法等等,跟MM约会时,只要做一个Interpreter,照着上面的脚本执行就可以了。

  解释器模式:给定一个语言后,解释器模式可以定义出其文法的一种表示,并同时提供一个解释器。客户端可以使用这个解释器来解释这个语言中的句子。解释器模式将描述怎样在有了一个简单的文法后,使用模式设计解释这些语句。在解释器模式里面提到的语言是指任何解释器对象能够解释的任何组合。在解释器模式中需要定义一个代表文法的命令类的等级结构,也就是一系列的组合规则。每一个命令对象都有一个解释方法,代表对命令对象的解释。命令对象的等级结构中的对象的任何排列组合都是一个语言。

16、ITERATOR—我爱上了Mary,不顾一切的向她求婚。

          Mary:“想要我跟你结婚,得答应我的条件”

          我:“什么条件我都答应,你说吧”

          Mary:“我看上了那个一克拉的钻石”

          我:“我买,我买,还有吗?”

          Mary:“我看上了湖边的那栋别墅”

          我:“我买,我买,还有吗?”

          Mary:“你的小弟弟必须要有50cm长”

          我脑袋嗡的一声,坐在椅子上,一咬牙:“我剪,我剪,还有吗?”

          ……

  迭代子模式:迭代子模式可以顺序访问一个聚集中的元素而不必暴露聚集的内部表象。多个对象聚在一起形成的总体称之为聚集,聚集对象是能够包容一组对象的容器对象。迭代子模式将迭代逻辑封装到一个独立的子对象中,从而与聚集本身隔开。迭代子模式简化了聚集的界面。每一个聚集对象都可以有一个或一个以上的迭代子对象,每一个迭代子的迭代状态可以是彼此独立的。迭代算法可以独立于聚集角色变化。

17、MEDIATOR—四个MM打麻将,相互之间谁应该给谁多少钱算不清楚了,幸亏当时我在旁边,按照各自的筹码数算钱,赚了钱的从我这里拿,赔了钱的也付给我,一切就OK啦,俺得到了四个MM的电话。

  调停者模式:调停者模式包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用。从而使他们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用。保证这些作用可以彼此独立的变化。调停者模式将多对多的相互作用转化为一对多的相互作用。调停者模式将对象的行为和协作抽象化,把对象在小尺度的行为上与其他对象的相互作用分开处理。

18、MEMENTO—同时跟几个MM聊天时,一定要记清楚刚才跟MM说了些什么话,不然MM发现了会不高兴的哦,幸亏我有个备忘录,刚才与哪个MM说了什么话我都拷贝一份放到备忘录里面保存,这样可以随时察看以前的记录啦。

  备忘录模式:备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捉住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。

19、OBSERVER—想知道咱们公司最新MM情报吗?加入公司的MM情报邮件组就行了,tom负责搜集情报,他发现的新情报不用一个一个通知我们,直接发布给邮件组,我们作为订阅者(观察者)就可以及时收到情报啦

  观察者模式:观察者模式定义了一种一队多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使他们能够自动更新自己。

20、STATE—跟MM交往时,一定要注意她的状态哦,在不同的状态时她的行为会有不同,比如你约她今天晚上去看电影,对你没兴趣的MM就会说“有事情啦”,对你不讨厌但还没喜欢上的MM就会说“好啊,不过可以带上我同事么?”,已经喜欢上你的MM就会说“几点钟?看完电影再去泡吧怎么样?”,当然你看电影过程中表现良好的话,也可以把MM的状态从不讨厌不喜欢变成喜欢哦。

  状态模式:状态模式允许一个对象在其内部状态改变的时候改变行为。这个对象看上去象是改变了它的类一样。状态模式把所研究的对象的行为包装在不同的状态对象里,每一个状态对象都属于一个抽象状态类的一个子类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之改变。状态模式需要对每一个系统可能取得的状态创立一个状态类的子类。当系统的状态变化时,系统便改变所选的子类。

21、STRATEGY—跟不同类型的MM约会,要用不同的策略,有的请电影比较好,有的则去吃小吃效果不错,有的去海边浪漫最合适,单目的都是为了得到MM的芳心,我的追MM锦囊中有好多Strategy哦。

  策略模式:策略模式针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。策略模式把行为和环境分开。环境类负责维持和查询行为类,各种算法在具体的策略类中提供。由于算法和环境独立开来,算法的增减,修改都不会影响到环境和客户端。

22、TEMPLATE METHOD——看过《如何说服女生上床》这部经典文章吗?女生从认识到上床的不变的步骤分为巧遇、打破僵局、展开追求、接吻、前戏、动手、爱抚、进去八大步骤(Template method),但每个步骤针对不同的情况,都有不一样的做法,这就要看你随机应变啦(具体实现);

  模板方法模式:模板方法模式准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先制定一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。

23、VISITOR—情人节到了,要给每个MM送一束鲜花和一张卡片,可是每个MM送的花都要针对她个人的特点,每张卡片也要根据个人的特点来挑,我一个人哪搞得清楚,还是找花店老板和礼品店老板做一下Visitor,让花店老板根据MM的特点选一束花,让礼品店老板也根据每个人特点选一张卡,这样就轻松多了;

  访问者模式:访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由的演化。访问者模式使得增加新的操作变的很容易,就是增加一个新的访问者类。访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的节点类中。当使用访问者模式时,要将尽可能多的对象浏览逻辑放在访问者类中,而不是放到它的子类中。访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。

]]>
Fri,27 Jun 2008 14:06:06 CST 0
<![CDATA[利用 HttpClient 实现 WI/SSO 中的 Eager Sign in]]> .html

利用 HttpClient 实现 WI/SSO 中的 Eager Sign in

developerWorks
文档选项
将打印机的版面设置成横向打印模式

打印本页

将此页作为电子邮件发送

将此页作为电子邮件发送

级别: 中级

赵 汇川 (zhaohuic@cn.ibm.com), 软件工程师, IBM 中国软件开发中心
于 静 (yujjing@cn.ibm.com), 软件工程师, IBM 中国软件开发中心

2008 年 1 月 31 日

本文首先简单介绍了 IBM WI/SSO 的基本概念及其实现方式,在此基础上着重讨论 Eager Sign in 登录认证方式,指出和分析 Eager Sign in 的一些问题,并且提供相应的解决方案。WI/SSO 默认的 Eager Sign in 将用户认证信息直接提交到 WebSEAL 提供的认证表单,缺乏灵活性以及适用性。本文的解决方案在自定义登录页面和 WebSEAL 认证表单之间加入了中间过程,将该登录认证过程分为两步提交:1)提供一个自定义登录页面和 Servlet 用来收集用户认证信息,通常是用户名和口令;2)在服务器端将该 Servlet 收集到的认证信息连同必要的 HTTP 请求数据通过 HttpClient 一同提交到 WebSEAL 的认证表单,并根据 WebSEAL 返回的结果进行相应的处理。如果在认证过程中发生任何错误,通过结果处理可以转到自定义的错误页面,避免 WebSEAL 默认的认证错误处理。该解决方案的优点在于其灵活性以及适用性,可以有效地提升用户体验。

SSO 与 IBM WI/SSO

SSO 介绍

SSO 基本概念

单点登录(Single Sign On,简称为 SSO),是目前比较流行的企业业务整合的解决方案之一。利用 SSO,用户只需在应用服务器上登录一次,便可以访问启用了 SSO 域中的任何应用服务器,而无需再次登录。其实质就是获得对企业资源的访问权限。

单点登录不仅是企业应用集成(EAI)的一个重要方面,还是 SOA 按需时代的要求之一。它不仅降低安全风险和管理消耗,并且大大简化 SOA 的安全问题,从而提高服务之间的合作效率。

SSO 实现

SSO 并不是 Java EE 中的标准实现,而是各中间件提供商在提供 Java EE 应用服务器集群时提供的一种共享认证信息的机制,所以各厂商提供的实现方式不一样。例如 IBM 的 WebSphere 是通过 Cookies 记录认证信息,BEA 的 WebLogic 通过 Session 共享技术实现认证信息的共享,国内深圳金蝶的 Apusic 采用的技术和 BEA WebLogic 基本一致。

IBM WI/SSO 介绍

WI/SSO 基本概念及原理

IBM 提供的 SSO 解决方案——WI/SSO(Web Identity/Single Sign On)是 IBM 为其外部应用提供的标准用户服务,用来对 ibm.com 的用户进行注册,授权以及管理。它允许用户通过 IBM Tivoli Access Manager(TAM) 中的 WebSEAL 逆向代理安全服务器组件访问被保护的应用程序。WebSEAL 会对用户进行统一认证,并且管理用户认证信息。


图 1 WI/SSO 体系结构
WI/SSO 体系结构

如图 1 所示,WebSEAL 位于外部和内部防火墙之间的非保护区(Demilitarized Zone,DMZ),用来向绿色区域的 Web 服务器转发请求。它会一直跟踪受保护的 IP 地址,接收发往 Web 服务器或应用服务器的请求,提供认证和授权服务,并在发送请求前把该请求的地址更改为实际目标地址转发出去。

图 2 以一个典型的用户登录会话为例,描述了 WI/SSO 认证过程。


图 2 典型的用户登录会话场景
 典型的用户登录会话场景
  1. 首先,用户请求 ibm.com 域中任何被 WebSEAL 保护的资源,并将用户名 / 口令或者证书提交给 WebSEAL,要求认证;
  2. WebSEAL 利用 LDAP 注册表中存储的用户信息对用户进行认证;
  3. WebSEAL 签发一个 LTPA 令牌,在本地缓存并发送给 Web 服务器,最后发送到应用服务器;
  4. 应用服务器接受令牌并允许用户访问受保护的资源;WebSEAL 通过将 SSL 会话标识或者用户请求带来的的其它 Cookie 映射到 LTPA 令牌来维护状态,并且将它们发送到应用服务器。

在整个认证过程中, WI/SSO 还提供一些用于单点登录的标准静态页面,例如登录页面、帐户被锁页面等等。通常这些静态页面用来重定向到 WI 用户服务应用中相应 JSP 页面,同时设置一些 Cookie 的值。表 1 列出了几个重要的静态 HTML 页面与 JSP 页面、Cookie 之间的对应关系。


表 1. WI/SSO 的几个标准页面
静态页面相应的 JSP设置的 Cookies描述
login.htmllogin.jsp?persistParge=truePD-ERR=%ERROR_CODE%
PD-REFPAGE=%URL%
PD-REFERER=%REFERER%
登录页面
acct_locked.htmlacc_locked.jspPD-REFPAGE=%URL%五次登录,帐户被锁
help.htmlhelp.jspPD-REFPAGE=%URL%已登录用户再次登录

 

WI/SSO 的登录认证方式

IBM WI/SSO 方案提供两种基本登录认证方式:

  • 标准登录认证

    使用标准方式登录认证时,在应用中页面中需要添加一个标记为类似“现在登录”的链接,指向受 WebSEAL 保护的任何资源,点击该链接将自动导向到 WI/SSO 登录页面(如图 3 所示)。



    图 3 WI/SSO 登录页面
    WI/SSO 登录页面

    由图 3 可知,标准登录认证方式的登录页面是由 WebSEAL 统一提供的,这势必会导致在对遗留系统进行 SSO 方案集成时,登录、修改密码、出错等页面风格与企业应用风格不一致的问题,同时也会给用户造成一定的困惑。

    为解决此问题,WI/SSO 中还提供另一种更为灵活的登录认证方式,这就是 Eager Sign in。

  • Eager Sign in 登录认证

    Eager Sign in 登录认证方式允许应用提供根据企业应用风格自定义的登录页面,不再使用 WebSEAL 提供的标准登录页面,并且以 WebSEAL 所认可的方式提交认证请求。之后,WebSEAL 会对用户提交的认证信息进行认证,如果认证成功,会将用户重定向到应用指定的目标页面;否则,将用户重定向到 WebSEAL 标准的错误页面或者相应的登录 / 修改口令 / 用户被锁页面。

     

    通过上面的介绍可以看到,Eager Sign in 登录认证方式的登录页面可以由企业应用自身提供,保持了遗留应用系统的整体风格,从而带来更完美的用户体验。因此,本文后面部分将着重介绍这种登录认证方式,并结合其不足提出改进的解决方案。




回页首


WI/SSO Eager Sign in

Eager Sign in

Eager Sign in 登录认证方式要求用户根据企业应用整体风格自定义登录页面。为了以 WebSEAL 所认可的方式提交用户认证请求,需要在自定义的登录页面中加入一个 Form 表单,如下所示:

<form action="../../pkmslogin.form" method="POST" 
 enctype="application/www-x-form-urlencoded">

该表单用来收集用户名和口令,并以设定的方式将用户名称 username 和口令 password 等参数 POST 到 WebSEAL 的标准登录表单 pkmslogin.form。WebSEAL 对用户进行认证,根据 WebSEAL 的配置将用户重定向到 loginredir.jsp 页面。该页面用来设置 IBMISS/IBMISP Cookie,并且将用户重定向到应用在 PD-SGNPAGE Cookie 指定的目标页面。至此,成功完成了整个 Eager Sign in 登录认证过程。

但是,如果在登录过程中出现任何错误,WebSEAL 会将用户自动重定向到标准的登录错误处理页面,比如标准的登录页面、修改口令页面、用户被锁页面等。下一小节将重点讨论 Eager Sign in 的不足并加以分析。

Eager Sign in 缺点

虽然 Eager Sign in 登录认证方式允许用户自定义登录页面,并且指定登录成功后所转向的目标页面,但是却不允许用户定制登录失败后的出错处理页面。显然这在某种程度上会影响用户的网络体验,主要体现为如下几个方面:

  • 用户登录认证失败,页面会自动转向到 WI/SSO 所定义的标准登录页面,应用程序不能自定义错误处理页面以及控制页面转向流程;
  • 当登录失败尝试超过五次时,会自动转向到标准的帐户锁定页面,页面并不十分友好。同样的,应用程序也不能自定义帐户锁定提示页面;
  • 对所保护应用页面的请求都必须通过 WebSEAL 逆向代理服务器转向,降低了应用的性能;
  • 不能和企业已有应用系统的认证模块相结合,企业应用整合困难;
  • 将认证信息直接提交到 WebSEAL 提供的认证表单,缺乏灵活性和适用性。



回页首


分步登录认证方案

为解决 Eager Sign in 的几个问题,本文提供了利用 HttpClient 实现 WI/SSO Eager Sign in 的分步登录认证方案——在自定义登录页面和 WebSEAL 认证表单之间加入了中间过程,将用户登录认证过程分为两步提交。首先,应用提供自定义的登录页面自身用于采集用户认证信息,一般包括用户名和口令;其次,在服务器端将采集到的认证信息以及必要的 HTTP 请求数据通过 HttpClient 提交到 WebSEAL 的认证表单,并对认证结果分析处理。如果在认证过程中发生任何错误 , 通过结果处理可以转到自定义的错误页面,避免 WebSEAL 默认的认证错误处理。该解决方案的优点在于其灵活性以及适用性,可以有效地提升用户体验。

HttpClint 介绍

HTTP 协议可能是现在 Internet 上使用得最多、最重要的协议了,越来越多的 Java 应用程序需要直接通过 HTTP 协议来访问网络资源。虽然 java.net 工具包提供了通过 HTTP 访问互联网资源的基本功能,但它并不完全满足企业应用程序所需要的丰富性和灵活性要求。Apache HttpClient 提供了一个高效、快速更新、功能丰富的支持 HTTP(S) 协议的客户端编程工具,极大地方便网络访问应用开发。

HttpClient 的主要功能包括以下几个方面,更多详细的功能可以参见 HttpClient 的主页 http://commons.apache.org/httpclient

  • 实现了所有 HTTP 的方法(GET,POST,PUT,HEAD 等)
  • 支持自动转向
  • 支持 HTTPS 协议

分步认证方案

针对 Eager Sign in 登录认证方式不能控制登录流程等问题,本文提出了分步登录认证的方案,目的是利用 HttpClient 模拟客户端浏览器与 WebSEAL 进行交互,从而控制认证流程。若用户未通过认证,由 HttpClient 捕获 WebSEAL 响应的 HTTP 信息,并且对认证结果进行分析处理,最终将用户重定向到应用提供的登录处理页面,以获得极大的灵活性和适用性。该分步认证方案不仅可以控制 WebSEAL 认证流程,而且可以和企业应用很好的配合,从而兼容遗留认证模块。一旦认证成功,改进的登录认证方案就可以减少客户端浏览器与 WebSEAL 的交互,进而提高应用效率。

下面详细介绍利用 HttpClient 实现 WI/SSO 登录的分步认证策略:

第一步,提供自定义登录页面用以采集用户认证信息,包括用户名以及口令,设置 WebSEAL 认证表单需要的请求参数。

用户自定义的登录页面表单中的参数需要满足如下一些条件:

  • 表单要 POST 到第二步所使用的 Servlet,也就是说表单使用这个 Servlet 为 Action
  • POST 请求主体必须包含如下三个参数:
    • 用户名 username
    • 口令 password
    • 登录表单的类型 login-form-type
  • 以用户名 / 口令方式认证时,login-form-type 的值必须为 "pwd"
  • content-length 请求头必须与请求体的长度相等

例如,使用 telenet 提交认证信息数据:

prompt>telnet webseal.example.com 80
Connected to webseal.example.com.
Escape character is '^]'.
POST /myServlet HTTP/1.1
host: webseal.webseal.com
content-length: 56

username=testuser&password=my0passwd&login-form-type=pwd

其中,login-form-type 指定了用户登录方式,例如:

用户名 / 口令登录表单: <INPUT TYPE="HIDDEN" NAME="login-form-type" VALUE="pwd">

令牌登录表单: <INPUT TYPE="HIDDEN" NAME="login-form-type" VALUE="token">

自定义的认证信息采集页面应该如下面的代码片段所示:

<form action="myServlet" enctype="application/x-www-form-urlencoded" method="post">
 <input type="hidden" name="login-form-type" value="pwd"/>
 <input type="text" name="username" id="username" value=""/>
 <input type="password" name="password" id="password" value=""/>
</form>

 

第二步,将采集到的认证信息用户名和口令参数连同必要的 Cookie、Header 提交到 WebSEAL 的认证表单,并对返回结果进行分析处理。具体如下:

(1) 在向 WebSEAL 认证表单提交认证信息之前,首先利用 HttpClient Get 方法尝试获取一个受保护的页面,用于判断该用户是否已经登录。如果返回响应状态是 SC_OK 200,并且返回内容为所访问的受保护页面,则说明该页面已找到,用户可以直接访问到受保护的资源,即用户已经成功登录过,不需要再次提交认证信息;否则说明该用户没有登录,需要继续执行(2),向 WebSEAL 提交认证信息。

(2) 在提交认证请求之前,为了能控制页面转向流程,首先需要禁止 HttpClient Post 方法的自动转向功能:

将登录页面采集到的认证信息如用户名和口令参数连同必要的 Cookie、Header 拷贝到 postMethod 中。值得注意的是,为了符合 WebSEAL 的规范,定制登录成功页面,需要为 HttpClient 额外添加两个会话 Cookie,分别是 PD-SGNPAGE 和 PD-FROMPAGE。其中,

1. PD-SGNPAGE ——指定用户认证成功后所导向的目标页面编码后的 URL;

2. PD-FROMPAGE ——指定用户选择“取消”后所显示页面编码后的 URL。它必须是一个没有被保护的页面,并且可能是与目前所显示页面相同的页面(带有登录表单的页面)。

(3) 提交 postMethod 并判断返回状态代码:

int statusCode = ssoHttpClient.executeMethod(postMethod);

如果返回 SC_OK 200 状态,说明登录失败或者已经登录,转入认证失败处理,要么是无效的用户名 / 口令,要么是多次尝试失败被锁,要么是已经登录再次提交认证信息;如果返回 SC_MOVED_TEMPORARILY 302 状态,说明登录成功,将会重定向到 loginredir.jsp 页面,继续请求重定向页面,最终会重定向到受保护的目标页面。

其中,HTTP 的返回代码为 200 说明页面已经找到,对 GET 和 POST 请求的应答文档跟在后面。返回代码为 302 是暂时重定向,说明所请求的资源在一个不同的 URL 处临时保存。出现该状态代码时,HttpClient 的方法(和客户端浏览器类似)一般会自动访问 HTTP 请求头中 Location 头指定的 URL。将 HttpClient 的方法自动转向功能取消掉,所以需要再次发起请求才能完成重定向,这带来的好处是我们可以控制页面的转向。

认证结果分析处理

认证成功


图 4 认证成功流程
 认证成功流程

将认证信息提交到 WebSEAL 认证表单后,如果认证成功,则执行如图 4 所示的流程。请求 loginredir.jsp 页面,并判断返回状态代码,如果页面请求成功,会再次返回 SC_MOVED_TEMPORARILY 302 状态。同时,loginredir.jsp 还会根据用户标识设置 IBMISS 和 IBMISP 两个 Cookie 值,并重定向到 PD-SGNPAGE Cookie 中指定的登录成功后显示的目标页面。

继续请求用户指定的登录成功后显示的目标页面,并判断返回状态代码,如果请求成功,返回 SC_OK 200 状态,说明目标页面已经找到,可以访问受 WebSEAL 保护的页面。

为了将目标页面返回给用户浏览器端,还需要将 postMethod 中的 Header 和 HttpClient 获取到的 Cookie 加到 HttpServletResponse 中以发送给客户端浏览器。至此,用户完成登录认证,并定向到登录成功页面。

认证失败

图 5 描述了用户登录失败的情况。可以看到,由于分步登录方案人为控制认证流程,Servlet 可以捕获 WebSEAL 重定向到标准错误页面的响应,从而可以导向到应用自定义的错误页面。这样就避免了 Eager sign in 不能自定义错误处理页面的问题。


图 5 认证失败流程
 认证失败流程

将认证信息提交到 WebSEAL 认证表单后,如果认证失败,返回 SC_OK 200 状态,则执行如图 5 所示的流程。

WebSEAL 返回的响应页面内容如下:

<html>
<head>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Cache-Control" content="no-cache" />
<meta http-equiv="Expires" content="0" />

<meta http-equiv="Set-Cookie" content="PD-REFPAGE=/pkmslogin.form; 
 path=/; domain=.ibm.com" />
<meta http-equiv="Set-Cookie" content="
 PD-REFERER=https://webauth.ibm.com/SSOWithLTPA/login.jsp; path=/; domain=.ibm.com" />
<meta http-equiv="Set-Cookie" content="PD-ERR=0x132120c8; path=/; domain=.ibm.com" />

<meta http-equiv="Refresh" content="0; url=
 https://www-304.ibm.com/usrsrvc/account/userservices/jsp/login.jsp?persistPage=true" />

<title>IBM Registration>/title>
</head>
</html>

可以看出,该页面的作用是设置 PD-REFPAGE、PD-REFERER 和 PD-ERR 三个 Cookie 值,并且交由客户端自动刷新页面到请求头中所指定的 URL,可能是要求再次登录或者帐户被锁的页面。在这里,我们可以通过匹配自动刷新的 URL 地址的方法判断用户认证的失败原因,包括如下两种情况:无效的用户名 / 口令,多次失败尝试用户被锁。另外还有一种情况是:用户已经登录但仍然提交认证信息到 WebSEAL 认证表单,自动刷新的目标 URL 会是 help.jsp。

其中,PD-REFPAGE 代表了用户所请求的页面或 WebSEAL 缓存的页面 url。这个 url 认证成功后 WebSEAL 所应该请求的页面。

PD-REFERER 代表了请求从哪儿来的页面,或是 HTTP referrer 请求头。如果用户在地址栏手动输入地址,或是使用书签触发一个登录的流程,该 Cookie 的值即为空“none”,意味着不能判定 referrer 请求头。

PD-ERR 通过登录或账户管理页面设置。该 Cookie 的值即错误编码用来查找错误信息。

认证失败的情况如下:

  • 无效的用户名 / 口令

    如果用户提交了非法或者无效的用户名 / 口令,则 WebSEAL 返回自动刷新到要求再次登录的页面,刷新的目标 URL 是 https://www-304.ibm.com/usrsrvc/account/userservices/jsp/login.jsp?persistPage=true

  • 多次登录失败尝试

    用户多次输入错误的用户名 / 口令,提交到 WebSEAL 认证表单,则 WebSEAL 返回自动刷新到提示用户被锁的页面,刷新的目标 URL 是 https://www-304.ibm.com/usrsrvc/account/userservices/jsp/acct_locked.jsp

  • 已经登录再次提交认证信息

    如果用户已经成功登录并且再次提交认证信息,则 WebSEAL 返回自动刷新到提示用户已经登录的页面,刷新的目标 URL 是 https://www-304.ibm.com/usrsrvc/account/userservices/jsp/help.jsp

最后,需要 Get 方法中的 Header 和 HttpClient 的 Cookie 一并加入 HttpServletResponse 中返回给用户,并根据分析的失败原因,重新请求应用中定义的相应错误处理页面。

从上面的介绍中可以看出,该方案保留了 Eager Sign in 方式自定义登录页面的优点,能够很好地和遗留系统的页面风格保持一致。还针对 Eager Sign in 方式缺少灵活性和适用性的问题,使用分步登录认证的策略,即使用 HttpClient 模拟客户端浏览器与 WebSEAL 进行交互认证。一旦在该过程中发生认证错误,HttpClient 能捕获该错误并重定向到应用本身的错误处理页面,这样也实现了错误页面的定制,提升了用户体验。

此外,由于分步登录的方案能够人为控制页面转向流程,使用户有可能在 WI/SSO 方案之外根据企业已有应用系统的认证策略增加一些安全措施,减少了迁移的工作量,做到了与遗留系统认证模块的完美结合。并且,由于集成了已有的认证策略,用户在登录之后,不必将受保护应用的所有页面请求都通过 WebSEAL 逆向代理服务器转向,从而提高了应用的性能。

遇到的问题及解决办法

下面介绍在利用 HttpClient 实现 WI/SSO Eager Sign in 过程中可能遇到的一些问题及其解决办法。

字符编码和 Content-Length 请求头

自定义登录页面的编码方式可能出现在不同地方,一是服务器返回的 HTTP 头中,二是得到的 html/xml 页面中。为正确读取认证信息,在服务器端读取用户提交的信息时必须指定字符编码。从请求中获取参数时,设置字符编码方式一般是:

httpRequest.setCharacterEncoding("UTF-8");

一旦设置完请求的编码方式,则会按照 UTF-8 的方式读取用户提交的参数值。

将参数通过 HttpClient 接口添加到 Post 方法后,HttpClient 会对整个提交的内容进行编码,从而导致实际的内容长度和请求 Content-Length 头不一致,所以最好在处理请求头时忽略 Content-Length 头。或者调用 HttpClient 接口对内容进行编码后获取新的内容长度,从而保证传入的 Content-Length 值和实际内容长度一致,以便能够正确的提交到 WebSEAL 认证表单。

String content = EncodingUtil.formUrlEncode(postMethod.getParameters(), 
 postMethod.getRequestCharSet());
postMethod.setRequestHeader("Content-Length", ""   content.length());

Cookie 处理

HttpClient Cookie 规范参数

在 HttpClient 中有两种方法来指定 Cookie 规范的使用:

HttpClient client = new HttpClient();
client.getState().setCookiePolicy(CookiePolicy.COMPATIBILITY);

这种方法设置的规范只对当前 HttpState 有效。另一种方法是指定系统的属性,对每个新建立的 HttpState 对象都有效。

System.setProperty(“apache.commons.httpclient.cookiespec”, “COMPATIBILITY”);

PD-ERR 和 PD-ID

用户认证成功后,会生成 PD-ID Cookie,当该 Cookie 被添加到客户浏览器之后,任何到受 SSO 保护的应用的请求都会附上该 Cookie。同样,用户输入了无效的用户名 / 口令提交到 WebSEAL 认证表单进行认证时,会生成 PD-ERR Cookie,除非显式地清除,该 Cookie 会一直存在客户端的请求中直到当前会话结束。

用户认证失败后,SSO 生成 PD-ERR 同时清除 PD-ID;用户再次提交正确的用户名 / 口令 , 认证成功后,SSO 会生成 PD-ID 同时清除 PD-ERR Cookie。所以我们需要在服务器端对客户浏览器的请求的 Cookie 和 HttpClient 认证结果中的 Cookie 进行同步,以到达认证状态的一致性。

Cookie 域

WI/SSO 所生成的所有 Cookie 的域都被指定为 ibm.com,从而保证在 ibm.com 域下的受 SSO 保护的应用之间作边界切换时能够共享 Cookie。但是有时候从请求中获取 Cookie 时 Domain 和 Path 可能都为空,所以不得不重置这些 Cookie,将其 Domain 设置为 ibm.com,Path 设置为“/”,这样,发送到 ibm.com 域下所有应用的请求都会附上这些 Cookie。

支持 HTTPS 协议

WI/SSO 依赖于 SSL 进行用户信息的传递。HttpClient 提供对 SSL 的支持,但是在使用 SSL 之前必须安装 JSSE。这里有两种方法可以打开 HTTPS 连接,一种是将服务器颁发的证书导入到本地的 keystore 中,另一种是扩展 HttpClient 来实现自动接受证书。就第一种方法可以参考引用中的 HttpClient 入门文章,下面介绍第二种方法。

扩展 HttpClient 实现自动接受证书

因为这种方法自动接受证书,因此存在一定的安全问题,所以在使用该方法前请仔细考虑应用系统的安全需求,在真正产品中并不建议使用自动接受证书或者接受所有证书。

具体步骤如下:

  1. 提供一个自定义的 Socket factory。自定义的类必须实现 HttpClient 接口 org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory,在实现接口的类中调用自定义的 X509TrustManager,就这些类的实现可以参考 HttpClient 官方网站。
  2. 创建一个 org.apache.commons.httpclient.protocol.Protocol 实例,指定协议名称和默认的端口号:
    Protocol https = new Protocol(“https”, new MySecureProtocolSocketFactory(), 443);
  3. 注册创建的 https 协议对象,然后就可以按照普通方式打开 HTTPS 的目标地址
    Protocol.registerProtocol(“https”, https);

HttpClient 重定向

HttpClient 支持自动转向处理,但是像 POST 和 PUT 方式这种要求接受后继服务的请求方式,暂时不支持自动转向,因此如果碰到 POST 方式提交后返回的是 301 或者 302 的话需要自己处理,所要请求的地址可以在头字段 location 中得到。不过需要注意的是,有时候 location 返回的可能是相对路径,因此需要对 location 返回的值做一些处理才可以发起向新地址的请求。

另外除了在头中包含的信息可能使页面发生重定向外,在页面中也有可能会发生页面的重定向。引起页面自动转发的标签是:<meta http-equiv="refresh" content="5; url=http://www.ibm.com/us">。如果你想在程序中也处理这种情况的话得自己分析页面来实现转向。需要注意的是,在上面那个标签中 url 的值也可以是一个相对地址,如果是这样的话,需要对它做一些处理后才可以转发。

为了禁止 httpClient 自动重定向,需要在 post 之前将 httpClient 的重定向功能关闭:

httpMethod.setFollowRedirects(false);




回页首


总结

本文针对 WI/SSO 中 Eager Sign in 的登录认证方式缺少灵活性和适用性的问题提出了分步登录的认证方案。该方案不仅保留了 Eager Sign in 方式自定义登录页面的优点,还实现了错误页面的定制,结合了已有系统的认证策略,从而做到与遗留系统认证模块的完美结合,提升了用户体验。

值得注意的是,我们并不能保证本文所提出的分布登录认证方案可以完美地实现企业应用单点登录集成。如果读者希望采用本方案,请参考 IBM WI/SSO 的文档以了解更多的信息。



参考资料



作者简介

 

赵汇川是一位 IBM 软件工程师,工作在 IBM 中国软件开发实验室企业应用开发部门,在 Java 开发和 Web 开发方面有丰富的经验,现在正从事企业电子商务应用的开发和支持工作。


 

于静是一位 IBM 软件工程师,工作在 IBM 中国软件开发实验室企业应用开发部门,在 Java 开发和 Web 开发方面有丰富的经验,现在正从事企业电子商务应用的开发和支持工作。

]]>
Thu,26 Jun 2008 13:35:48 CST 0
<![CDATA[HttpClient入门]]> .html HttpClient简介

HTTP 协议可能是现在 Internet 上使用得最多、最重要的协议了,越来越多的 Java 应用程序需要直接通过 HTTP 协议来访问网络资源。虽然在 JDK 的 java.net 包中已经提供了访问 HTTP 协议的基本功能,但是对于大部分应用程序来说,JDK 库本身提供的功能还不够丰富和灵活。HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。HttpClient 已经应用在很多的项目中,比如 Apache Jakarta 上很著名的另外两个开源项目 Cactus 和 HTMLUnit 都使用了 HttpClient,更多使用 HttpClient 的应用可以参见http://wiki.apache.org/jakarta-httpclient/HttpClientPowered。HttpClient 项目非常活跃,使用的人还是非常多的。目前 HttpClient 版本是在 2005.10.11 发布的 3.0 RC4 。

 



回页首

 

HttpClient 功能介绍

以下列出的是 HttpClient 提供的主要的功能,要知道更多详细的功能可以参见 HttpClient 的主页。

  • 实现了所有 HTTP 的方法(GET,POST,PUT,HEAD 等)
  • 支持自动转向
  • 支持 HTTPS 协议
  • 支持代理服务器等

下面将逐一介绍怎样使用这些功能。首先,我们必须安装好 HttpClient。

 



回页首

 

HttpClient 基本功能的使用

GET 方法

使用 HttpClient 需要以下 6 个步骤:

1. 创建 HttpClient 的实例

2. 创建某种连接方法的实例,在这里是 GetMethod。在 GetMethod 的构造函数中传入待连接的地址

3. 调用第一步中创建好的实例的 execute 方法来执行第二步中创建好的 method 实例

4. 读 response

5. 释放连接。无论执行方法是否成功,都必须释放连接

6. 对得到后的内容进行处理

根据以上步骤,我们来编写用GET方法来取得某网页内容的代码。

  • 大部分情况下 HttpClient 默认的构造函数已经足够使用。
    HttpClient httpClient = new HttpClient();
  • 创建GET方法的实例。在GET方法的构造函数中传入待连接的地址即可。用GetMethod将会自动处理转发过程,如果想要把自动处理转发过程去掉的话,可以调用方法setFollowRedirects(false)。
    GetMethod getMethod = new GetMethod("http://www.ibm.com/");
  • 调用实例httpClient的executeMethod方法来执行getMethod。由于是执行在网络上的程序,在运行executeMethod方法的时候,需要处理两个异常,分别是HttpException和IOException。引起第一种异常的原因主要可能是在构造getMethod的时候传入的协议不对,比如不小心将"http"写成"htp",或者服务器端返回的内容不正常等,并且该异常发生是不可恢复的;第二种异常一般是由于网络原因引起的异常,对于这种异常 (IOException),HttpClient会根据你指定的恢复策略自动试着重新执行executeMethod方法。HttpClient的恢复策略可以自定义(通过实现接口HttpMethodRetryHandler来实现)。通过httpClient的方法setParameter设置你实现的恢复策略,本文中使用的是系统提供的默认恢复策略,该策略在碰到第二类异常的时候将自动重试3次。executeMethod返回值是一个整数,表示了执行该方法后服务器返回的状态码,该状态码能表示出该方法执行是否成功、需要认证或者页面发生了跳转(默认状态下GetMethod的实例是自动处理跳转的)等。
    //设置成了默认的恢复策略,在发生异常时候将自动重试3次,在这里你也可以设置成自定义的恢复策略
    getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
        		new DefaultHttpMethodRetryHandler()); 
    //执行getMethod
    int statusCode = client.executeMethod(getMethod);
    if (statusCode != HttpStatus.SC_OK) {
      System.err.println("Method failed: "   getMethod.getStatusLine());
    }
  • 在返回的状态码正确后,即可取得内容。取得目标地址的内容有三种方法:第一种,getResponseBody,该方法返回的是目标的二进制的byte流;第二种,getResponseBodyAsString,这个方法返回的是String类型,值得注意的是该方法返回的String的编码是根据系统默认的编码方式,所以返回的String值可能编码类型有误,在本文的"字符编码"部分中将对此做详细介绍;第三种,getResponseBodyAsStream,这个方法对于目标地址中有大量数据需要传输是最佳的。在这里我们使用了最简单的getResponseBody方法。
    byte[] responseBody = method.getResponseBody();
  • 释放连接。无论执行方法是否成功,都必须释放连接。
    method.releaseConnection();
  • 处理内容。在这一步中根据你的需要处理内容,在例子中只是简单的将内容打印到控制台。
    System.out.println(new String(responseBody));

下面是程序的完整代码,这些代码也可在附件中的test.GetSample中找到。

 

package test;
import java.io.IOException;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
public class GetSample{
  public static void main(String[] args) {
  //构造HttpClient的实例
  HttpClient httpClient = new HttpClient();
  //创建GET方法的实例
  GetMethod getMethod = new GetMethod("http://www.ibm.com");
  //使用系统提供的默认的恢复策略
  getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
    new DefaultHttpMethodRetryHandler());
  try {
   //执行getMethod
   int statusCode = httpClient.executeMethod(getMethod);
   if (statusCode != HttpStatus.SC_OK) {
    System.err.println("Method failed: "
        getMethod.getStatusLine());
   }
   //读取内容 
   byte[] responseBody = getMethod.getResponseBody();
   //处理内容
   System.out.println(new String(responseBody));
  } catch (HttpException e) {
   //发生致命的异常,可能是协议不对或者返回的内容有问题
   System.out.println("Please check your provided http address!");
   e.printStackTrace();
  } catch (IOException e) {
   //发生网络异常
   e.printStackTrace();
  } finally {
   //释放连接
   getMethod.releaseConnection();
  }
 }
}

 

POST方法

根据RFC2616,对POST的解释如下:POST方法用来向目的服务器发出请求,要求它接受被附在请求后的实体,并把它当作请求队列(Request-Line)中请求URI所指定资源的附加新子项。POST被设计成用统一的方法实现下列功能:

  • 对现有资源的注释(Annotation of existing resources)
  • 向电子公告栏、新闻组,邮件列表或类似讨论组发送消息
  • 提交数据块,如将表单的结果提交给数据处理过程
  • 通过附加操作来扩展数据库

调用HttpClient中的PostMethod与GetMethod类似,除了设置PostMethod的实例与GetMethod有些不同之外,剩下的步骤都差不多。在下面的例子中,省去了与GetMethod相同的步骤,只说明与上面不同的地方,并以登录清华大学BBS为例子进行说明。

  • 构造PostMethod之前的步骤都相同,与GetMethod一样,构造PostMethod也需要一个URI参数,在本例中,登录的地址是http://www.newsmth.net/bbslogin2.php。在创建了PostMethod的实例之后,需要给method实例填充表单的值,在BBS的登录表单中需要有两个域,第一个是用户名(域名叫id),第二个是密码(域名叫passwd)。表单中的域用类NameValuePair来表示,该类的构造函数第一个参数是域名,第二参数是该域的值;将表单所有的值设置到PostMethod中用方法setRequestBody。另外由于BBS登录成功后会转向另外一个页面,但是HttpClient对于要求接受后继服务的请求,比如POST和PUT,不支持自动转发,因此需要自己对页面转向做处理。具体的页面转向处理请参见下面的"自动转向"部分。代码如下:
    String url = "http://www.newsmth.net/bbslogin2.php";
    PostMethod postMethod = new PostMethod(url);
    // 填入各个表单域的值
    NameValuePair[] data = { new NameValuePair("id", "youUserName"),				
    new NameValuePair("passwd", "yourPwd") };
    // 将表单的值放入postMethod中
    postMethod.setRequestBody(data);
    // 执行postMethod
    int statusCode = httpClient.executeMethod(postMethod);
    // HttpClient对于要求接受后继服务的请求,象POST和PUT等不能自动处理转发
    // 301或者302
    if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || 
    statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
        // 从头中取出转向的地址
        Header locationHeader = postMethod.getResponseHeader("location");
        String location = null;
        if (locationHeader != null) {
         location = locationHeader.getValue();
         System.out.println("The page was redirected to:"   location);
        } else {
         System.err.println("Location field value is null.");
        }
        return;
    }

完整的程序代码请参见附件中的test.PostSample

 



回页首

 

使用HttpClient过程中常见的一些问题

下面介绍在使用HttpClient过程中常见的一些问题。

字符编码

某目标页的编码可能出现在两个地方,第一个地方是服务器返回的http头中,另外一个地方是得到的html/xml页面中。

  • 在http头的Content-Type字段可能会包含字符编码信息。例如可能返回的头会包含这样子的信息:Content-Type: text/html; charset=UTF-8。这个头信息表明该页的编码是UTF-8,但是服务器返回的头信息未必与内容能匹配上。比如对于一些双字节语言国家,可能服务器返回的编码类型是UTF-8,但真正的内容却不是UTF-8编码的,因此需要在另外的地方去得到页面的编码信息;但是如果服务器返回的编码不是UTF-8,而是具体的一些编码,比如gb2312等,那服务器返回的可能是正确的编码信息。通过method对象的getResponseCharSet()方法就可以得到http头中的编码信息。
  • 对于象xml或者html这样的文件,允许作者在页面中直接指定编码类型。比如在html中会有<meta http-equiv="Content-Type" content="text/html; charset=gb2312"/>这样的标签;或者在xml中会有<?xml version="1.0" encoding="gb2312"?>这样的标签,在这些情况下,可能与http头中返回的编码信息冲突,需要用户自己判断到底那种编码类型应该是真正的编码。

自动转向

根据RFC2616中对自动转向的定义,主要有两种:301和302。301表示永久的移走(Moved Permanently),当返回的是301,则表示请求的资源已经被移到一个固定的新地方,任何向该地址发起请求都会被转到新的地址上。302表示暂时的转向,比如在服务器端的servlet程序调用了sendRedirect方法,则在客户端就会得到一个302的代码,这时服务器返回的头信息中location的值就是sendRedirect转向的目标地址。

HttpClient支持自动转向处理,但是象POST和PUT方式这种要求接受后继服务的请求方式,暂时不支持自动转向,因此如果碰到POST方式提交后返回的是301或者302的话需要自己处理。就像刚才在POSTMethod中举的例子:如果想进入登录BBS后的页面,必须重新发起登录的请求,请求的地址可以在头字段location中得到。不过需要注意的是,有时候location返回的可能是相对路径,因此需要对location返回的值做一些处理才可以发起向新地址的请求。

另外除了在头中包含的信息可能使页面发生重定向外,在页面中也有可能会发生页面的重定向。引起页面自动转发的标签是:<meta http-equiv="refresh" content="5; url=http://www.ibm.com/us">。如果你想在程序中也处理这种情况的话得自己分析页面来实现转向。需要注意的是,在上面那个标签中url的值也可以是一个相对地址,如果是这样的话,需要对它做一些处理后才可以转发。

处理HTTPS协议

HttpClient提供了对SSL的支持,在使用SSL之前必须安装JSSE。在Sun提供的1.4以后的版本中,JSSE已经集成到JDK中,如果你使用的是JDK1.4以前的版本则必须安装JSSE。JSSE不同的厂家有不同的实现。下面介绍怎么使用HttpClient来打开Https连接。这里有两种方法可以打开https连接,第一种就是得到服务器颁发的证书,然后导入到本地的keystore中;另外一种办法就是通过扩展HttpClient的类来实现自动接受证书。

方法1,取得证书,并导入本地的keystore:

  • 安装JSSE (如果你使用的JDK版本是1.4或者1.4以上就可以跳过这一步)。本文以IBM的JSSE为例子说明。先到IBM网站上下载JSSE的安装包。然后解压开之后将ibmjsse.jar包拷贝到<java-home>\lib\ext\目录下。
  • 取得并且导入证书。证书可以通过IE来获得:

    1. 用IE打开需要连接的https网址,会弹出如下对话框:



    2. 单击"View Certificate",在弹出的对话框中选择"Details",然后再单击"Copy to File",根据提供的向导生成待访问网页的证书文件



    3. 向导第一步,欢迎界面,直接单击"Next",



    4. 向导第二步,选择导出的文件格式,默认,单击"Next",



    5. 向导第三步,输入导出的文件名,输入后,单击"Next",



    6. 向导第四步,单击"Finish",完成向导



    7. 最后弹出一个对话框,显示导出成功


  • 用keytool工具把刚才导出的证书倒入本地keystore。Keytool命令在<java-home>\bin\下,打开命令行窗口,并到<java-home>\lib\security\目录下,运行下面的命令:

    keytool -import -noprompt -keystore cacerts -storepass changeit -alias yourEntry1 -file your.cer

    其中参数alias后跟的值是当前证书在keystore中的唯一标识符,但是大小写不区分;参数file后跟的是刚才通过IE导出的证书所在的路径和文件名;如果你想删除刚才导入到keystore的证书,可以用命令:

    keytool -delete -keystore cacerts -storepass changeit -alias yourEntry1
  • 写程序访问https地址。如果想测试是否能连上https,只需要稍改一下GetSample例子,把请求的目标变成一个https地址。
    GetMethod getMethod = new GetMethod("https://www.yourdomain.com");

    运行该程序可能出现的问题:

    1. 抛出异常java.net.SocketException: Algorithm SSL not available。出现这个异常可能是因为没有加JSSEProvider,如果用的是IBM的JSSE Provider,在程序中加入这样的一行:

    if(Security.getProvider("com.ibm.jsse.IBMJSSEProvider") == null)
     Security.addProvider(new IBMJSSEProvider());

    或者也可以打开<java-home>\lib\security\java.security,在行

    security.provider.1=sun.security.provider.Sun
    security.provider.2=com.ibm.crypto.provider.IBMJCE

    后面加入security.provider.3=com.ibm.jsse.IBMJSSEProvider

    2. 抛出异常java.net.SocketException: SSL implementation not available。出现这个异常可能是你没有把ibmjsse.jar拷贝到<java-home>\lib\ext\目录下。

    3. 抛出异常javax.net.ssl.SSLHandshakeException: unknown certificate。出现这个异常表明你的JSSE应该已经安装正确,但是可能因为你没有把证书导入到当前运行JRE的keystore中,请按照前面介绍的步骤来导入你的证书。

方法2,扩展HttpClient类实现自动接受证书

因为这种方法自动接收所有证书,因此存在一定的安全问题,所以在使用这种方法前请仔细考虑您的系统的安全需求。具体的步骤如下:

  • 提供一个自定义的socket factory(test.MySecureProtocolSocketFactory)。这个自定义的类必须实现接口org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory,在实现接口的类中调用自定义的X509TrustManager(test.MyX509TrustManager),这两个类可以在随本文带的附件中得到
  • 创建一个org.apache.commons.httpclient.protocol.Protocol的实例,指定协议名称和默认的端口号
    Protocol myhttps = new Protocol("https", new MySecureProtocolSocketFactory (), 443);
  • 注册刚才创建的https协议对象
    Protocol.registerProtocol("https ", myhttps);
  • 然后按照普通编程方式打开https的目标地址,代码请参见test.NoCertificationHttpsGetSample

处理代理服务器

HttpClient中使用代理服务器非常简单,调用HttpClient中setProxy方法就可以,方法的第一个参数是代理服务器地址,第二个参数是端口号。另外HttpClient也支持SOCKS代理。

 

httpClient.getHostConfiguration().setProxy(hostName,port);

 



回页首

 

结论

从上面的介绍中,可以知道HttpClient对http协议支持非常好,使用起来很简单,版本更新快,功能也很强大,具有足够的灵活性和扩展性。对于想在Java应用中直接访问http资源的编程人员来说,HttpClient是一个不可多得的好工具。

 

参考资料

  • Commons logging包含了各种各样的日志API的实现,读者可以通过站点http://jakarta.apache.org/commons/logging/得到详细的内容
  • Commons codec包含了一些一般的解码/编码算法。包含了语音编码、十六进制、Base64和URL编码等,通过http://jakarta.apache.org/commons/codec/可以得到详细的内容
  • rfc2616是关于HTTP/1.1的文档,可以在http://www.faqs.org/rfcs/rfc2616.html上得到详细的内容,另外rfc1945是关于HTTP/1.0的文档,通过http://www.faqs.org/rfcs/rfc1945.html可以得到详细内容
  • SSL――SSL 是由 Netscape Communications Corporation 于 1994 年开发的,而 TLS V1.0 是由 Internet Engineering Task Force(IETF)定义的标准,它基于 SSL V3.0,并且在使用的加密算法上与其有些许的不同。例如,SSL 使用 Message Authentication Code(MAC)算法来生成完整性校验值,而 TLS 应用密钥的 Hashing for Message Authentication Code(HMAC)算法。
  • IBM JSSE提供了SSL(Secure Sockets Layer)和TLS(Transport Layer Security)的java实现,在http://www-03.ibm.com/servers/eserver/zseries/software/java/jsse.html中可以得到详细的信息
  • Keytool是一个管理密钥和证书的工具。关于它详细的使用信息可以在http://www.doc.ic.ac.uk/csg/java/1.3.1docs/tooldocs/solaris/keytool.html上得到
  • HTTPClient的主页是http://jakarta.apache.org/commons/httpclient/,你可以在这里得到关于HttpClient更加详细的信息

 

作者简介

 

金发华是一名工作在 IBM CSDL 的软件工程师。他喜欢钻研各种新的技术,在 Java 网络开发和 Web 开发方面颇有经验。

 

 

陈樟洪是一位 IBM CSDL 的软件工程师,目前从事企业电子商务应用的开发。

]]>
Thu,26 Jun 2008 13:32:36 CST 0
<![CDATA[使用 Apache HttpClient 突破 J2EE 站点认证]]> .html

使用 Apache HttpClient 突破 J2EE 站点认证

 

山 崺颋 (shanyit@cn.ibm.com), 软件工程师, IBM 中国软件开发中心
孙 元涛 (sunyuant@cn.ibm.com), 软件工程师, IBM 中国软件开发中心

2008 年 6 月 13 日

商业性 Web 站点大都提供站点认证功能以保护某些受限资源,HTTP 协议和 J2EE 规范对 Web 站点的认证过程都已有了详尽的定义,常见浏览器都能根据相应协议提供对应的界面形式帮助用户完成站点的认证过程。但在某些情况下,我们需要编写程序直接获取站点的受保护资源,在这类情况下,就不能利用浏览器给定的界面去完成认证,而需要我们根据不同的协议人工地发送相应请求以完成整个认证过程。本文根据这种需求给出一个基于 Apache HttpClient 应用包的解决方案。

J2EE 站点认证简介

出于安全性的需要和用户授权管理的考虑,常见的 J2EE 站点对特定资源都会加入认证/授权机制。例如一个公网上的论坛,一个只对特定用户开放的 RSS 或 Atom Feed,这些资源都必须在确信访问者为被授权用户时才能向访问者开放。为了实现这样的功能,J2EE 站点通常会采用某种站点认证机制,其中常见的有 HTTP Basic 认证和 J2EE Form-Based 认证。

HTTP Basic 认证

HTTP Basic 认证是 HTTP 认证协议(rfc2617)所定义的标准认证方式。要求 HTTP Basic 认证的服务器会在客户端访问受保护资源时向客户端发出请求,要求客户端上传用户名和密码对。服务器在收到用户名/密码并验证通过后,才将保护资源的内容返回给客户端。它的工作机制如下图:


图 1. HTTP Basic 认证原理
图 1. HTTP Basic 认证原理

由于是 HTTP 规范,因而常见的浏览器,如 Internet Explorer,Mozilla Firefox,在 步骤 2 中收到服务器对用户名和密码的请求时会弹出认证对话框,供用户输入用户名/密码。


图 2. Firefox 在收到步骤 2 中请求时弹出的用户名/密码输入框
图 2. Firefox 在收到步骤 2 中请求时弹出的用户名/密码输入框

HTTP Basic 认证方式使用 base64 编码方式传送用户名和密码,而 base64 仅仅是一种公开的编码格式而非加密措施,因而如果信道本身不使用 SSL 等安全协议,用户密码较容易被截获。

J2EE Form-Based 认证

Form-Based 认证不同于 HTTP Basic 认证,它是 J2EE 对于认证方式的一种扩展。它使用自定义的 HTML 表单(通常为 login.jsp)作为输入用户名和密码的用户界面,最终将用户在表单上填入的用户名/密码提交至服务器。它的工作机制如下:


图 3. Form-Based 认证原理
图 3. Form-Based 认证原理

Form-Based 认证方式在 J2EE 站点中更为常见。这一方面是由于它提供了自定义的用户名密码输入界面;另一方面它的传输也更为安全,通常情况下 login.jsp 会被配置为需要使用 SSL 信道访问,这样在步骤 2、3 中对用户名和密码的传送就被安全信道所保护,而较难被非法截取。





Apache HttpClient 认证功能简介

Apache HttpClient 是 Apache 开源组织提供的纯 Java 实现的 HTTP 开源包。它能模拟各类 HTTP 客户端所需功能,例如 HTTP/HTTPS 连接,GET/PUT 请求,甚至提供了超时重试的功能。

HttpClient 也提供了对标准 HTTP 认证的接口,在最新的 HttpClient 3.1 中,支持的认证方式有:

  • Basic 认证:即前面提到的 rfc2716 规范中定义的 HTTP Basic 认证方式。
  • Digest 认证:一种基于摘要的更为安全的认证协议,虽然它的应用没有 Basic 认证方式广泛。
  • NTLM 认证:微软制定的认证协议规范,然而此项标准的细节却并不公开。

我们可以注意到 Form-Based 认证并不在其中,这是因为 Form-Based 认证方式并非 HTTP 协议标准,而是 J2EE 提供的一种特殊的认证方式,因而开发者需要在 HttpClient 基础上另行开发适合 Form-Based 认证的方案。





使用 Apache HttpClient 通过 HTTP Basic 认证

由于 HttpClient 内置支持 HTTP Basic 认证方式,因而使用 HttpClient 通过 HTTP Basic 认证的步骤显得较为简单。

  1. 为 HttpClient 的状态对象添加用户名/密码对。可以注意到在 setCredentials 方法中的另一个参数为 AuthScope 对象。事实上我们添加的每个用户名/密码对都与一个 AuthScope 对象相关联。AuthScope 对象确定了此用户名/密码对的适用站点,在示例中所给出的用户名/密码对将只适用于 www.sample.com 位于 80 端口上的资源。HttpClient 在与其他站点交互时将不会使用此用户名/密码对,这样有效地防止了机密数据被传送至不必要的站点。
  2. 开启 HttpClient 提供的占先式(Preemptive)认证功能。开启了这个功能后,HttpClient 对于那些处在之前请求过的URI空间范围内的资源,会主动地随请求一起向服务器发送 Basic 认证数据,而不是等待服务器返回是否需要认证的响应后再提交认证。在多数情况下,能够减少请求-响应传递的次数,从而间接提高了服务器的响应能力。值得注意的是在这种情况下必须在 AuthScope 对象中明确指定适用站点,以避免向不相关的站点泄漏敏感数据。
  3. 创建 GetMethod 对象,此对象将使用 GET 方式对保护资源发出 HTTP 请求。
  4. setDoAuthentication(true) 语句将告知 HttpClient 在服务器端发回需要认证的请求后,自动将我们在步骤 1 中设置的用户名/密码对发送至服务器,以完成认证过程。
  5. 执行 GET 请求,获取和处理受保护资源的内容。

清单 1. Basic 认证示例
HttpClient client = new HttpClient();
	
// 1
client.getState().setCredentials(
    new AuthScope("www.sample.com", 80, AuthScope.ANY_REALM),
    new UsernamePasswordCredentials("username", "password")
);
         
// 2
client.getParams().setAuthenticationPreemptive(true);

// 3
GetMethod get = new GetMethod("http://www.sample.com/protected.html");

// 4
get.setDoAuthentication( true );

try {
    // 5
    int status = client.executeMethod( get );

    // process the content from the response
    …

} finally {
    get.releaseConnection();
}

由于 Basic 认证方式直接向服务器发送未经加密的用户名/密码对,导致这些敏感数据很容易在网络传输过程中被截取,因此安全性很低。所幸 HttpClient 对基于安全套接字层(SSL)的 HTTP 协议(HTTPS)提供了足够的支持,而且使用起来也很简单。不过之前需确保本地机器已经安装好 JSSE(Sun 提供的 JDK 1.4 及之后的版本已集成 JSSE)。

使用 HttpClient 进行标准的 SSL 连接对用户来说是透明的。参照清单 1,用户只需用符合 HTTPS 协议的 URL 作为参数生成 GetMethod 对象即可。除此之外,HttpClient 还允许用户定制 SSL 使得客户端程序能够自动接受不同类型的证书。

利用 HttpClient 实现一个自定义的 SSL 协议包括以下 3 个关键步骤:

  1. 定制一个实现了 org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory 接口的工厂类。这个工厂类的作用是开启一个与服务器通讯的 Socket 并进行必需的初始化动作。关于实现该接口的具体细节,HttpClient 项目的主页上有详细的代码实例和注释说明。
  2. 利用之前创建的工厂类对象、HTTPS 协议名称和默认端口号实例化一个新的 org.apache.commons.httpclient.protocol.Protocol 对象。
  3. 注册这个自定义的 Protocol 对象使其与某个协议名绑定,当 HttpClient 处理此类协议时,将默认调用这个自定义 Protocol 对象。

清单 2. 在 HttpClient 中自定义 SSL 示例
// 1
SecureProtocolSocketFactory sampleSSLSocketFactory = new SampleSSLSocketFactory();
	
// 2
Protocol httpsProtocol = new Protocol("https", sampleSSLSocketFactory, 443);

// 3
Protocol.registerProtocol("https", httpsProtocol);

HttpClient client = new HttpClient();

client.getState().setCredentials(
    new AuthScope("www.sample.com", 80, AuthScope.ANY_REALM),
    new UsernamePasswordCredentials("username", "password")
);
         
// Request the protected resource via SSL
GetMethod get = new GetMethod("https://www.sample.com/protected.html");

get.setDoAuthentication( true );

try {
    int status = client.executeMethod( get );

    // process the content from the response
    …
} finally {
    get.releaseConnection();
}





使用 Apache HttpClient 通过 Form-Based 认证

Form-Based 认证相对 HTTP Basic 认证而言过程较为复杂,需要开发者记录下相关的 cookie 信息和部分 header 字段并多次向站点发出请求。它的大致原理如下:

注意:不同的应用可能有不同的配置方式,开发者可以先在浏览器中手动访问受保护资源,获取 login.jsp。进行分析后即可获知对应的认证服务资源 j_security_check 的位置以及对应的用户名与密码在表单中的字段。

假定我们需要访问的受保护资源为 http://www.sample.com/sampleApp/sample.rss。首先我们需要向此保护资源发出请求。而由 Form-Based 认证原理一节中可知,J2EE 服务器会将此请求重定向至 login.jsp。如果仔细分析 login.jsp 我们能发现它仅仅是一个 HTML 表单,其中有两个字段 j_username 和 j_password 分别记录用户名和密码,而提交的目标则是 j_security_check。通常情况下,J2EE 构架会在每个站点应用的根节点定义一个 j_security_check 的资源。而我们的站点的应用程序根(Application Root)为 sampleApp。因而,通过将用户名,密码以及相关 cookie 和 header 字段以 POST 方式发送至 http://www.sample.com/sampleApp/j_security_check 即可通过站点认证。在通过站点认证后,服务器端将给出一个新的重定向,通常它将指向了用户最初试图访问的受保护资源(本例中也就是 http://www.sample.com/sampleApp/sample.rss)。我们只需要再次创建访问对象向此资源发出请求即可获得其内容。

以下给出一个示例:


清单 3. Form-Based 认证示例
HttpClient client = new HttpClient();
client.getState().setCookiePolicy(CookiePolicy.COMPATIBILITY);

// 1
GetMethod authget = new GetMethod("httpwww.sample.comsampleAppsample.rss");
try {
    client.executeMethod(authget);
}
catch (HttpException httpe) {
    httpe.printStackTrace();
}
catch (IOException ioe) {
    ioe.printStackTrace();
}

// 2
NameValuePair[] data = new NameValuePair[2];
data[0] = new NameValuePair("j_username", username);
data[1] = new NameValuePair("j_password", password);

PostMethod authpost = new PostMethod("http://www.sample.com/sampleApp/j_security_check");
authpost.setRequestBody(data);

// 3
Header hCookie = authget.getRequestHeader("Cookie");
Header hHost = authget.getRequestHeader("Host");
Header hUserAgent = authget.getRequestHeader("User-Agent");
if (hCookie == null || hHost == null || hUserAgent == null) {
    return null;
}

authpost.setRequestHeader(hCookie);
authpost.setRequestHeader(hHost);
authpost.setRequestHeader(hUserAgent);

authget.releaseConnection();

try {
    client.executeMethod(authpost);

    // 4
    Header header = authpost.getResponseHeader("location");
    if (header != null) {
        String newuri = header.getValue(); 
        GetMethod redirect = new GetMethod(newuri);

        client.executeMethod(redirect); 
        // process the content from the response
        redirect.releaseConnection();            
    }
} catch (HttpException httpe) {
    httpe.printStackTrace();
    return null;
} catch (IOException ioe) {
    ioe.printStackTrace();
    return null;
}
authpost.releaseConnection();

其中各个步骤解释如下:

  1. 使用 GET 方式请求 sample.rss。服务器收到连接后将在响应中给出连接信息,HttpClient 在接收到响应后会将其保存至 cookie 中。
  2. 准备第二次对 j_security_check 的连接,将用户名和密码填入新的 POST 请求的正文。
  3. 将 cookie 和部分 header 字段拷贝至新请求的报头中,并发送请求。
  4. 从认证成功的响应中获取重定向,并对重定向指向的资源发出请求,获取并处理内容。




小结

随着 Web 2.0 时代的到来,Web 站点的数据和内容显得愈加重要。而为了收集这些数据,人们需要利用计算机本身的搜集能力,通过后台请求,而不是浏览器交互的方式去获取站点的数据。而商业站点中普遍存在的认证/授权机制显然成为了开发此类数据收集程序的一道屏障。Apache HttpClient 根据这些需求,提供了多种 HTTP 认证机制的实现方案。开发人员也可以利用 HttpClient 强大的底层功能,设计特定方案以通过 J2EE 站点的认证体系。

]]>
Thu,26 Jun 2008 13:21:36 CST 0
<![CDATA[Erlang 编程手册(第二部分,包括匹配和模块)]]> .html Erlang 编程手册(第二部分,包括匹配和模块)

 

3 匹配 Pattern Matching

3.1 匹配 Pattern Matching

变量通过匹配机制进行对数据的绑定。匹配发生在函数的执行过程、case- receive- try-表达式和匹配操作符(=)表达式中。

在匹配中,左手边的“pattern模式”将于右手边的项进行匹配。如果匹配成功,则该未绑定变量变为已绑定状态。如果绑定失败,就发生一个运行时错误。

例子:

1> X.
** 1: variable 'X' is unbound **
2> X = 2.
2
3> X   1.
3
4> {X, Y} = {1, 2}.
** exited: {{badmatch,{1,2}},...} **
5> {X, Y} = {2, 3}.
{2,3}
6> Y.
3
 

4 模块Modules

4.1 模块语法Module Syntax

Erlang代码被模块分割为不同的部分。一个模块通常包含一组属性和函数声明,均以(.)结尾。例如:

-module(m).          % module attribute
-export([fact/1]).   % module attribute

fact(N) when N>0 ->  % beginning of function declaration
    N * fact(N-1);   %  |
fact(0) ->           %  |
    1.               % end of function declaration
4.2 模块属性Module Attributes

一个模块属性定义了该模块的特定信息。一个属性通常包含一个标记和对应的值。

-Tag(Value).

Tag必须是常量,这时Value必须是字符类型的项。

任何的模块属性都可以被设定。属性是存储在编译后的代码中的,并且能够被使用。例如,函数beam)lib:chunks/2。

这里有几个模块属性是预定义了含义的,一些包含了两个参数,但是用户定义的模块属性只能有一个参数。

4.2.1 预定义模块属性Pre-Defined Module Attributes

预定义的模块属性可以被之前的任何函数声明所替换。

-module(Module).
模块声明,定义该模块的名称。模块的名称必须是一个常量,应该与该模块所在的代码文件名相同。
该属性必须首先被定义,而且也是惟一一个被强制要求的属性。
-export(Functions).
暴露函数。指定在模块中的哪些函数被向外公开可见。Functions是一个列表[Name1/Arity1,...,NameN/ArityN],这里的NameI是一个常量,二ArityI应该是一个整数。
-import(Module,Functions).
导入函数。导入的函数可以被以本地函数一样的方式进行调用,这不需要使用模块名作为前缀。
Module,是一个常量,指定导入的模块,Funcations是一个类似于export的函数列表。
-compile(Options).
编译选项。Options,一个单一选项或者是一组选项,这些选项将被在编译的时候自动添加道编译选项中。
-vsn(Vsn).
模块版本。Vsn是一个字符项,可以通过beam_lib:version/1来读取。
如果该属性没有被指定,版本默认为该模块的校验和。
4.2.2 行为模块属性Behaviour Module Attribute

我们能够通过行为(Behaviour)指定某个模块为回调模块:

-behaviour(Behaviour).

Behaviour 是行为的名称,可能是某个用户自定义的行为或者某个OTP标准行为 gen_server, gen_fsm, gen_event 或者 supervisor。

拼写为 behavior 系统也是接受的。

4.2.3 宏和记录的定义Macro and Record Definitions

用来定义宏和记录的方式和模块的属性的语法是一样的:

-define(Macro,Replacement).
-record(Record,Fields).

宏和记录的定义可以存在于模块的任何地方,甚至在函数的声明中也可以。

4.2.4 文件包含File Inclusion

和模块的其他属性的使用一样:

-include(File).
-include_lib(File).

File, 一个字符串,应该指向一个真是存在的文件。被指向的文件的内容将被包含在声明include的地方。

包含文件一般用在记录和宏定义处,用来供多个模块共享。推荐对这些将被包含的文件使用.hrl作为后缀名。

File 可以使用一个路径组件$VAR开始,并且返回os:getenv(VAR),如果os:getenv(VAR)返回false,则$VAR为空。

例子:

-include("my_records.hrl").
-include("incdir/my_records.hrl").
-include("/home/user/proj/my_records.hrl").
-include("$PROJ_ROOT/my_records.hrl").

include_libinclude类似, 但是并不指向绝对文件,而忽视第一个路径组件被假设为一个应用的名称。例子:

-include_lib("kernel/include/file.hrl").

代码中使用了 code:lib_dir(kernel) 来寻在当前版本的kernel的目录,并且在这些子目录中寻找文件file.hrl。

4.2.5 文件和行的设置Setting File and Line

类似的,这里预定义了宏FILE和LINE:

-file(File, Line).

该属性被一些工具(例如Yecc)获取编译的相关信息和获取源文件的相关信息。

4.3 注释Comments

注释可以放置于模块的任何地方,除了字符串和引起的常量中。注释以“%”开始,但是不包含一个结尾符也是合法的。(在每个行的末尾会有一个空白字符)

]]>
Fri,13 Jun 2008 13:56:21 CST 0
<![CDATA[Erlang 编程参考手册(第一部分)]]> .html Erlang 编程参考手册(第一部分)

 

2 数据类型

2.1 项Term

Erlang提供了一组数据类型,我们将在本章节中逐个认识。某种数据类型的一个实例称为一个项。

2.2 数值Number

这里有两种数值类型,整型和浮点型。除了一些常见的转换外,Erlang还有两种特殊的转换形式:

  • $char
    获取字符的ASCII码。
  • base#value
    base进制(进制的范围为2到36的整数)。在Erlang 5.2/OTP R9B和早前的版本中,这个范围较小,只能是2到16.

例子:

1> 42.
42
2> $A.
65
3> $\n.
10
4> 2#101.
5
5> 16#1f.
31
6> 2.3.
2.30000
7> 2.3e3.
2300.00
8> 2.3e-3.
2.30000e-3
2.3 常量Atom

一个常量可以看作一个词条,而且该词条就是它的名称。一个常量如果不是以小写字符开始或者包含其他字符(例如数字、_、@等)则应该是使用单引号包起来。

例子:

hello
phone_number
'Monday'
'phone number'
2.4 比特式Binary

一个比特式用来存储没有类型的内存数据。

比特式被表示成为比特的直接形式。

例子:

1> <<10,20>>.
<<10,20>>
2> <<"ABC">>.
<<65,66,67>>

更多的例子可以稍等一段时间,会有编程例子放出。

2.5 引用Reference

一个引用是在Erlang运行时系统中的一个唯一的项,通过make_ref/0创建。

2.6 函数项Fun

一个函数项是一个函数化对象。它可以创建一个匿名函数,并且传递函数自身,作为另一个函数的参数。

例子:

1> Fun1 = fun (X) -> X 1 end.
#Fun<erl_eval.6.39074546>
2> Fun1(2).
3

在编程例子中会有更多的Fun表达式例子,敬请期待。

2.7 端口标识符Port Identifier

一个端口标识符就是一个Erlang端口。open_port/2,被用来创建端口,将返回该类型的一个值。

2.8 进程ID Pid

一个进程标识符pid,spawn/1,2,3,4,spawn_link/1,2,3,4,和spawn_opt/4,都可以用来创建进程,返回一个该类型的值。例子:

1> spawn(m, f, []).
<0.51.0>

内置函数 self() 返回调用进程的pid,例子:

-module(m).
-export([loop/0]).

loop() ->
    receive
        who_are_you ->
            io:format("I am ~p~n", [self()]),
            loop()
    end.

1> P = spawn(m, loop, []).
<0.58.0>
2> P ! who_are_you.
I am <0.58.0>
who_are_you
2.9 元组Tuple

带有确定数量的项的复合数据类型:

{Term1,...,TermN}

其中每个项都被称为元素。元素的数量就是该元组的大小。

有很多操作元组的函数。

例子:

1> P = {adam,24,{july,29}}.
{adam,24,{july,29}}
2> element(1,P).
adam
3> element(3,P).
{july,29}
4> P2 = setelement(2,P,25).
{adam,25,{july,29}}
5> size(P).
3
6> size({}).
0
2.10 列表List

带有不定长项的复合数据类型。

[Term1,...,TermN]

其中的每一项都称为一个元素。元素的数量就是这个列表的长度。

形式上,一个列表可以是空列表[]或者是一个有头元素和尾元素的列表。可以使用[H|T]对列表进行划分。列表[Term1,...,TermN]其实可以被表示为[Term1|[...|[TermM|[]]]。

例子:
[] is a list, thus
[c|[]] is a list, thus
[b|[c|[]]] is a list, thus
[a|[b|[c|[]]]] is a list, or in short [a,b,c].

尾部是一个列表的列表有时候称为proper list严格列表。当然,尾部不是列表的列表,例如[a|b]也是合法的。在实际中,我们使用单列表可能更多一点。

例子:

1> L1 = [a,2,{c,4}].
[a,2,{c,4}]
2> [H|T] = L1.
[a,2,{c,4}]
3> H.
a
4> T.
[2,{c,4}]
5> L2 = [d|T].
[d,2,{c,4}]
6> length(L1).
3
7> length([]).
0

一组操作列表的函数可以在STDLIB中的模块lists中找到。

2.11 字符串String

字符串使用双引号引起,但是并不是一个单独的Erlang数据类型。Erlang系统内部是使用[$h,$e,$l,$l,$o]来表示字符串"hello"的,也就是[104,101,108,108,111]。

两个临近的字符串可以被链接为一个字符串。这是在编译时完成的,而非运行时。例子:

"string" "42"

等效于:

"string42"
2.12 记录Record

一个记录是一个数据结构,用来存储定长的元素组。它有一个命名域,这一点与C语言相同。尽管如此,记录并不是一个真正意义上的数据类型。记录被Erlang表示为元组表达式,这也是在编译时完成的。因此,记录表达式并没有被Erlang所真正“认识”,除了某些特殊的操作。

例子:

-module(person).
-export([new/2]).

-record(person, {name, age}).

new(Name, Age) ->
    #person{name=Name, age=Age}.

1> person:new(ernie, 44).
{person,ernie,44}
2.13 布尔Boolean

在Erlang中没有布尔值。而是使用常量true和false来表示。

例子:

1> 2=<3.
true
2> true or false.
true
2.14 转义字符Escape Sequences

下面是一些可以被使用的转义字符:

Recognized Escape Sequences.

描述

\b
backspace

\d
delete

\e
escape

\f
form feed

\n
newline

\r
carriage return

\s
space

\t
tab

\v
vertical tab

\XYZ, \YZ, \Z
character with octal representation XYZ, YZ or Z

\^a...\^z
\^A...\^Z
control A to control Z

\'
single quote

\"
double quote

\\
backslash

 

2.15 类型转换Type Conversions

下面是一些用来进行类型转换的内置函数:

1> atom_to_list(hello).
"hello"
2> list_to_atom("hello").
hello
3> binary_to_list(<<"hello">>).
"hello"
4> binary_to_list(<<104,101,108,108,111>>).
"hello"
5> list_to_binary("hello").
<<104,101,108,108,111>>
6> float_to_list(7.0).
"7.00000000000000000000e 00"
7> list_to_float("7.000e 00").
7.00000
8> integer_to_list(77).
"77"
9> list_to_integer("77").
77
10> tuple_to_list({a,b,c}).
[a,b,c]
11> list_to_tuple([a,b,c]).
{a,b,c}
12> term_to_binary({a,b,c}).
<<131,104,3,100,0,1,97,100,0,1,98,100,0,1,99>>
13> binary_to_term(<<131,104,3,100,0,1,97,100,0,1,98,100,0,1,99>>).
{a,b,c}
]]>
Fri,13 Jun 2008 13:54:44 CST 0
<![CDATA[Erlang 编程(第十部分)]]> .html Erlang 编程(第十部分)

 

4 记录和宏

大一些的程序往往是由很多个文件组成的,我们在各个部分只见定义出统一的接口。

4.1 将一个大程序分割为多个独立的文件

下面,我们将把上一次写的一个信使程序分割为5个独立的文件。

mess_config.hrl
头文件,包含配置数据
mess_interface.hrl
定义客户端和信使只见的接口
user_interface.erl
用户接口函数
mess_client.erl
信使客户端函数
mess_server.erl
信使服务器端函数

我们同样清理一下消息与shell之间的传递接口,客户端和服务器端都是用记录(records)进行定义,我们将引入宏(macros)的概念。

%%%----FILE mess_config.hrl----

%%% Configure the location of the server node,
-define(server_node, messenger@super).

%%%----END FILE----
%%%----FILE mess_interface.hrl----

%%% Message interface between client and server and client shell for
%%% messenger program 

%%%Messages from Client to server received in server/1 function.
-record(logon,{client_pid, username}).
-record(message,{client_pid, to_name, message}).
%%% {'EXIT', ClientPid, Reason}  (client terminated or unreachable.

%%% Messages from Server to Client, received in await_result/0 function 
-record(abort_client,{message}).
%%% Messages are: user_exists_at_other_node, 
%%%               you_are_not_logged_on
-record(server_reply,{message}).
%%% Messages are: logged_on
%%%               receiver_not_found
%%%               sent  (Message has been sent (no guarantee)
%%% Messages from Server to Client received in client/1 function
-record(message_from,{from_name, message}).

%%% Messages from shell to Client received in client/1 function
%%% spawn(mess_client, client, [server_node(), Name])
-record(message_to,{to_name, message}).
%%% logoff

%%%----END FILE----
%%%----FILE user_interface.erl----

%%% User interface to the messenger program
%%% login(Name)
%%%     One user at a time can log in from each Erlang node in the
%%%     system messenger: and choose a suitable Name. If the Name
%%%     is already logged in at another node or if someone else is
%%%     already logged in at the same node, login will be rejected
%%%     with a suitable error message.

%%% logoff()
%%%     Logs off anybody at at node

%%% message(ToName, Message)
%%%     sends Message to ToName. Error messages if the user of this 
%%%     function is not logged on or if ToName is not logged on at
%%%     any node.

-module(user_interface).
-export([logon/1, logoff/0, message/2]).
-include("mess_interface.hrl").
-include("mess_config.hrl").

logon(Name) ->
    case whereis(mess_client) of 
        undefined ->
            register(mess_client, 
                     spawn(mess_client, client, [?server_node, Name]));
        _ -> already_logged_on
    end.

logoff() ->
    mess_client ! logoff.

message(ToName, Message) ->
    case whereis(mess_client) of % Test if the client is running
        undefined ->
            not_logged_on;
        _ -> mess_client ! #message_to{to_name=ToName, message=Message},
             ok
end.

%%%----END FILE----
%%%----FILE mess_client.erl----

%%% The client process which runs on each user node

-module(mess_client).
-export([client/2]).
-include("mess_interface.hrl").

client(Server_Node, Name) ->
    {messenger, Server_Node} ! #logon{client_pid=self(), username=Name},
    await_result(),
    client(Server_Node).

client(Server_Node) ->
    receive
        logoff ->
            exit(normal);
        #message_to{to_name=ToName, message=Message} ->
            {messenger, Server_Node} ! 
                #message{client_pid=self(), to_name=ToName, message=Message},
            await_result();
        {message_from, FromName, Message} ->
            io:format("Message from ~p: ~p~n", [FromName, Message])
    end,
    client(Server_Node).

%%% wait for a response from the server
await_result() ->
    receive
        #abort_client{message=Why} ->
            io:format("~p~n", [Why]),
            exit(normal);
        #server_reply{message=What} ->
            io:format("~p~n", [What])
    after 5000 ->
            io:format("No response from server~n", []),
            exit(timeout)
    end.

%%%----END FILE---
%%%----FILE mess_server.erl----

%%% This is the server process of the messneger service

-module(mess_server).
-export([start_server/0, server/0]).
-include("mess_interface.hrl").

server() ->
    process_flag(trap_exit, true),
    server([]).

%%% the user list has the format [{ClientPid1, Name1},{ClientPid22, Name2},...]
server(User_List) ->
    io:format("User list = ~p~n", [User_List]),
    receive
        #logon{client_pid=From, username=Name} ->
            New_User_List = server_logon(From, Name, User_List),
            server(New_User_List);
        {'EXIT', From, _} ->
            New_User_List = server_logoff(From, User_List),
            server(New_User_List);
        #message{client_pid=From, to_name=To, message=Message} ->
            server_transfer(From, To, Message, User_List),
            server(User_List)
    end.

%%% Start the server
start_server() ->
    register(messenger, spawn(?MODULE, server, [])).

%%% Server adds a new user to the user list
server_logon(From, Name, User_List) ->
    %% check if logged on anywhere else
    case lists:keymember(Name, 2, User_List) of
        true ->
            From ! #abort_client{message=user_exists_at_other_node},
            User_List;
        false ->
            From ! #server_reply{message=logged_on},
            link(From),
            [{From, Name} | User_List]        %add user to the list
    end.

%%% Server deletes a user from the user list
server_logoff(From, User_List) ->
    lists:keydelete(From, 1, User_List).

%%% Server transfers a message between user
server_transfer(From, To, Message, User_List) ->
    %% check that the user is logged on and who he is
    case lists:keysearch(From, 1, User_List) of
        false ->
            From ! #abort_client{message=you_are_not_logged_on};
        {value, {_, Name}} ->
            server_transfer(From, Name, To, Message, User_List)
    end.
%%% If the user exists, send the message
server_transfer(From, Name, To, Message, User_List) ->
    %% Find the receiver and send the message
    case lists:keysearch(To, 2, User_List) of
        false ->
            From ! #server_reply{message=receiver_not_found};
        {value, {ToPid, To}} ->
            ToPid ! #message_from{from_name=Name, message=Message}, 
            From !  #server_reply{message=sent} 
    end.

%%%----END FILE---

4.2 头文件

你应该已经注意到了一些文件的扩展名为.hrl。这种文件将被.erl文件所包含:

-include("File_Name").

例如:

-include("mess_interface.hrl").

上面的写法的含义是:我们将从当前目录中寻找mess.interface.hrl文件,并将其内容合并在.erl文件中。

.hrl 文件可以包含任何合法的Erlang代码,但是我们通常在里面放置记录和宏定义。

4.3 记录

一个记录的定义如下:

-record(name_of_record,{field_name1, field_name2, field_name3, ......}).

例如:

-record(message_to,{to_name, message}).

等效于:

{message_to, To_Name, Message}

创建一个记录最好的办法如下:

#message_to{message="hello", to_name=fred)

这将创建消息:

{message_to, fred, "hello"}

注意,你不需要担心你创建时给不同的部分赋值的顺序。使用记录的一个优势是:我们通过替换在头文件中的定义,我们可以轻松的改变接口。例如,如果你打算增加一个新的域到记录中,你将只需要在使用新的域的地方修改即可,而不需要在所有记录被引用的地方修改代码。如果你在创建一个记录的时候没有对所有域赋值,那么这些域将被自动赋值为常量undefined。

匹配在创建记录的过程中很有用。例如在case或者receive中:

#message_to{to_name=ToName, message=Message} ->

等同于:

{message_to, ToName, Message}

4.4 宏

被添加到新的信使程序中的还有宏。文件mess_config.hrl包含如下定义:

%%% Configure the location of the server node,
-define(server_node, messenger@super).

我们在 mess_server.erl 中包含该文件:

-include("mess_config.hrl").

在mess_server.erl中的?server_node都将被替换为messenger@super。

但我们创建服务器进程时也使用了宏:

spawn(?MODULE, server, [])

这是一个标准的宏(通过系统定义,而不是用户定义)。?MODULE将被一直替换为当前的模块名(也就是在启动该文件时使用的参数-module定义)。但我们在某些时候使用宏会更加方便一点,例如参数。

这在Erlang系统中的三个Elrang(*.erl)文件被分别编译为目标代码文件(.beam)。在我们执行到相关代码时,Erlang系统将载入和链接这些文件进入系统。我们上面的例子中,都是简单的将其放在同一个目录中。通常我们习惯将.beam放入其他目录中。

在这个信使程序中,没有假定任何发送消息的类别,可以是任何合法的Erlang的内容。

]]>
Fri,13 Jun 2008 13:41:16 CST 0
<![CDATA[Erlang 编程(第九部分)]]> .html Erlang 编程(第九部