2008年11月18日星期二

兰迪语录:真正实现你的童年梦想

兰迪语录:真正实现你的童年梦想




Brick walls are there for a reason: they let us prove how badly we want things.

人生路上有阻挡你梦想的砖墙,那是有原因的。这些砖墙让我们来证明我们究竟有多么想要得到我们所需要的。



Experience is what you get when you didn't get what you wanted.

当你得不到你想的到的东西时,你会得到经验。



Never lose the child-like wonder.

永远不要失去孩童一样的好奇心。



If we do something which is pioneering, we will get arrows in the
back. But at the end of the day, whether we succeed or not, the journey
will be worthwhile.

当我们做一些前人没做过的事情时,有人会放冷箭。然而,最后的结果是,无论我们是否成功,过程都是值得的。



Be good at something; it makes you valuable.

在某些方面要能突出你的专才,这会使你有价值。



If you live your life the right way, the karma will take care of itself, and the dreams will come to you.

如果你用正确方式地去度过你的一生,命运会让合适的梦想会来到你身边。



Stay positive no matter what, but not in denial.

无论发生什么事情,一定要往好的方面想,但是不要拒绝接受事实。



Make things fun.

凡事尽量弄得有趣些。



Dream a big dream.

有大的梦想。



Learn from all the people in your life.

向你生活中所有的人学习



Be dare to do things differently.

大胆去用不同的方式来做事情。



Find the right place to nurture your dreams.

找一个适合你培养梦想的地方。



If you screwed up, and no one cares to say anything, that means you have been dropped. Criticism is your best friend.

如果你做错了,没人说你什么,那是因为别人已放弃了你。批评才是你最好的朋友。



Life is a gift. Find the good things from others. If you can wait long enough, the good side of other people will show.

生命是一个礼物。看别人好的一面。如果你给别人时间的话,他们好的一面会展现出来。



Don't go the short-cut, always tell the truth.

不要投机取巧,要永远说真话。



Don't complain, just work harder.

遇到困难,不要抱怨,要更加努力地去作。

深入浅析:面向对象编程四大原则

深入浅析:面向对象编程四大原则

在面向对象设计中,如何通过很小的设计改变就可以应对设计需求的变化,这是令设计者极为关注的问题。为此不少OO先驱提出了很多有关面向对象的设计原则用于指导OO的设计和开发。下面是几条与类设计相关的设计原则。

  1. 开闭原则(the Open Closed Principle OCP)


  一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。因此在进行面向对象设计时要尽量考虑接口封装机制、抽象机制和多态技术。该原则同样适合于非面向对象设计的方法,是软件工程
计方法的重要原则之一。我们以收音机的例子为例,讲述面向对象的开闭原则。我们收听节目时需要打开收音机电源,对准电台频率和进行音量调节。但是对于不同
的收音机,实现这三个步骤的细节往往有所不同。比如自动收缩电台的收音机和按钮式收缩在操作细节上并不相同。因此,我们不太可能针对每种不同类型的收音机
通过一个收音机类来实现(通过重载)这些不同的操作方式。但是我们可以定义一个收音机接口,提供开机、关机、增加频率、降低频率、增加音量、降低音量六个
抽象方法。不同的收音机继承并实现这六个抽象方法。这样新增收音机类型不会影响其它原有的收音机类型,收音机类型扩展极为方便。此外,已存在的收音机类型
在修改其操作方法时也不会影响到其它类型的收音机。


  2. 替换原则 (the Liskov Substitution Principle LSP)


  子类应当可以替换父类并出现在父类能够出现的任何地方。这个原则是Liskov于1987年提出的设计原则。它同样可以从Bertrand Meyer 的DBC (Design by Contract) 的概念推出。


  我们以学生为例,夜校生为学生的子类,因此在任何学生可以出现的地方,夜校生均可出现。这个例子有些牵强,一个能够反映这个原则的例子时圆和椭圆,圆是椭圆的一个特殊子类。因此任何出现椭圆的地方,圆均可以出现。但反过来就可能行不通。


  运用替换原则时,我们尽量把类B设计为抽象类或者接口,让C类继承类B(接口B)并实现操作A和操作B,运行时,类C实例替换B,这样我们即可进行新类的扩展(继承类B或接口B),同时无须对类A进行修改。


  3. 依赖原则 (the Dependency Inversion Principle DIP)


  在进行业务设计时,与特定业务有关的依赖关系应该尽量依赖接口和抽象类,而不是依赖于具体类。具体类只负责相关业务的实现,修改具体类不影响与特定业务有关的依赖关系。


  在结构化设计中,我们可以看到底层的模块是对高层抽象模块的实现(高层抽象模块通过调用底层模块),这说明,抽象的模块要依赖具体实现相关的模块,底层模块的具体实现发生变动时将会严重影响高层抽象的模块,显然这是结构化方法的一个"硬伤"。


  面向对象方法的依赖关系刚好相反,具体实现类依赖于抽象类和接口。


  为此,我们在进行业务设计时,应尽量在接口或抽象类中定义业务方法的原型,并通过具体的实现类(子类)来实现该业务方法,业务方法内容的修改将不会影响到运行时业务方法的调用。


  4. 接口分离原则(the Interface Segregation Principle ISP)


  采用多个与特定客户类有关的接口比采用一个通用的涵盖多个业务方法的接口要好。


  ISP原则是另外一个支持诸如COM等组件化的使能技术。缺少ISP,组件、类的可用性和移植性将大打折扣。


  这个原则的本质相当简单。如果你拥有一个针对多个客户的类,为每一个客户创建特定业务接口,然后使该客户类继承多个特定业务接口将比直接加载客户所需所有方法有效。


  以上四个原则是面向对象中常常用到的原则。此外,除上述四原则外,还有一些常用的经验诸如类结构层次以三到四层为宜、类的职责明确化(一个类对
应一个具体职责)等可供我们在进行面向对象设计参考。但就上面的几个原则看来,我们看到这些类在几何分布上呈现树型拓扑的关系,这是一种良好、开放式的线
性关系、具有较低的设计复杂度。一般说来,在软件设计中我们应当尽量避免出现带有闭包、循环的设计关系,它们反映的是较大的耦合度和设计复杂化。


小公司如何做项目管理


小公司如何做项目管理

http://www.javaeye.com/topic/216779


我所在的公司和大多数国内IT公司一样,十几到几十人的规模,每次在做完项目过程中我们都会感觉累,老板其实也很累,在小公司老板更像是一个项目经理的角色,很多东西都没有流程化的东西可走,所以很多事情都要等老板拍板后才可以继续下去,员工在很多时候就会感到迷茫,随着公司规模的扩大,公司也意识到没有一套规范的项目管理方案是万万不行的,自己在这方面也摸索的一段时间。



首先接触的是敏捷开发的方法,但很快我就感觉这个方法行不通,至少对于我们是这样,因为我们无法保证和客户以及业务人员及时沟通,一个月见几次面就很不错
了,而且我们的开发人员也并不具有敏捷能力。后来接触了下CMMI,CMMI对于小公司就更不靠谱了,它庞大的身躯足以把一个小公司压垮,如果仅为一个证
书的话,我建议完全可以向o6z订购,但不可否认的是CMMI也有很多优秀的地方可以借鉴。那么我对小公司项目管理的看法是一定要精简,做到不是傻瓜都能
够理解并且能够执行,况且很多项目经理(老板)也并不是领域专家。在此我想简单谈谈我对适合小公司的项目管理方案的一些想法
所谓基本适合就是80%适合,我要是说100%适合那我是在扯淡,另外20%怎么办?那就像06z所说的那样,靠经验这个王道。


首先要谈的是
求这个东西,那么什么是需求?需求就是掏钱买你产品的人一些需要,只要是客户的需要,不管是合理不合理那都是需求。其实很多开发人员都意识需求的重要性,
那么真正去做需求的人有多少呢?需求应该是包括需求开发和需求管理这两个过程,这里有个特别的情况是对于自主研发的项目,
我接触的项目也是这种情况居多,于是我们认为自己就是客户,所以需求开发做很简单甚至跳过去,结果后期的需求管理非常混乱,我觉得既然自己是客户,那就要当好客户这个角色,在做客户时应完全忘却自己是个开发人员,同样要把需求做全面。很多教科书上都说应该做需求,但关于怎么做的问题上却和实际情况差别比较大,以下是我关于需求该做什么以及怎么做到一些看法。


需求调研


我觉得需求调研非常的重要,1年前我还打算做一个在线教育服务平台,理念就是淘宝在网上卖商品,我在网上卖教育资源,我提供网上交易场所,签约的老师、学校以及培训机构提供可交易的服务,这种服务可以通过视频、音频、在线PPT、文本的形式展现。忙活了好一阵,发现这个市场早就有很多人做了,而且这个市场并不是很好做,首先在网上学习的人有几个?并且先不说前期推广需要海量资金就是所需要的那么些高性能服务器丫也买不起!这件事就此搁浅,结果信了马云的邪,2年后你
创业你在创业!我觉得这就是典型的需求调研没做好,没有对用户需求做调查,没有考虑同行竞争,没有考虑可行性!另外还要考虑括行业标准和法律规定,比如前
些时候国家就出台了关于办视频网站的政策,我觉得你丫没有足够的背景就不要往火坑里跳楼。总之根据所做行业情况尽可能的把需求调研做全面,这样才可以保证
项目首先是可以赚钱的。那么文档要写吗?我觉得可以不要正式的文档,小公司的人手本来就不够用,要把主要文档
化工作集中在重要的环节上,对于需求调研,本来就很杂乱,完全可以记在工作笔记上,放到需求分析的时候整理。


需求分析


为了得到用户的金钱,我们总是在说,用户是上帝,用户永远是对的,尽管背地里在说客户端坏话:“你丫钱给的倒不多,要求还真少,这需求根本不合理,是正常人的逻辑吗?”,如果你想活下去,最终我们还是要想方设法满足用户的要求。用户是个外界因素,我们无法控制
那么我们只有尽可能改进需求分析的方法来尽量减少不必要的麻烦。那么我觉得日本人做法倒是可以借鉴,在有条件的情况下派专人去现场,随时记录关键性的需
求,即使不能去现场也尽可能的获取尽可能多大信息,不要指望开发后去获取什么有价值的东西。那么是否应该做个原型给客户看看?我是觉得这不大合适,因为如
果项目周期短的话,等你做好原型,黄花菜都凉了。但我觉得等到需求做到差不多的时候可以做用户界面,所谓用户界面就是用户接口,是和用户打交道的地方,所
谓一图解千言,有了界面用户会清楚自己所买的东西在未来会是个什么样的东西,再者开发几个有说明性都界面倒是不会暂用很多时间。等到需求确定下来后
要整理成文档了,这个是很重要的一步,是做设计时候的重要凭证和依据,这个文档就是用户规格说明书,所谓规格就是有规范的格式和内容。


3 需求评审



们已经有了较正规的文档了,那么下一步就是召集所有开发人员开会,最好有客户代表参加,尽管我是很厌烦开会,但该开的会还是要开到,因为之前我遇到这种情
况,开发人员根据设计文档写代码,可是他并不知道自己在开发什么,站在自己的角度想一下,如果自己都不确定自己做的东西,即使有再完备的设计,也会对开发
毫无兴趣,只会让自己觉得自己是个代码机器。所以所有人员参加需求评审是让大家知道自己在做一件有意义的事情,自己正在满足社会的需要,自己在为和谐社会
做贡献,即使你从没那么想过,那你敢保证的你的潜意识没那么想过吗?人是要有社会满足感的吧。另外开会前一定要准备关键有价值的议题,据我观察需求评审会
最容易扯到不着边的话题,所以主持人要控制话题,会议控制在2-3个钟头,
最好做成幸运52的形式,所有人员一定要互动起来,否则变成了个人演讲。需求也做了,会也开了,那么要求客户签字吧。


需求管理


需求管理是在开发开始之后进行的,这也是另所有人头疼的一件事,之前做完一个项目后,客户经常打电话找我们,改过来改过去,后我听到电话,血压都要高50个百分点,后来索性就不接电话,客户就在网上找我,搞的我连QQ都不敢登,但躲是躲不掉滴,客户直接打我手机,丫的真烦人,见过难缠的,没见过这么难缠的。后来转念一想,难道这种情况真的不能避免吗?至少是可以大幅度的缓解吧。这就是我们需求管理中的变更管理没做好,改了哪些地方自己都忘记了,最后是跟着感觉走,拆东墙补西墙。
这里我建议要建立需求跟踪矩阵表,有了这个表我们至少可以对要修改的地方有了依据,迫使我们去调查到底是改什么地方,怎么改,最后改成了什么样。可能你会
说客户需要大幅度修改原有计划,很难跟踪到具体某一项需求,那么我觉得这是由于前期的需求开发没有做好,在后期客户进行实质性的修改的几率是很小的,比如
客户要求我们做个OA系统,最后总不会要我们改成个门户网站吧,在举个例子,在比如你开发一个ERP系统,客户自己的业务流程不会轻易的改变吧,总不至于
把盘点这个业务改成一个报表系统吧。如果真是这样,我们完全有理由告诉客户,你丫乖乖掏银子,我们再给你们开发2期工程,要改,没门!


大家开始拍砖吧,有时间继续……



很多东西说不清楚. 就需求管理本身,在我们这种环境里面就说不清楚了. 如果直接跳到后面, 当然会死得更惨. 可如何走好第一步呢? 需求跟踪矩阵如何用呢? 呵呵.... 看的都晕.


我之前的想法和你一样,由于很多东西说不清,所以干脆不说,所以我们逃避,可是需求管理是躲不了的,否则就像你说的死得更惨,我也想听听你更好的建议:),就像牛根生所说的,办法总比问题多。

需求管理难做和做不做是2回事,我觉得只要你认真去做,总会有收获,需求跟踪矩阵怎么用?那要看你要怎么用,变更总要有原因吧,针对变更总得有解决措施吧,你的后续修改工作总得有个依据吧,修改完之后总得有个结果记录吧,附上需求变更表一份。







在上篇文章里,我简要谈了项目管理中的需求开发和管理,那么在这篇文章里就和各位以闲话家常的方式讨论下项目规划和项目监控。项目规划、项目监控其实也是项目管理中比较核心的工作,也是很多开发人员最敏感的话题,因为这两个东西与公司的领导和员工关系都非常的密切。

   
先从我以前的学校说起,以前我们学校有片荒地,当时的领导觉得学校应该搞绿化,于是组织在荒地上植树,不到一年换了一个校长,这位校长觉得学校应该抓体育
运动,决定再造一个足球场,于是把树移走,造了一个足球场,再后来北京奥运会来了,学习为了迎合绿色奥运的理念又开始植树,这就是没有规划和监控的典型例
子,结果是劳民又伤财。当然对于学校来说,有国家财政的支持,有资本这么折腾,可是对于小公司做项目来说,这么折腾几下估计很快就要牺牲了。


   
事实求是的说大多数小公司在这两个方面做得很少有令人的满意的,小公司的老板其实也会意识到公司在项目规划和监控方面做得不咋地,但很少能做到有效的改
进,其实这个也是有很多方面的原因的,以我自作多情的猜测主要有以下两个原因,对于小公司,尽管盈利不是很多,但基本还是可以撑下去的,老板会觉得管他乱
不乱,公司总之每个月还是有盈利的,少就少点吧,多干几年自己的下半辈子基本有别墅有车了,所以比较保守,要是改革吧,万一鸡飞蛋打怎么办?还是本分点
好,小心使得万年船。其实是对项目规划和监控其实需要大量的成本,老板觉得钱应该花在刀刃上,搞这些东西就是在务虚。再者更恶劣的老板有病就乱烧香,就有
想想借助CMMI这个洋玩意治病的,其实很多老板都蛮喜欢CMMI的,CMMI就是假定人就是一个机器的部件,可以替换可以不停的运转,总之管机器总比管
人省心吧,结果是万分的矛盾,银子撒了一大把,收效却甚微,甚至比以前更乱,大家做的都不开心。与其听咨询师们拿什么高深的方法论来瞎掰,不如我们谈点实
际的,想就以下议题来和各位消遣。


1 工作量估算


   
对于工作量估算很多项目经理(老板)喜欢用数学公式来计算,可能数学公式更加的客观和科学,ok,,看看市面流行的计算方法吧,最常见的是基于代码行的数
学模型,那么这里存在不少问题,工作量估算主要是为了在项目进行中进行有效的项目监控,那么软件开发尚未结束,谁知道最后的代码行有多大?代码经常会被修
改,那么修改的代码算不算?如果算,那么代码修改越多难道能说明工作量越大?代码效率的区别也是很大的,假如一个10行代码可以实现的东西被写成50行,
难道能客观的反映工作量?还有2种比较高级点的方法是基于功能点和COCOMO的方法,那么我想问的是它们的公式中的系数该怎么定?那么不少咨询师忽悠我
说,根据自己的实际情况来定呗,那么我想问的是,算命是迷信,电脑意味着科学,那么用电脑算命算不算迷信?所以我是主张这里还是要靠人的经验来估算,大家
可以在项目周会上对工作量进行充分的估算,在估算时要同时考虑到项目执行的难度,根据经验给出合理的评估。


2 任务分配


   
大多数的做法是将整个项目划分成一个个可以独立执行的原子任务,这些任务要划分优先级和难度,至少心理有个数,并且每项任务要制定负责人。那么问题就出在
这个任务分配上了,软件开发是一项智力创造的活动,如果按CMMI假设的那样,在分配任务时忽略人的因素是万万不可取的,我就有血的教训,不久之前做一个
ruby的项目,然后开始在公司内部随便抓了几个有点ruby基础的人,也没太顾忌别人的想法。做着做着,觉得他们有点心在曹营心在汉,平时还是抱着本
thinking in java看,做ruby也是在敷衍了事,结果是代码质量不行,需要大规模的修改。当然按理说员工应该服从公司的安排,做一样就要
做好一样,但员工也有员工的规划,你去叫他做他压根就不喜欢的事只能说明管理有问题。


   
另外还有一个普遍性的问题是能者多劳,有个朋友刚进公司动手能力很强,也非常的积极,每次做项目都分给他最难最累的任务,做着做着也就厌倦了,这时老板会
忽悠你说,你能力强,要挑起公司的大梁,以后公司壮大了给你个什么职位,我觉得这就是在扯淡,这就是我经常见到的忽悠式的管理,很多管理手段完全靠人情,
很多人都是在这种环境中被忽悠长大,到最后怎么样?被忽悠了几年还不是另谋高就了,所以指望人情化管理的公司很难长大。我觉得该讲原则的地方就要讲原则,
在任务分配上给能力强的分配少而精的任务,而且要考虑到员工自己的想法,有些人想做java架构师,你叫他做oracle dba就不合适,有些对ui设
计感兴趣,你叫他做系统分析员也不合适,有些人就喜欢搞技术,你硬要叫他做管理也是不合适。
 


3 进度管理


   
在做进度之前,一项最重要的任务是识别关键任务,很多进度表进行任务安排时将各项任务平均分的特点为觉得极不合适,有些任务比较难处理,而且许多后续任务
依赖于该项任务,那么这项任务就应该配备更精良的人手和充裕的时间,依我的经验80%的时间都是在处理这些20%的关键任务上。这里还有个比较重要的问题
是时间安排,我听很多项目经理说时间安排要尽可能的紧,也就是比预计要靠前,这样员工才有紧迫感。我觉得这是不可取的,首先即使你按原计划进行,八成也是
要要延期的,那么这就会导致项目严重延期,长此以往,项目延期成了家常便饭,不延期反而不正常,于是大家都成了老油条,那么进度表不就是废纸一张,毫无约
束力而言吗!我觉得根据实际情况指定个合理的进度表是比较重要的,或许你会说项目还是在延期,那我觉得是你项目估算没有做好,项目延期在10%左右比较正
常,否则就可以调查是什么原因导致进度滞后,如果是客观原因,以后完全可以延长项目时间,总之一个合理的进度表比较重要。


4 项目奖金


   
这里牵扯到一个钱的问题,据我了解国内大多小公司很少有项目奖金这么一说,年底给点路费就不错了!国内的大多数项目经理更像是一个技术负责人,根本没有用
钱的权利,我就曾像公司申请项目奖金,结果计划全盘泡汤,给的理由很荒唐,说项目奖金不好分配,给张三多一点吧,李四不爽,反之亦然。我心理暗自想:“你
丫不想给就直说呗!”,这里会导致一个问题,就是“项目经理”凭什么约束成员,大锅饭的道理我也不想再解释了,总之结果就是3个月的项目就得做个5个月,
其实老板的小算盘看似很精明,其实未见得,虽然项目奖金能省就省了,那么工作效率的下降所带来的成本的提高,孰轻孰重?长远一点说,产品质量的下滑导致的
项目维护的成本你计算过吗?依我的经验,3个月的有效工作时间其实也就是1个月,这已经不错了。不过项目奖金的分配确实是个难问题,但有没有项目奖金和分
配合理与否是2码子事吧?由于我也没有能耐申请到项目奖金所以也就没有深入研究这个问题,只得望梅止渴,看看人家华为了,员工根据能力分等级,加上年限、
加班、表现得出个权值来计算。总之现有鸡才能有蛋,这个问题需要更深入的讨论。


    先写那么多,有时间继续……



URL重写

Apache伪静态html(URL Rewrite)设置法


2007-02-12 10:41作者:出处:天极网责任编辑:龙犊














一 打开 Apache 的配置文件 httpd.conf 。
二 将#LoadModule rewrite_module modules/mod_rewrite前面的#去掉
三 在 httpd.conf中添加:
<IfModule mod_rewrite.c>
    RewriteEngine On
    #RewriteCond %{ENV:SCRIPT_URL} (?:index|dispbbs)[-0-9]+\.html
    RewriteRule ^(.*?(?:index|dispbbs))-([-0-9]+)\.html$ $1.php?__is_apache_rewrite=1&__rewrite_arg=$2
</IfModule>


四 要实现asp帖子URL到php帖子的映射,在 第三步的<IfModule mod_rewrite.c>和</IfModule>之间添加:
    RewriteMap tolowercase int:tolower
    RewriteCond %{QUERY_STRING} (?:boardid|page|id|replyid|star|skin)\=\d+ [NC]
    RewriteRule ^(.*(?:index|dispbbs))\.asp$ $1.php?${tolowercase:%{QUERY_STRING}}&__is_apache_rewrite=1


五 保存httpd.conf并重启Apache。



Url Rewrite Filter 是一个基于java的 URL rewrite 一个包。 使用它后就可以使用一些友好的URL来代替 ?&组成的URL了。 例如可以把 http://www.cngump.com/world/china/guangzhou 转换为 http://www.cngump.com/world.jsp?country=china&city=guangzhou

步骤如下:

1. 下载和安装 Tomcat

2.  部署一个空白的 Java Web Application.    urlrewrite.war  

3.  下载  Url Rewrite Filter http://tuckey.org/urlrewrite/

4.  解压 下载后的 urlrewritefilter-2.6.zip 到 TOMCAT_HOME\webapps\urlrewrite\ 下

5.  修改 web.xml 如下


   <?xml version=”1.0″ encoding=”UTF-8″?>

<web-app id=”WebApp_9″ version=”2.4″ xmlns=”http://java.sun.com/xml/ns/j2ee

         xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance

         xsi:schemaLocation=”http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd“>


    <display-name>Url Rewrite</display-name>


    <filter>

        <filter-name>UrlRewriteFilter</filter-name>

        <filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>

    </filter>

    <filter-mapping>

        <filter-name>UrlRewriteFilter</filter-name>

        <url-pattern>/*</url-pattern>

    </filter-mapping>

   


</web-app>

6. 在更目录新建一个 world.jsp 文件。代码如下:

<html>

 <header>

  <title>world</title>

 </header>

 <body>

 <%

        String country = request.getParameter(”country”);

        String city = request.getParameter(”city”);

        out.write(”Country=” + country);

        out.write(”<br>city=” + city);

    %>


 </body>

</html>


7. 修改 WEB-INF 下的 urlrewrite.xml: (使用正则式进行解释)


urlrewrite

<rule>

    <from>/world/([0-9]+)/([0-9]+)</from>

    <to>/world.jsp?country=$1&city=$2</to>

</rule>


</urlrewrite


8.  重新加载 urlrewrite 后测试:


http://localhost:8080/urlrewrite/world/china/guangzhou

http://localhost:8080/urlrewrite/world.jsp?country=china&city=guangzhou效果一样



Acegi框架介绍


Acegi框架介绍


  概述
    【IT168 专稿】对于任何一个完整的应用系统,完
善的认证和授权机制是必不可少的。Acegi
Security(以下简称Acegi)是一个能为基于Spring的企业应用提供强大而灵活安全访问控制解决方案的框架,Acegi已经成为
Spring官方的一个子项目,所以也称为Spring
Security。它通过在Spring容器中配置一组Bean,充分利用Spring的IoC和AOP功能,提供声明式安全访问控制的功能。虽然,现在
Acegi也可以应用到非Spring的应用程序中,但在Spring中使用Acegi是最自然的方式。
Acegi可以实现业务对象方法级的安全访问控制粒度,它提供了以下三方面的应用程序的安全:

? URL资源的访问控制
    如所有用户(包括其名用户)可以访问index.jsp登录页面,而只有授权的用户可以访问/user/addUser.jsp页面。Acegi允许通过正则表达式或Ant风格的路径表达式定义URL模式,让授权用户访问某一URL匹配模式下的对应URL资源。

? 业务类方法的访问控制
    Spring容器中所有Bean的方法都可以被Acegi管理,如所有用户可以调用BbtForum#getRefinedTopicCount()方法,而只有授权用户可以调用BbtForum#addTopic()方法。

? 领域对象的访问控制
   
业务类方法代表一个具体的业务操作,比如更改、删除、审批等,业务类方法访问控制解决了用户是否有调用某种操作的权限,但并未对操作的客体(领域对象)进
行控制。对于我们的论坛应用来说,用户可以调用BbtForum#updateUser(User
user)方法更改用户注册信息,但应该仅限于更改自己的用户信息,也即调用BbtForum#updateUser()所操作的User这个领域对象必
须是受限的。

    Acegi通过多个不同用途的Servlet过滤器对URL资源进行保护,在请求受保护的URL资源前,Acegi的Servlet过滤器判断用户是否有权访问目标资源,授权者被开放访问,而未未被授权者将被阻挡在大门之外。
    Acegi通过Spring AOP对容器中Bean的受控方法进行拦截,当用户的请求引发调用Bean的受控方法时,Acegi的方法拦截器开始工作,阻止未授权者的调用。 
    
   
对领域对象的访问控制建立在对Bean方法保护的基础上,在最终开放目标Bean方法的执行前,Acegi将检查用户的ACL(Aeccess
Control
List:访问控制列表)是否包含正要进行操作的领域对象,只有领域对象被授权时,用户才可以使用Bean方法对领域对象进行处理。此外,Acegi还可
以对Bean方法返回的结果进行过滤,将一些不在当前用户访问权限范围内的领域对象剔除掉——即传统的数据可视域范围的控制。一般来说,使用Acegi控
制数据可视域未非理想的选择,相反通过传统的动态SQL的解决方案往往更加简单易行。

   
从本质特性上来说,Servlet过滤器就是最原始的原生态AOP,所以我们可以说Acegi不但对业务类方法、领域对象访问控制采用了AOP技术方案,
对URL资源的访问控制也使用了AOP的技术方案。使用AOP技术方案的框架是令人振奋的,这意味着,开发者可以在应用程序业务功能开发完毕后,轻松地通
过Acegi给应用程序穿上安全保护的“铁布衫”。
Acegi体系结构
   
乘飞机前需要通过安检,乘客必须提供身份证以验证其身份。在通过安检进入候机室后,国航、海航、南航等不同航空公司的飞机陆续到达,但你只能登上机票上对
应航班的飞机。在登机后,只能坐在机票对应的座位上——你不能抢占他人的座位,你不能在座位上刻字留念、你不能要求空姐打开机窗……

    乘飞机的过程最能体现安全控制的流程,我们可以从中找到身份认证、资源访问控制、领域对象安全控制的对应物:安检对应身份认证,登机对应资源访问控制而按号就座则对应领域对象安全控制。
    Acegi通过两个组件对象完成以上安全问题的处理:AuthenticationManager(认证管理器)、AccessDecisionManager(访问控制管理器),如图 1所示:


图 1 Acegi体系结构


   
SecurityContextHolder是框架级的容器,它保存着和所有用户关联SecurityContext实
例,SecurityContext承载着用户(也称认证主体)的身份信息的权限信息,
AuthenticationManager、AccessDecisionManager将据此进行安全访问控制。
   
   
SecurityContext的认证主体安全信息在一个HTTP请求线程的多个调用之间是共享的(通过ThreadLocal),但它不能在多个请求之
间保持共享。为了解决这个问题,Acegi将认证主体安全信息缓存于HttpSession中,当用户请求一个受限的资源时,Acegi通过
HttpSessionContextIntegrationFilter将认证主体信息从HttpSession中加载到
SecurityContext实例中,认证主体关联的SecurityContext实例保存在Acegi容器级的
SecurityContextHolder里。当请求结束之后,HttpSessionContextIntegrationFilter执行相反的操
作,将SecurityContext中的认证主体安全信息重新转存到HttpSession中,然后从SecurityContextHolder中清
除对应的SecurityContext实例。通过HttpSession转存机制,用户的安全信息就可以在多个HTTP请求间共享,同时保证
SecurityContextHolder中仅保存当前有用的用户安全信息,其整体过程如图 2所示:



图 2 SecurityContext在HttpSession和请求线程间的转交过程



   
当用户请求一个受限的资源时,AuthenticationManager首先开始工作,它象一个安检入口,对用户身份进行核查,用户必须提供身份认证的
凭证(一般是用户名/密码)。在进行身份认证时,AuthenticationManager将身份认证的工作委托给多个
AuthenticationProvider。因为在具体的系统中,用户身份可能存储在不同的用户信息安全系统中(如数据库、CA中心、LDAP服务
器),不同用户信息安全系统需要不同的AuthenticationProvider执行诸如用户信息查询、用户身份判断、用户授权信息获取等工作。只要
有一个AuthenticationProvider可以识别用户的身份,AuthenticationManager就通过用户身份认证,并将用户的授
权信息放入到SecurityContext中。

  
当用户通过身份认证后,试图访问某个受限的程序资源时,AccessDecisionManager开始工作。
AccessDecisionManager采用民主决策机制判断用户是否有权访问目标程序资源,它包含了多个AccessDecisionVoter。
在访问决策时每个AccessDecisionVoter都拥有投票权,AccessDecisionManager统计投票结果,并按照某种决策方式根
据这些投票结果决定最终是否向用户开放受限资源的访问。

重要组件类介绍
    每个框架都有一些核心的概念,这些概念被固化为类和接口,成为框架的重要组件类。框架的管理类、操作类都在这些组件类的基础上进行操作。在进入Acegi框架的具体学习前,有必要事先了解一下这些承载Acegi框架重要概念的组件类。
    首先,我们要接触是UserDetails接口,它代表一个应用系统的用户,该接口定义了用户安全相关的信息,如用户名/密码,用户是否有效等信息,你可以根据以下接口方法进行相关信息的获取:
    String getUsername():获取用户名; 
     String getPassword():获取密码; 
     boolean isAccountNonExpired():用户帐号是否过期; 
     boolean isAccountNonLocked():用户帐号是否锁定; 
     boolean isCredentialsNonExpired():用户的凭证是否过期; 
     boolean isEnabled():用户是否处于激活状态。
    当以上任何一个判断用户状态的方法都返回false时,用户凭证就被视为无效。
   
UserDetails还定义了获取用户权限信息的方法:GrantedAuthority[]
getAuthorities(),GrantedAuthority代表用户权限信息,它定义了一个获取权限描述信息(以字符串表示,如
PRIV_COMMON)的方法:String getAuthority()。


图 3 用户和权限


   
在未使用Acegi之前,我们可能通过类似User、Customer等领域对象表示用户的概念,并在程序中编写相应的用户认证的逻辑。现在,你要做的一
个调整是让原先这些代表用户概念的领域类实现UserDetails接口,这样,Acegi就可以通过UserDetails接口访问到用户的信息了。 

   
UserDetails可能从数据库、LDAP等用户信息资源中返回,这要求有一种机制来完成这项工作,UserDetailsService正是充当这
一角色的接口。UserDetailsService接口很简单,仅有一个方法:UserDetails
loadUserByUsername(String username) ,这个方法通过用户名获取整个UserDetails对象。
Authentication代表一个和应用程序交互的待认证用户,Acegi从类似于登录页面、Cookie等处获取待认证的用户信息(一般是用户名密码)自动构造Authentication实例。



图 4 Acegi的认证用户


    Authentication可以通过Object
getPrincipal()获取一个代表用户的对象,这个对象一般可以转换为UserDetails,从中可以取得用户名/密码等信息。在
Authentication被AuthenticationManager认证之前,没有任何权限的信息。在通过认证之后,Acegi通过
UserDetails将用户对应的权限信息加载到Authentication中。Authentication拥有一个
GrantedAuthority[] getAuthorities()方法,通过该方法可以得到用户对应的权限信息。
   
Authentication和UserDetails很容易被混淆,因为两者都有用户名/密码及权限的信息,接口方法也很类似。其实
Authentication是Acegi进行安全访问控制真正使用的用户安全信息的对象,它拥有两个状态:未认证和已认证。UserDetails是代
表一个从用户安全信息源(数据库、LDAP服务器、CA中心)返回的真正用户,Acegi需要将未认证的Authentication和代表真实用户的
UserDetails进行匹配比较,通过匹配比较(简单的情况下是用户名/密码是否一致)后,Acegi将UserDetails中的其它安全信息(如
权限、ACL等)拷贝到Authentication中。这样,Acegi安全控制组件在后续的安全访问控制中只和Authentication进行交
互。

由于Acegi对程序资源进行访问安全控制时,一定要事先获取和请求用户对应的Authentication,Acegi框架必须为Authentication提供一个“寓所”,以便在需要时直接从“寓所”把它请出来,作为各种安全管理器决策的依据。

   
SecurityContextHolder就是Authentication容身的“寓所”,你可以通过
SecurityContextHolder.getContext().getAuthenication()代码获取Authentication。
细心观察一下这句代码,你会发现在SecurityContextHolder和Authentication之间存在一个getContext()中
介,这个方法返回SecurityContext对象。SecurityContext这个半路杀出来的程咬金有什么特殊的用途呢?我们知道
Authentication是用户安全相关的信息,请求线程其它信息(如登录验证码等)则放置在SecurityContext中,构成了一个完整的安
全信息上下文。SecurityContext接口提供了获取和设置Authentication的方法:
 Authentication getAuthentication()
 void setAuthentication(Authentication authentication)


图 5 认证用户信息存储器


   
SecurityContextHolder是Acegi框架级的对象,它在内部通过ThreadLocal为请求线程提供线程绑定的
SecurityContext对象。这样,任何参与当前请求线程的Acegi安全管理组件、业务服务对象等都可以直接通过
SecurityContextHolder.getContext()获取线程绑定的SecurityContext,避免通过方法入参的方式获取用户
相关的SecurityContext。

   
线程绑定模式对于大多数应用来说是适合的,但是应用本身会创建其它的线程,那么只有主线程可以获得线程绑定SecurityContext,而主线程衍生
出的新线程则无法得到线程绑定的SecurityContext。Acegi考虑到了这些不同应用情况,提供了三种绑定SecurityContext的
模式:
 SecurityContextHolder.MODE_THREADLOCAL:SecurityContext绑定到主线程,这是默认的模式;
 SecurityContextHolder.MODE_GLOBAL:SecurityContext绑定到JVM中,所有线程都使用同一个SecurityContext;
 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL::SecurityContext绑定到主线程及由主线程衍生的线程中。
    你可以通过SecurityContextHolder.setStrategyName(String strategyName)方法指定SecurityContext的绑定模式。
用户认证过程
  Acegi支持多种方式的用户认证:如典型的基于数据库的认证、基于LDAP的认证、基于Yale中心认证等方式。不同的认证环境拥有不同的用户认证方式,现在我们先抛开这些具体的细节,考察一下Acegi对受限资源进行访问控制的典型过程:
    1.你点击一个链接访问一个网页;
    2.浏览器发送一个请求到服务器,服务器判断出你正在访问一个受保护的资源;
    3.如果此时你并未通过身份认证,服务器发回一个响应提示你进行认证——这个响应可能是一个HTTP响应代码,抑或重定向到一个指定页面;
    4.根据系统使用认证机制的不同,浏览器或者重定向到一个登录页面中,或者由浏览器通过一些其它的方式获取你的身份信息(如通过BASIC认证对话框、一个Cookie或一个X509证书);
    5.浏览器再次将用户身份信息发送到服务器上(可能是一个用户登录表单的HTTP POST信息、也可能是包含认证信息的HTTP报文头);
    6.服务器判断用户认证信息是否有效,如果无效,一般情况下,浏览器会要求你继续尝试,这意味着返回第3步。如果有效,则到达下一步;
    7.服务器重新响应第2步所提交的原始请求,并判断该请求所访问的程序资源是否在你的权限范围内,如果你有权访问,请求将得到正确的执行并返回结果。否则,你将收到一个HTTP 403错误,这意味着你被禁止访问。
    在Acegi框架里,你可以找到对应以上大多数步骤的类,其中ExceptionTranslationFilter、AuthenticationEntryPoint、AuthenticationProvider以及Acegi的认证机制是其中的代表者。

   
ExceptionTranslationFilter是一个Acegi的Servlet过滤器,它负责探测抛出的安全异常。当一个未认证用户访问服务器
时,Acegi将引发一个Java异常。Java异常本身对HTTP请求以及如何认证用户是一无所知
的,ExceptionTranslationFilter适时登场,对这个异常进行处理,启动用户认证的步骤(第3步)。如果已认证用户越权访问一个资
源,Acegi也将引发一个Java异常,ExceptionTranslationFilter则将这个异常转换为HTTP
403响应码(第7步)。可见,Acegi通过异常进行通讯,
ExceptionTranslationFilter接收这些异常并作出相应的动作。

   
当ExceptionTranslationFilter通过Java异常发现用户还未认证时,它到底会将请求重定向哪个页面以要求用户提供认证信息呢?
这通过咨询AuthenticationEntryPoint来达到目的——Acegi通过AuthenticationEntryPoint描述登录页
面。

   
当你的浏览器通过HTTP表单或HTTP报文头向服务器提供用户认证信息时,Acegi需要将这些信息收集到Authentication中,Acegi
用“认证机制”描述这一过程。此时,这个新生成Authentication只包含用户提供的认证信息,但并未通过认证。
AuthenticationProvider
负责对Authentication进行认证。AuthenticationProvider究竟如何完成这一过程呢?请回忆一下上节我们所介绍的
UserDetails和UserDetailsService,大多数AuthenticationProvider通过
UserDetailsService获取和未认证的Authentication对应的UserDetails并进行匹配比较来完成这一任务。当用户认
证信息匹配时,Authentication被认为是有效的,AuthenticationProvider进一步将UserDetails中权限、
ACL等信息拷贝到Authentication。
当Acegi通过认证机制收集到用户认证信息并填充好Authentication后,Authentication将被保存到SecurityContextHolder中并处理用户的原始请求(第7步)。

    你完全可以抛开Acegi的安全机制,编写自己的Servlet过滤器,使用自己的方案构建Authentication对象并将其放置到SecurityContextHolder中。也许你使用了CMA(Container
Managed Authentication:容器管理认证),CMA允许你从ThreadLocal或JNDI中获取用户认证信息,这时你只要获取这些信息并将其转换为Authentication就可以了。
   安全对象访问控制
   
Acegi称受保护的应用资源为“安全对象”,这包括URL资源和业务类方法。我们知道在Spring
AOP中有前置增强、后置增强、异常增强和环绕增强,其中环绕增强的功能最为强大——它不但可以在目标方法被访问前拦截调用,还可以在调用返回前改变返回
的结果,甚至抛出异常。Acegi使用环绕增强对安全对象进行保护。
    Acegi通过AbstractSecurityInterceptor为安全对象访问提供一致的工作模型,它按照以下流程进行工作:
    1. 从SecurityContext中取出已经认证过的Authentication(包括权限信息);
    2. 通过反射机制,根据目标安全对象和“配置属性”得到访问目标安全对象所需的权限;
    3. AccessDecisionManager根据Authentication的授权信息和目标安全对象所需权限做出是否有权访问的判断。如果无权访问,Acegi将抛出AccessDeniedException异常,否则到下一步;
4. 访问安全对象并获取结果(返回值或HTTP响应);
5. AbstractSecurityInterceptor可以在结果返回前进行处理:更改结果或抛出异常。
    Acegi称受保护的应用资源为“安全对象”,这包括URL资源和业务类方法。我们知道在Spring
AOP中有前置增强、后置增强、异常增强和环绕增强,其中环绕增强的功能最为强大——它不但可以在目标方法被访问前拦截调用,还可以在调用返回前改变返回
的结果,甚至抛出异常。Acegi使用环绕增强对安全对象进行保护。    
Acegi通过AbstractSecurityInterceptor为安全对象访问提供一致的工作模型,它按照以下流程进行工作:     1.
从SecurityContext中取出已经认证过的Authentication(包括权限信息);     2.
通过反射机制,根据目标安全对象和“配置属性”得到访问目标安全对象所需的权限;     3.
AccessDecisionManager根据Authentication的授权信息和目标安全对象所需权限做出是否有权访问的判断。如果无权访
问,Acegi将抛出AccessDeniedException异常,否则到下一步; 4. 访问安全对象并获取结果(返回值或HTTP响应);
5. AbstractSecurityInterceptor可以在结果返回前进行处理:更改结果或抛出异常。


图 6 AbstractSecurityInterceptor工作流程


   
安全对象和一般对象的区别在于前者通过Acegi的“配置属性”进行了描述,如“/view.jsp=PRIV_COMMON”配置属性就将“
/view.jsp”这个URL资源标识为安全对象,它表示用户在访问/view.jsp时,必须拥有PRIV_COMMON这个权限。配置属性通过
XML配置文件,注解、数据库等方式提供。安全对象通过配置属性表示为一个权限,这样,Acegi就可以根据Authentication的权限信息获知
用户可以访问的哪些安全对象。
    根据安全对象的性质以及具体实现技术,AbstractSecurityInterceptor拥有以下三个实现类:
 FilterSecurityInterceptor:对URL资源的安全对象进行调用时,通过该拦截器实施环绕切面。该拦截器使用Servlet过滤器实现AOP切面,它本身就是一个Servlet过滤器;
 MethodSecurityInterceptor:当调用业务类方法的安全对象时,可通过该拦截器类实施环绕切面;
 AspectJSecurityInterceptor:和MethodSecurityInterceptor类似,它是针对业务类方法的拦截器,只不过它通过AspectJ实施AOP切面。

Acegi版本升级的一些重大变化 
   
Acegi项目开始于2003年,Acegi团队在发布新版本时非常谨慎,在本书写作之时,Acegi最新版本为1.0.3。在此之前Acegi已经发布
了10多个预览版本,由于Acegi框架优异的表现,许多大型应用早在Acegi
1.0正式版本发布之前(2006年5月),就已经采用Acegi框架作为其安全访问控制的解决方案。

    在Acegi社区里,来自世界各地众多优秀的安全领域专家对Acegi的改进和发展献计献策,Acegi团队广泛听取并吸收各种有益的建议,将它们融入到Acegi的框架中,使Acegi成为构建在Spring基础上企业应用的首选安全控制框架。
Acegi 1.0.3版本相比于早期预览版本发生了很大的变化,对于需要进行Acegi版本的项目来说,了解这一变化特别重要。下面,我们列出Acegi的一些重大的升级更新:

包名的更新:在0.9.0及之前的版本中,Acegi采用net.sf.acegisecurity包名前缀,在1.0.0版本之后更改为
org.acegisecurity(Hibernate也走过相同的道路,好在Acegi在正式版本发布之时就完成了这种转变);


ACL模块的调整:ACL模块发生了重大的调整,Acegi团队接收了社区大量关于ACL模块的反馈意见,重新设计了ACL模块的底层结构,在性能、封装
性、灵活性上得到了质的提升。事实上,Acegi使用org.acegisecurity.acls包代替了原来的
org.acegisecurity.acl包,后者将在后期的版本中删除,由于这种伤筋动骨的变化,将很难兼容原来ACL模块。不过,目前基于新框架的
ACL模块还没有进行充分的测试,Acegi承诺在1.1.0版本发布时提供最终的实现;


删除了ContextHolder及其相关类:在Acegi
0.9版本中,ContextHolder及其相关类被彻底从Acegi项目中删除。ContextHolder可以在多个HTTP请求中共享同一个
ThreadLocal,这和Spring提倡的ThreadLocal只应在同一线程中共享相悖。现在,Acegi使用
SecurityContextHolder替换ContextHolder,它的生命周期是一个HTTP 请求;


使用FilterChainProxy同时代理多个过滤器:在早期的版本中,Acegi通过FilterToBeanProxy将web.xml中的
Servlet过滤器定义转移到Spring容器中。这比直接在web.xml中配置Servlet过滤器要方便一些,但是Acegi框架往往需要定义多
个Servlet过滤器,使web.xml配置文件变得冗长难看。在Acegi
0.8版本中提供FilterChainProxy,它可以同时代理多个Servlet过滤器并保证过滤器的顺序。因此在新版本
中,FilterChainProxy成为推荐的选择。

    小结
   
Acegi是Spring项目下一个成熟的安全访问控制框架,它允许利用了Spring
IoC的AOP的功能完成安全对象的访问控制。在Acegi框架中,SecurityContextHolder处于非常核心的位置,它是存放认证管理器
用户安全信息SecurityContext的“容器”,SecurityContext保存着用户安全访问控制所需的信息,直接被访问决策管理器使用。
HttpSessionContextIntegrationFilter通过在SecurityContextHolder和HttpSession中
摆渡SecurityContext,使多个请求线程可以共享同一个SecurityContext。




使用Acegi进行身份认证(之一)

使用Acegi进行身份认证(之一)

【IT168 技术文档】概述

Acegi是专门为Spring Web应用提供安全保护的开源框架,它通过配置的方式就可以对业已存在的应用实施安全控制。在Acegi实施安全控制之前,必须获取操作者的身份,并进一步获知用户的权限,这样Acegi才可能对应用资源实施安全控制。在本文中,我们将介绍Acegi如何对操作者进行身份认证的内容。

将Acegi集成到Web应用程序中

Acegi
通过多个不同用途的Servlet过滤器截取HTTP请求,实施访问安全的控制。按照传统的方式,我们应该在web.xml配置文件中通
过<filter>定义Servlet过滤器并使用<filter-mapping>元素定义过滤器对应的URL匹配模式。

由于这些过滤器需要Spring容器中其它Bean的支持完成访问权限控制,最原始的方式是在过滤器中通过硬编码的方式访问Spring容器中的Bean:

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
FooBean foo = (FooBean)ctx.getBean(“foo”);

这种硬编码的方式显然不太方便,如何让过滤器既能够正确工作又能享受到Spring IoC的好处呢?Acegi通过“过滤器代理”来解决这个问题。

代理Acegi的过滤器

org.acegisecurity.util.FilterToBeanProxy
使用Spring容器中的Bean代理过滤器,FilterToBeanProxy是一个标准的Servlet过滤器。在内部
FilterToBeanProxy通过WebApplicationContext访问Spring容器,并将过滤器处理逻辑委托给一个Spring容
器中的Bean。这样,我们就可以在web.xml中定义FilterToBeanProxy,声明过滤器匹配的URL模式,并在Spring容器中充分
利用IoC进行定义委托的Bean。

FilterToBeanProxy支持两个初始化参数:

l
targetClass:委托Bean的全限定类名,委托类必须实现过滤器接口。你必须在Spring容器中至少定义一个该类的Bean,否则将抛出异
常。当Spring容器中拥有多个该委托类的Bean时,FilterToBeanProxy选择第一个委托类的Bean(这种情况应该避免);

l targetBean:当然你也可以通过一个Bean名指定委托的Bean。
图 1说明了Acegi是如何通过FilterToBeanProxy将Servlet容器和Spring容器结合起来共同定义一个功能齐备的Servlet过滤器的:
图 1 通过过滤器代理将Servlet容器和Spring容器结合起来

org.acegisecurity.securechannel.ChannelProcessingFilter
是一个Acegi的过滤器,它负责处理安全通道的转换。如果直接在web.xml中定义ChannelProcessingFilter,我们将很难在不
编写代码的情况下,使其引用Spring容器中的Bean。借助FilterToBeanProxy的帮助,我们就可以放心地在Spring容器中配置
ChannelProcessingFilter,得到一个完整可用实例。当然ChannelProcessingFilter是需要服务于特定URL请
求的,而过滤器URL映射只能在Servlet容器的web.xml中定义,这个工作由FilterToBeanProxy对应的<filter-
mapping>元素完成。

如果我们将Servlet容器和Spring容器比作牛郎和织女,那么
FilterToBeanProxy无疑就是鹊桥了。事实上,Acegi本身并不依赖于FilterToBeanProxy,它是一种充分利用
Spring容器依赖注入好处配置Servlet过滤器的一个方法,正因为如此,所以Acegi社区中有很多开发者建议
FilterToBeanProxy应该从Acegi中剥离出来,将其加入到Spring的核心类库中。
使用代理过滤器链对处理HTTP请求

Acegi通过众多的过滤器完成不同安全
制的任务,在前面我们提到了三个过滤器:HttpSessionContextIntegrationFilter、
ExceptionTranslationFilter和ChannelProcessingFilter。除此以外,还有LogoutFilter、
AuthenticationProcessingFilter等不下10个的过滤器。

当需要配置多个Servlet过滤器时,虽然
我们可以通过FilterToBeanProxy分别进行配置,但这将导致冗长难看的web.xml,同时还需要小心谨慎地通过过滤器的配置顺序保证它们
的调用顺序。为了解决这个问题,Acegi在0.8版本中添加了一个org.acegisecurity.util.FilterChainProxy。
FilterChainProxy可以同时指定多个过滤器并将它们组成一个过滤器链,而FilterToBeanProxy只要将代理目标设置为
FilterChainProxy就可以了。来看下面的配置:

代码清单 1 web.xml
<web-app>
<context-param> ①指定Spring配置文件
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext.xml,
classpath:applicationContext
-acegi-plugin.xml
</param-value>
</context-param>
<filter>②将过滤器委托给FilterChainProxy
<filter-name>AcegiFilterChainProxy</filter-name>
<filter-class>
org.acegisecurity.util.FilterToBeanProxy
</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>
org.acegisecurity.util.FilterChainProxy
</param-value>
</init-param>
</filter>
<filter-mapping>③对所有的URL进行过滤
<filter-name>AcegiFilterChainProxy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>

</web-app>

在web.xml中我们首先需要定义Spring的配置文件,以便将Spring容器集成到Servlet容器中。紧接着,我们定义一个
FilterToBeanProxy的过滤器拦截所有URL请求并委托给FilterChainProxy进行处理。FilterChainProxy在
Spring容器中定义如下所示:

代码清单 2 applicationContext-acegi-plugin.xml
<beans>
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">①目标委托的Bean
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_UPPERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
②多个过滤器组成的过滤器链
/**=channelProcessingFilter,httpSessionContextIntegrationFilter,logoutFilter
</value>
</property>
</bean>
<bean id="channelProcessingFilter" ③过滤器链中的过滤器(1)
class="org.acegisecurity.securechannel.ChannelProcessingFilter">

</bean>
<bean id="httpSessionContextIntegrationFilter" ④过滤器链中的过滤器(2)
class="org.acegisecurity.context.HttpSessionContextIntegrationFilter">

</bean>

</beans>

在①处定义的FilterChainProxy
Bean是web.xml的FilterToBeanProxy的委托目标。FilterChainProxy通过
filterInvocationDefinitionSource定义多个相互链接的过滤器,如②所示。这些过滤器以逗号分隔,它表示web.xml中
定义的匹配URL(/*)请求将分别通过channelProcessingFilter、
httpSessionContextIntegrationFilter及logoutFilter这三个过滤器的拦截处理。这个过滤器链通过过滤器对
应的Bean名字(如③和④)进行定义,过滤器都实现了javax.servlet
.Filter接口。你完全可以参照实例方式定义多组滤器链,以便处理不同URL请求。不过,在一般情况下,你只要配置一个过滤器链就可以了。

filterInvocationDefinitionSource属性由两类信息组成:其一是指令信息,如

CONVERT_URL_TO_UPPERCASE_BEFORE_COMPARISON
表示判断URL匹配时,首先将URL转变为大写的格式,如果设置为
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON则表示比较前事先转换为小写的形式。而
PATTERN_TYPE_APACHE_ANT表示使用Ant路径风格进行匹配URL的描述,如果不提供这个指令,Acegi使用正则表达式来解析
URL路径映射。
1




身份认证管理

使用Acegi保护应用程序的第一步是根据用户提供的认证信息进行身份认证,以确定用户的身份,获取对应的权限信息,准备好Authentication。通过认证的Authentication拥有权限信息,它是Acegi进行后续安全对象访问安全控制的依据。

Acegi
对所有类型安全对象的访问控制机制基本相同:判断Authentication是否已经包含访问安全对象所需的权限。但Acegi进行身份认证的方式则多
姿多彩、五花八门,身份认证方式的多样性源于认证系统的多样性。由于篇幅所限,不可能面面俱到的逐一讲解所有的认证方式,我们将以最常见的基于数据库的认
证方式并对其它的认证方式进行简单的概述。

基于内存存储用户信息的身份认证

如果你开发的系统仅有少数几个固定的用户,且用户信息管理的功能很简单,这时你可以将用户直接保存在配置文件中,在系统启动将使用户信息常驻于内存中。下面,我们就从这个最简单的身份认证方式出发开始学习Acegi实际应用的征程。
代码清单 3 applicationContext-acegi-plugin.xml
身份认证过滤器

<beans>
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>

/**=authenticationProcessingFilter ①使用认证处理过滤器处理匹配的URL
</value>
</property>
</bean>
<bean id="authenticationProcessingFilter" ②认证处理过滤器
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
②-1过滤器处理的URL
<property name="filterProcessesUrl" value="/j_acegi_security_check"/>
<property name="defaultTargetUrl" value="/main.jsp"/> ②-2认证成功后转向的URL
②-3认证失败后转向的URL
<property name="authenticationFailureUrl" value="/index.jsp?login_error=1"/>
</bean>
</beans>


首先,在①处通过FilterChainProxy定义了仅包含一个authenticationProcessingFilter的过滤器链。这
个过滤器链匹配于所有URL的请求,换句话说,authenticationProcessingFilter有机会对所有URL请求进行拦截处理,而在
这所有这些URL请求中,究竟哪个URL请求才是用户身份认证的请求呢?如果你心中也拥有这个疑问,则说明你正走在正确的理解问题的思路上。

authenticationProcessingFilter
在②-1、2、3处,定义了一些URL,它们分别对应身份认证请求的URL、认证成功后转向的URL以及认证失败后转向的URL。其中②-1处的
filterProcessesUrl属性表示这个URL是用户提交身份信息并要求进行身份认证的HTTP请求(一般是一个包含用户名/密码的表单
HTTP
POST请求),“/j_acegi_security_check”是默认的设置,之所以在此显式给出设置值,是为了说明你可以根据实际需要进行调整。


现在,authenticationProcessingFilter已经知道了申请进行用户身份认证URL,但它如何知道这个HTTP
请求分别通过什么参数代表用户名和密码呢?Acegi框架规定必须使用j_username和j_password作为用户名和密码的HTTP参数名。这
两个参数名是固定的,Acegi不允许对此进行配置,不过这种约定俗成而非事事配置的风格正渐渐成为流行的设计模式。

我们来看一下提供用户名/密码输入表单的登录页面:
代码清单 4 index.jsp:登录页面

<%@ page contentType="text/html;charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<body>
<c:if test="${not empty param.login_error}">
<font color="red">用户名或密码错误。</font>
</c:if>
②处理用户身份认证服务所对应的URL
<form name="form1" method="post" action="<c:url value="/j_acegi_security_check"/>">
用户名:
<input type="text" name="j_username"/><br/>③用户名的参数名
密 码:
<input type="password" name="j_password"/><br/>④用户密码的参数名
<input type="submit" value="登录"/>
</form>
</body>
</html>


 ②处对应代码清单
3中②-1处的设置值,它表示处理用户身份认证服务所在的URL,而③和④是Acegi规定的承载用户名、密码的参数名。当这个表单提交后
authenticationProcessingFilter将从请求中抽取j_username和j_password参数的值,两者组成了代表请求
认证的用户信息,Acegi使用这个信息构造Authenticaion实例,并封装成SecurityContext放入到
SecurityContextHolder中,然后启动用户身份认证的流程。当认证成功后,页面转向代码清单
3中②-2处所定义的/main.jsp,如果认证失败则转向代码清单
3中②-3处所定义的/index.jsp?login_error=1。由于登录失败后也转向index.jsp页面,所以代码清单
4的①处专门对此进行了处理,当发现请求参数包含login_error时,打出一行提示登录失败的信息。

现在,我们已经了解了
authenticationProcessingFilter从何处获取需要进行认证的用户信息,并对认证成功和失败后的转向进行了定义。那么
authenticationProcessingFilter究竟如何对用户信息(用户名/密码)的合法性进行认证呢?

authenticationProcessingFilter其实只负责获取需要认证的用户信息并根据结果完成页面转向,它相当于一个客户受理的窗口,真正的用户身份认证工作则交由后台的AuthenticationManager完成。


下面,我们为authenticationProcessingFilter注入AuthenticationManager,后者将完成具体的身份认证工作:
代码清单 5 applicationContext-acegi-plugin.xml
认证管理器的配置

<beans>

<bean id="authenticationProcessingFilter"
class
="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<property name="filterProcessesUrl" value="/j_acegi_security_check"/>
<property name="defaultTargetUrl" value="/main.jsp"/>
<property name="authenticationFailureUrl" value="/index.jsp?login_error=1"/>
①注入认证管理器
<property name="authenticationManager" ref="authenticationManager" />
</bean>
②认证管理器
<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
-1使用基于DAO的认证提供者提供认证服务
<ref local="daoAuthenticationProvider" />
</list>
</property>
</bean>
<bean id="daoAuthenticationProvider" ③基于DAO的认证提供者
class
="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
-1根据用户名获取系统中真实UserDetails 对象的服务类
<property name="userDetailsService" ref="userDetailsService" />
</bean>
④ 该服务类根据缓存在内存中的用户信息列表获取userDetails对象
<bean id="userDetailsService" class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl">
<property name="userMap">-1用户信息
<value>
john
=john,PRIV_COMMON,PRIV_1
tom
=tom,PRIV_COMMON,PRIV_1,PRIV_2
peter
=peter,disabled,PRIV_COMMON,PRIV_1
</value>
</property>
</bean>
</beans>


在①处,我们为authenticationProcessingFilter注入了一个AuthenticationManager Bean。AuthenticationManager有两个实现类:
l MockAuthenticationManager:这个实现类用于开发时的测试环境,它将所有待认证的用户标志为有效有户;
l ProviderManager:该实现类将用户身份认证的工作委托给多个提供者来完成。


里,我们使用ProviderManager来定义一个AuthenticationManager
Bean,如②所示。可以通过ProviderManager的providers属性配置多个认证提供者,认证提供者必须实现
org.acegisecurity.providers.AuthenticationProvider接口。身份认证的多样性通过
AuthenticationProvider接口实现类的多样性体现出来,Acegi针对不同的安全系统提供了10多个不同的认证提供者。这些不同的认证提供者在表 1中进行说明: 

这里,我们使用DaoAuthenticationProvider,在②-1所示。它负责从数据库或其它保存用户信息的媒介中获取用户信息进行认证。

DaoAuthenticationProvider
首先从SecurityContextHolder的Authentication中得到待认证的用户名,并根据该用户名获取保存在数据库或其它媒介中代
表真正系统用户的UserDetails对象。紧接着比较Authentication和UserDetails的匹配关系(如看密码是否相等),如果两
者匹配,认证成功,并将UserDetails的权限信息将拷贝到Authentication中。如果不匹配,认证将失败。

DaoAuthenticationProvider通过UserDetailsService完成UserDetails的获取工作,根据存储用户信息媒介的不同,Acegi提供了两个UserDetailsService的实现类:

l InMemoryDaoImpl:该实现类负责从内存中获取用户的信息,它允许通过UserMap或Properties直接在Spring配置文件中定义系统用户信息。

l JdbcDaoImpl:该实现类从数据库中获取用户的信息。我们将在下一节讨论该实现类的使用方法。


这里,我们使用InMemoryDaoImpl获取UserDetails,如③-1所示并通过userMap属性定义系统用户信息,如④-1所示。
userMap属性的类型为UserMap,Acegi已经向Spring注册了对应的编辑器,所以我们可以通过格式化的字符串为UserMap类型的属
性提供配置值。在④-1处,我们提供了几行键值对的配置值,每一行代表一个用户及其对应的权限信息。下面我们通过图
2来了解代表一个用户的字符串的格式:


8 用户配置信息


们通过④-1定义了三个用户,john、tom和peter,它们对应的密码和用户名相同。其中john和tom是激活的用户,而peter用户处于非激
活状态。john和peter都拥有PRIV_COMMON,PRIV_1权限,而tom额外拥有PRIV_2的权限。

如果用户数比较多,在Spring中直接进行配置未免不太雅观,这时,你可以将用户信息转移到一个属性文件中,并通过userProperties进行加载:

<bean id="userDetailsService"

class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl">

<property name="userProperties">

<bean class="org.springframework.beans.factory.config.PropertiesFactoryBean">

<property name="location" value="/WEB-INF/users.properties" />

</bean>

</property>

</bean>

users.properties文件内容的格式和刚才我们介绍的方式相同,既每一个用户对应一行格式化配置串。

通过以上分析后,我们可以理出参与用户身份认证所涉及到4个Bean,它们之间的关系可通过图 9来描述:



图 9 基于内存用户信息认证的主要Bean之间的关系



我们知道,大部分应用系统的用户信息并非一个小小的属性
文件可以应付的,它们一般保存在数据库中。InMemoryDaoImpl存在的更多意义在于方便程序测试——你可以不依赖外部数据库环境完成测试。在生
产环境下,我们应该使用JdbcDaoImpl这个UserDetailsService实现类从数据库中获取UserDetails。

基于数据库存储用户信息的认证

使用JdbcDaoImpl和InMemoryDaoImpl类似,它们的基本原理都是根据Authentication中待认证的用户名查询出代表真实系统用户的UserDetails对象。只不过,InMemoryDaoImpl通过查询驻留于内存中的格式化用户信息列表完成的,而JdbcDaoImpl则通过查询数据库达到目的。

在介绍JdbcDaoImpl的具体使用之前,我们先在数据库中创建用户信息表和用户权限表,并初始化一些测试的用户数据:

代码清单 6 创建用户及权限的SQL脚本
CREATE TABLE T_USER ( ①用户信息表

USER_ID INTEGER NOT NULL AUTO_INCREMENT,

USERNAME VARCHAR(
30) NOT NULL,

PASSWORD VARCHAR(
30) DEFAULT NULL,

STATUS TINYINT(
1) NOT NULL DEFAULT '0',

PRIMARY KEY (`USER_ID`),

UNIQUE KEY `USERNAME` (`USERNAME`)

);

CREATE TABLE T_USER_PRIV (②用户权限表

USER_ID INTEGER NOT NULL DEFAULT '
0',

PRIV_NAME VARCHAR(
30) DEFAULT NULL,

PRIMARY KEY (USER_ID, PRIV_NAME)

);

INSERT INTO T_USER (USER_ID, USERNAME, PASSWORD, STATUS) VALUES ③用户数据

(
1,'tom','tom',1),

(
2,'john','john',1);

INSERT INTO T_USER_PRIV (USER_ID, PRIV_NAME) VALUES ④用户授权表

(
1,'PRIV_1'),

(
1,'PRIV_2'),

(
1,'PRIV_COMMON'),

(
2,'PRIV_1'),

(
2,'PRIV_COMMON');

COMMIT;
你可以简单地运行随书光盘的chapter17/schema/mysql/db.sql脚本文件完成用户安全相关信息表的创建工作。

下面,我们用JdbcDaoImpl替换InMemoryDaoImpl,从数据库中获取UserDetails对象,其代码如代码清单 7所示:

代码清单 7 applicationContext-acegi-plugin.xml

基于数据库存储的用信息获取

<bean id="daoAuthenticationProvider"
class
="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
①调整为从数据库获取UserDetails对象的服务类
<property name="userDetailsService" ref="userDetailsService" />
</bean>
<bean id="userDetailsService" class="org.acegisecurity.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource" />-1 数据源
<property name="usersByUsernameQuery">-2 根据用户名查询用户的SQL语句
<value>
SELECT username,password, status FROM t_user WHERE username
= ?
</value>
</property>
<property name="authoritiesByUsernameQuery">-3 根据用户名查询用户权限记录的SQL语句
<value>
SELECT u.username,p.priv_name FROM t_user u,t_user_priv p
WHERE u.user_id
=p.user_id AND u.username = ?
</value>
</property>
</bean>


这种替换的难度很小,仅需要配置一个JdbcDaoImpl Bean并将其作为daoAuthenticationProvider的userDetailsService的实现即可。

JdbcDaoImpl
其实是基于Spring
JDBC技术进行数据库访问的,它继承于org.springframework.jdbc.core.support.JdbcDaoSupport。
所以JdbcDaoImpl必不可少的一个属性是dataSource,它指定一个保存用户信息的数据源。

JdbcDaoImpl通过usersByUsernameQuery和authoritiesByUsernameQuery属性定义查询用户信息和用户权限的SQL语句。实际上,JdbcDaoImpl为以上两个属性提供了默认的SQL语句,分别是:

"SELECT username,password,enabled FROM users WHERE username = ?"



"SELECT username,authority FROM authorities WHERE username = ?"


般情况下,我们会使用自己的用户和权限表,所以你需要自定义以上两个属性。JdbcDaoImpl将Authentication中待认证用户名作为这两
个SQL语句的username参数值,并执行查询返回结果,然后根据列索引的方式从结果集中取得数据构造UserDetails对象,所以在构造SQL
语句时必须保持用户信息字段位于正确的列位置,字段名可以任意指定。

应该说,JdbcDaoImpl还不是非常实用的
UserDetailsService实现类,因为用户对象(UserDetails)除包含用户名/密码、是否激活、权限等信息外,还经常需要包含一些
诸如email、telephone等业务相关的信息。而JdbcDaoImpl通用类并不知道我们具体的业务需求,所以我们往往需要通过实现
UserDetailsService接口提供自己的实现类来完成这些工作。编写一个自己的UserDetailsService实现类不费什么力气,你
只要实现接口中的UserDetails loadUserByUsername(String
username)方法根据用户名获取UserDetails对象即可。在编写好自己的UserDetailsService后,在Spring配置并替
换①处的userDetailsService属性即可,限于篇幅,我们不再展开论述,读者可以试着自行完成。

小结
Acegi通过过滤器对请求实施拦截,当发现用户没有登录时,将强制用户进行系统登录,通过一定的接口规范,Acegi就可以获取操作者的用户身份,并进而获取用户的权限。在下一篇文章中,我们将接着介绍身份认证的高级话题。


Acegi学习小结

Acegi学习小结


一、基本原理

Acegi认证授权主要基于两大技术,一是Filter机制,二是AOP的拦截机制。通过FilterSecurityInterceptor很
好地实现了对URI的保护,通过MethodSecurityInterceptor实现了对Service的方法的拦截保护,通过ACL
实现了对prototype类型的Object进行过滤和保护。



二、基本概念

HttpSessionContextIntegrationFilter 存储SecurityContext in HttpSession

ChannelProcessingFilter 重定向到另一种协议,如http到https



ConcurrentSessionFilter
因为不使用任何SecurityContextHolder的功能,但是需要更新SessionRegistry来表示当前的发送请求的
principal,通过在web.xml中注册Listener监听Session事件,并发布相关消息,然后由SessionRegistry获得消
息以判断当前用户的Session数量。



AuthenticationProcessingFilter 普通认证机制(大多数用这个)



CasProcessingFilter  CAS认证机制



BasicProcessingFilter Http协议的Basic认证机制



HttpRequestIntegrationFilter Authentication 从容器的HttpServletRequest.getUserPrincipal()获得



JbossIntegrationFilter 与Jboss相关。



SecurityContextHolderAwareRequestFilter 与servlet容器结合使用。



RememberMeProcessingFilter 基于Cookies方式进行认证。



AnonymousProcessingFilter 匿名认证。



ExceptionTranslationFilter 捕获所有的Acegi Security 异常,这样要么返回一个HTTP错误响应或者加载一个对应的AuthenticationEntryPoint



AuthenticationEntryPoint 认证入口



三、Acegi认证授权流程

1、FilterToBeanProxy 负责代理请求给FilterChainProxy



2、FilterChainProxy 方便的将多个Filter串联起来,如上面基本概念中提到的各种Filter,当然如果对URI进行授权保护,也可以包含FilterSecurityInterceptor。注意各Filter的顺序。



3、AbstractSecurityInterceptor 调度中心。负责调用各模块完成相应功能。

   FilterSecurityInterceptor 对URI进行拦截保护

AspectJSecurityInterceptor 对方法进行拦截保护

MethodSecurityInterceptor 对方法进行拦截保护



4、AuthenticationManager 用户认证

-> AuthenticationProvider 实际进行用户认证的地方(多个)。

     -> UserDetailsService 返回带有GrantedAuthority的UserDetail或者抛出异常。



5、AccessDecisionManager(UnanimousBased/AffirmativeBased/ConsensusBased) 授权

-> AccessDecisionVoter(RoleVoter/BaseAclEntryVoter) 实际投票的Voter(多个).



6、RunAsManager 变更GrantedAuthority



7、AfterInvocationManager 变更返回的对象

   -> BaseInvocationProvider 实际完成返回对象变更的地方(多个)。



四、Acegi实例



http://www.javaeye.com/topic/43341




javascript 处理 JSON


javascript 处理 JSON





javascript 处理 JSON


关键字: json javascript



    因为JSONjavascript 的一个子集,所以,在javascript 中使用JSON是非常简单的。

   

   
js 代码


 

  1. var myJSONObject = {"bindings": [  
  2.         {"ircEvent""PRIVMSG""method""newURI""regex""^http://.*"},  
  3.         {"ircEvent""PRIVMSG""method""deleteURI""regex""^delete.*"},  
  4.         {"ircEvent""PRIVMSG""method""randomURI""regex""^random.*"}  
  5.     ]  
  6. };  




在上面的例子中,我们创建了只包含一个成员 "bindings" 的一个对象,bindings 则包含了一个由3个对象组成的数组。这3个对象都包含3个成员:"ircEvent", "method","regex"。



javascript 中, 成员可以通过“点号”来获取。

比如:

js 代码


  1. myJSONObject.bindings[0].method  




通过eval() 函数可以将JSON字符串转化为对象。

js 代码


  1. var myObject = eval('(' + myJSONtext + ')');  




eval 函数非常快,但是它可以编译任何 javascirpt 代码,这样的话就可能产生安全的问题。eval 的使用是基于传入的代码参数是可靠的假设的,有一些情况下,可能客户端是不可信任的。



如果基于安全的考虑的话,最好是使用一个 JSON 解析器。 一个 JSON 解析器将只接受 JSON 文本。所以是更安全的。

js 代码


 

  1. var myObject = JSON.parse(myJSONtext, filter);  




可选的 filter 参数将遍历每一个value key 值对, 并进行相关的处理。如:




js 代码


  1. myData = JSON.parse(text, function (key, value) {       


    1. return key.indexOf('date') >= 0 ? new Date(value) : value;    }); 





stringifier 函数的作用跟 parse 相反, 用来将一个js对象转换为 JSON 文本。


js 代码


  1. var myJSONText = JSON.stringifier(myObject);  




这里是一个开源的 JSON parser and JSON stringifier





  • json2.rar (3.2 KB)
  • 描述: 一个开源的 JSON parser and JSON stringifier
  • 下载次数: 272













评论




tonyhelp
2008-09-17
引用


我也翻译过这篇文章



我也用过 DOJO ,现在改用 jQuery了




wangsong76
2008-08-22
引用


我也翻译过这篇文章

我也用过 DOJO ,现在改用 jQuery了。









在异步应用程序中发送和接收信息时,可以选择以纯文本和 XML 作为数据格式。掌握 Ajax 的这一期讨论另一种有用的数据格式 JavaScript Object Notation(JSON),以及如何使用它更轻松地在应用程序中移动数据和对象。

  如果您阅读了本系列前面的文章,那么应已对数据格式有了相当的认识。前面的文章解释了在许多异步应用程序中如何恰当地使用纯文本和简单的名称/值对。可以将数据组合成下面这样的形式:

firstName=Brett&lastName=McLaughlin&email=brett@newInstance.com


  这样就行了,不需要再做什么了。实际上,Web 老手会意识到通过 GET 请求发送的信息就是采用这种格式。

  然后,本系列讨论了 XML。显然,XML 得到了相当多的关注(正面和负面的评价都有),已经在 Ajax 应用程序中广泛使用。关于如何使用 XML 数据格式,可以回顾 本系列前面的文章:

<request>
<firstName>Brett</firstName>
<lastName>McLaughlin</lastName>
<email>brett@newInstance.com</email>
</request>



  这里的数据与前面看到的相同,但是这一次采用 XML 格式。这没什么了不起的;这只是另一种数据格式,使我们能够使用 XML 而不是纯文本和名称/值对。

  本文讨论另一种数据格式,JavaScript Object Notation(JSON)。JSON 看起来既熟悉又陌生。它提供了另一种选择,选择范围更大总是好事情。

  选择的意义

 
 在深入研究 JSON 格式的细节之前,您应该了解为什么要用两篇文章讨论另一种数据格式(是的,本系列中的下一篇文章也讨论
JSON),尤其在已经了解了如何使用 XML 和纯文本的名称/值对的情况下。其实,原因很简单:解决任何问题的选择越多,找到问题的最佳
解决方案的可能性就越大,这比只能使用一个 解决方案要好得多。

回顾名称/值对和 XML

  本系列已经用了大量篇幅讨论适合使用名称/值对和 XML 的场合。总是应该首先考虑使用名称/值对。对于大多数异步应用程序中的问题,使用名称/值对几乎总是最简单的解决方案,而且它只需要非常基本的 JavaScript 知识。

 
 实际上,除非有某种限制迫使您转向 XML,否则用不着考虑使用别的数据格式。显然,如果要向需要 XML
格式的输入的服务器端程序发送数据,那么希望使用 XML 作为数据格式。但是,在大多数情况下,对于需要向应用程序发送多段信息的服务器,XML
是更好的选择;换句话说,XML 通常更适合用来向 Ajax 应用程序做出响应,而不是从 Ajax 应用程序发出请求。

添加 JSON

 
 在使用名称/值对或 XML 时,实际上是使用 JavaScript
从应用程序中取得数据并将数据转换成另一种数据格式。在这些情况下,JavaScript 在很大程度上作为一种数据操纵语言,用来移动和操纵来自
Web 表单的数据,并将数据转换为一种适合发送给服务器端程序的格式。

  但是,有时候 JavaScript
不仅仅作为格式化语言使用。在这些情况下,实际上使用 JavaScript 语言中的对象来表示数据,而不仅是将来自 Web
表单的数据放进请求中。在这些情况下,从 JavaScript 对象中提取数据,然后再将数据放进名称/值对或 XML,就有点儿多此一举
了。这时就合适使用 JSON:JSON 允许轻松地将 JavaScript 对象转换成可以随请求发送的数据(同步或异步都可以)。

  JSON 并不是某种魔弹;但是,它对于某些非常特殊的情况是很好的选择。不要认为您不会遇到这些情况。阅读本文和下一篇文章来了解 JSON,这样,遇到这类问题时您就知道该怎么办了。

JSON 基础

 
 简单地说,JSON 可以将 JavaScript
对象中表示的一组数据转换为字符串,然后就可以在函数之间轻松地传递这个字符串,或者在异步应用程序中将字符串从 Web
客户机传递给服务器端程序。这个字符串看起来有点儿古怪(稍后会看到几个示例),但是 JavaScript 很容易解释它,而且 JSON
可以表示比名称/值对更复杂的结构。例如,可以表示数组和复杂的对象,而不仅仅是键和值的简单列表。

简单 JSON 示例

  按照最简单的形式,可以用下面这样的 JSON 表示名称/值对:

{ "firstName": "Brett" }


  这个示例非常基本,而且实际上比等效的纯文本名称/值对占用更多的空间:

firstName=Brett


  但是,当将多个名称/值对串在一起时,JSON 就会体现出它的价值了。首先,可以创建包含多个名称/值对的记录,比如:

{ "firstName": "Brett", "lastName":"McLaughlin", "email": "brett@newInstance.com" }


  从语法方面来看,这与名称/值对相比并没有很大的优势,但是在这种情况下 JSON 更容易使用,而且可读性更好。例如,它明确地表示以上三个值都是同一记录的一部分;花括号使这些值有了某种联系。

值的数组

 
 当需要表示一组值时,JSON 不但能够提高可读性,而且可以减少复杂性。例如,假设您希望表示一个人名列表。在 XML
中,需要许多开始标记和结束标记;如果使用典型的名称/值对(就像在本系列前面文章中看到的那种名称/值对),那么必须建立一种专有的数据格式,或者将键
名称修改为 person1-firstName 这样的形式。

  如果使用 JSON,就只需将多个带花括号的记录分组在一起:

{ "people": [
{ "firstName": "Brett", "lastName":"McLaughlin", "email": "brett@newInstance.com" },
{ "firstName": "Jason", "lastName":"Hunter", "email": "jason@servlets.com" },
{ "firstName": "Elliotte", "lastName":"Harold", "email": "elharo@macfaq.com" }
]}


  这不难理解。在这个示例中,只有一个名为 people 的变量,值是包含三个条目的数组,每个条目是一个人的记录,其中包含名、姓和电子邮件地址。上面的示例演示如何用括号将记录组合成一个值。当然,可以使用相同的语法表示多个值(每个值包含多个记录):

{ "programmers": [
{ "firstName": "Brett", "lastName":"McLaughlin", "email": "brett@newInstance.com" },
{ "firstName": "Jason", "lastName":"Hunter", "email": "jason@servlets.com" },
{ "firstName": "Elliotte", "lastName":"Harold", "email": "elharo@macfaq.com" }
],
"authors": [
{ "firstName": "Isaac", "lastName": "Asimov", "genre": "science fiction" },
{ "firstName": "Tad", "lastName": "Williams", "genre": "fantasy" },
{ "firstName": "Frank", "lastName": "Peretti", "genre": "christian fiction" }
],
"musicians": [
{ "firstName": "Eric", "lastName": "Clapton", "instrument": "guitar" },
{ "firstName": "Sergei", "lastName": "Rachmaninoff", "instrument": "piano" }
]
}


  这里最值得注意的是,能够表示多个值,每个值进而包含多个值。但是还应该注意,在不同的主条目(programmers、authors
和 musicians)之间,记录中实际的名称/值对可以不一样。JSON 是完全动态的,允许在 JSON 结构的中间改变表示数据的方式。

  在处理 JSON 格式的数据时,没有需要遵守的预定义的约束。所以,在同样的数据结构中,可以改变表示数据的方式,甚至可以以不同方式表示同一事物。

在 JavaScript 中使用 JSON

  掌握了 JSON 格式之后,在 JavaScript 中使用它就很简单了。JSON 是 JavaScript 原生格式,这意味着在 JavaScript 中处理 JSON 数据不需要任何特殊的 API 或工具包。

将 JSON 数据赋值给变量

  例如,可以创建一个新的 JavaScript 变量,然后将 JSON 格式的数据字符串直接赋值给它:

var people =
{ "programmers": [
{ "firstName": "Brett", "lastName":"McLaughlin", "email": "brett@newInstance.com" },
{ "firstName": "Jason", "lastName":"Hunter", "email": "jason@servlets.com" },
{ "firstName": "Elliotte", "lastName":"Harold", "email": "elharo@macfaq.com" }
],
"authors": [
{ "firstName": "Isaac", "lastName": "Asimov", "genre": "science fiction" },
{ "firstName": "Tad", "lastName": "Williams", "genre": "fantasy" },
{ "firstName": "Frank", "lastName": "Peretti", "genre": "christian fiction" }
],
"musicians": [
{ "firstName": "Eric", "lastName": "Clapton", "instrument": "guitar" },
{ "firstName": "Sergei", "lastName": "Rachmaninoff", "instrument": "piano" }
]
}


  这非常简单;现在 people 包含前面看到的 JSON 格式的数据。但是,这还不够,因为访问数据的方式似乎还不明显。

访问数据

 
 尽管看起来不明显,但是上面的长字符串实际上只是一个数组;将这个数组放进 JavaScript
变量之后,就可以很轻松地访问它。实际上,只需用点号表示法来表示数组元素。所以,要想访问 programmers 列表的第一个条目的姓氏,只需在
JavaScript 中使用下面这样的代码:

people.programmers[0].lastName;


  注意,数组索引是从零开始的。所以,这行代码首先访问 people 变量中的数据;然后移动到称为 programmers 的条目,再移动到第一个记录([0]);最后,访问 lastName 键的值。结果是字符串值 “McLaughlin”。

  下面是使用同一变量的几个示例。

people.authors[1].genre // Value is "fantasy"

people.musicians[3].lastName // Undefined. This refers to the fourth entry,
and there isn't one

people.programmers.[2].firstName // Value is "Elliotte"


  利用这样的语法,可以处理任何 JSON 格式的数据,而不需要使用任何额外的 JavaScript 工具包或 API。

修改 JSON 数据

  正如可以用点号和括号访问数据,也可以按照同样的方式轻松地修改数据:
people.musicians[1].lastName = "Rachmaninov";
  在将字符串转换为 JavaScript 对象之后,就可以像这样修改变量中的数据。

转换回字符串

  当然,如果不能轻松地将对象转换回本文提到的文本格式,那么所有数据修改都没有太大的价值。在 JavaScript 中这种转换也很简单:

String newJSONtext = people.toJSONString();


  这样就行了!现在就获得了一个可以在任何地方使用的文本字符串,例如,可以将它用作 Ajax 应用程序中的请求字符串。

  更重要的是,可以将任何 JavaScript 对象转换为 JSON 文本。并非只能处理原来用 JSON 字符串赋值的变量。为了对名为 myObject 的对象进行转换,只需执行相同形式的命令:

String myObjectInJSON = myObject.toJSONString();


  这就是 JSON 与本系列讨论的其他数据格式之间最大的差异。如果使用
JSON,只需调用一个简单的函数,就可以获得经过格式化的数据,可以直接使用了。对于其他数据格式,需要在原始数据和格式化数据之间进行转换。即使使用
Document Object Model 这样的 API(提供了将自己的数据结构转换为文本的函数),也需要学习这个 API 并使用 API
的对象,而不是使用原生的 JavaScript 对象和语法。

  最终结论是,如果要处理大量 JavaScript 对象,那么 JSON 几乎肯定是一个好选择,这样就可以轻松地将数据转换为可以在请求中发送给服务器端程序的格式。