|
接口(interface)总对应于某种明确的概念,它并不简简单单的等价于其成员函数的集合。有的接口如java.io.Serializable甚至没有任何成员函数。接口最重要的就是名, 是对概念的甄别。接口发布出去之后才能够被实现。当我们使用某个接口的时候,即使我们只用到其中部分函数,我们也必须负担整个概念。虽说"有名,万物之母", 并不是任何时候我们都需要名的。我们会说,就要那个,蓝色的,这么高,... blabla, 对,就是这个(this)。模板(template)弱化了类型系统,它对系统的约束直接作用在细节行为上,降低了明确建模的需求,不需要概念的分解,合并,比接口更加灵活。但模板并不是任何时候都比接口更好。想象一下,我们拿着一张采购单,上面写着需要某个物品,前面有个尖,后面有个帽,细长形,大概这么长,这么粗,上面有螺纹,螺距这么大,。。。这是...三号螺钉?嗯,最近有一种新产品,塑料材质的,你要不要试试。
模板与接口在某种程度上是互补的。
naked的人现在已经无法在自然界中生存了,
我们需要依赖外在的衣服,房屋等才能维持基本的生存条件。曾几何时,那些曾经属于我们身体的一部分的功能已经逐渐被解离到外部对象中,只有思想似乎还要依
赖个人自身的能力。很多研究正努力把人的思维过程也外化了(思维导图MindMap就是一种很有趣的简易方式),也许有一天我们会走到离开工具就无法思
考,无法判断的境地。
tpl自定义标签的设计目标之一是尽量减少配置说明项. 在tpl标签库中, 标签定义格式如下
<标签库名称>
<自定义标签名 demandArgs="argA, argB"
importVars="varA, varB"
otherArgs="optionalArgA, optionalArgB" localScope="trueOrFalse" >
自定义标签的内容, 可以是任何tpl代码
</自定义标签名>
</标签库名称>
demandArgs中指定调用时必须给定的参数的名称列表,
importVars指定从调用环境中导入的变量的名称列表,otherArgs指定可选参数的名称列表. demandArgs,
importVars和otherArgs这三者的集合包含了所有该自定义标签能够接受的参数. tpl编译器会检查这些调用规则是否被满足.
在运行的时候, 未指定的可选参数会被初始化为null.
在调用时明确的指定的变量值会覆盖importVars导入的变量值. 例如
<c:set var="varA" value="a" />
<MyLib:自定义标签名 /> // 根据importVars设定, 在此标签内varA的值为a
<MyLib:自定义标签名 varA="b" /> // args设定会覆盖importVars导入的值,因此在标签内部 varA的值为b
// 调用标签完成后, varA的值恢复为a
tpl中的参数声明方式是非常简化的,但是它仍然保留了最关键的信息:变量名称. 而在弱类型的Expresison Language中, 变量类型本来就不重要. 与jsp tag中的标签声明作个对比.
<tag>
<name>template</name>
<tagclass>edu.thu.web.tags.TemplateTag</tagclass>
<bodycontent>JSP</bodycontent>
<attribute>
<name>src</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
jsp tag这种标签声明方式非常冗长, 提供的有效信息密度很低, 而相对于tpl标签的声明方式所能够提供的附加信息也没有很大的意义. 这种设计上的问题也深深的影响到JSF等派生技术.
localScope参数指定了此自定义标签是否具有局部变量环境, 如果为true(缺省值),
则调用此标签的时候会自动进行变量压栈处理, 在标签内部无法访问参数列表之外的变量, 运行中所产生的临时变量也不会影响到外部环境.
tpl中的变量堆栈与webwork的ValueStack机制是有一些差异的.
webwork2中的ognl语言在访问OgnlValueStack中的对象的时候缺省采用的是一种递归查找机制, 即在当前环境中找不到对象,
则自动查找上一层环境中的变量. tpl中的标签结构可以多重嵌套, 产生非常复杂的结构, 所以缺省情况下tpl标签采用了类似于函数堆栈的设计,
在子标签中的代码一般情况下是无法访问父标签环境中的变量的(除非指定了localScope参数为true).
localScope支持与importVars机制相结合之后, 我们可以实现比OgnlValueStack更加灵活也更加稳健的变量访问策略.
动态权限最简单的一个表现是时限性,subject只在某个时间段内具有某种权限。这只需要在user和role的映射中,或者role自身的属性中增加startTime和expireTime即可。
更复杂的动态性一般与流程控制相关,此时权限控制应该由工作流系统完成,而不是在数据上增加越来越多的权限标记。在witrix平台中,使用tpl模板技术来定制权限设置。
权限管理中进行数据访问控制,其基本模式如下
operation target = selector(resource)
selector = user selector + auth filter
这里需要对resource的结构,以及选择算子的显式建模。selector必须允许权限系统追加filter,例如
IDataSource包中所使用的Query对象。
sql语言的表达能力有限,
作为选择算子来使用有时需要resource作一些结构上的调整,增加一些冗余的字段。例如表达一段时间内的利率,我们需要使用from_date和
to_date两个字段来进行描述,其中to_date的值与下一条记录的from_date相同。
value from_date to_date
0.01 2003-01-01 2003-05-01
0.012 2003-05-01 2004-01-01
如果表达一条航线中的多个阶段,我们可能会在每条记录中增加起始站和终点站两个字段。
更重要的一个常见需求是树形结构在关系数据库中的表达。为了能够直接操纵一个分支下的所有记录,在层次固定的情况下,我们可能会增加多个分类字段,例如数
据仓库中的层次维度。在层次数目不确定的情况下,我们将不得不使用层次码或者类似于url的其他方案,通过layer_code like
'01.01.%'
之类的语句实现分支选择。为了限制选择的深度,我们可能还需要layer_level字段。基于层次码和层次数,我们可以建立多种选择算子,例如包含所有
直接子节点,包含自身及所有父节点等等。
权限控制中,subject可能不会简单的对应于userId, 而是包含一系列的security token或certificate,
例如用户登陆地址,登陆时间等。一般情况下,这些信息在权限系统中的使用都是很直接的,不会造成什么问题。
subject域中最重要的结构是user和role的分离,可以在不存在user的情况下,为role指定权限。有人进一步定义了userGroup的
概念,可以为userGroup指定role,而user从其所属的group继承role的设置。一般情况下,我不提倡在权限系统中引入
userGroup的概念。这其中最重要的原因就是它会造成多条权限信息传递途径,从而产生一种路径依赖,
并可能出现信息冲突的情况。一般user与group的关联具有明确的业务含义,因而不能随意取消。如果我们希望对user拥有的权限进行细调,除去
user从group继承的某个不应该拥有的权限,解决的方法很有可能是所谓的负权限,即某个权限条目描述的是不能做某某事。负权限会造成各个权限设置之
间的互相影响,造成必须尝试所有权限规则才能作出判断的困境,引出对额外的消歧策略的需求,这些都极大的限制了系统的可扩展性。在允许负权限的环境中,管
理员将无法直接断定某个权限设置的最终影响,他必须在头脑中完成所有的权限运算之后才能理解某用户最终拥有的实际权限,如果发现权限设置冲突,管理员可能
需要多次尝试才能找到合适方案。这种配置时的推理需求可能会增加配置管理的难度,造成微妙的安全漏洞,而且负权限导致的全局关联也降低了权限系统的稳定
性。我更倾向于将group作为权限设置时的一种辅助标记手段,系统中只记录用户最终拥有的角色,即相当于记录用户通过group拥有权限的推导完成的结
果,
如果需要权限细调,我们直接在用户拥有的角色列表上直接进行。当然,如果实现的复杂一些,权限系统对外暴露的接口仍然可以模拟为能够指定
userGroup的权限。
推理在面向对象语言中最明显的表现是继承,所以有些人将subject域中的推理直接等价于role之间的继承问题,这未必是最好的选择。继承可以形成非
常复杂的推理关系,但是可能过于复杂了(特别是直接使用sql语句无法实现树形推理查询)。按照级列理论,从不相关发展到下一阶段是出现简单的序关系,即
我们可以说subject出现级别上的差异,高级别subject将自动具有低级别的权限。一种选择是定义roleRank,规定高级别role自动具有
低级别role的权限,但考虑到user与role的两分结构,我们也可以同时定义userRank和roleRank,规定高级别user自动具有低级
别的role,而role之间不具有推理关系。在面向对象领域中,我们已经证实了完全采用继承来组织对象关系会导致系统的不稳定,所以我倾向于第二种选
择,即将role看作某种类似于interface的东西,一种权限的切片。为了进一步限制这种推导关系,我们可以定义所谓的安全域的概念.
security domain, 规定推导只能在一定的域中才能进行。 select user.userId, role.roleId from user, role where user.userRank > role.roleRank and user.domain = role.domain
将权限控制一般需要施加在最细的粒度上,这在复杂的系统中可能过于理想化了。复杂的情况下我们需要进行局部化设计,即进行某些敏感操作之前进行一系列复杂
的权限校验工作。当完成这些工作之后,进入某个security zone, 在其中进行操作就不再需要校验了。
总的来说,权限系统采用非常复杂的结构效果未必理想。很多时候只是个管理模式的问题,应该尽量通过重新设计权限空间的结构来加以规避。不过在一些非常复杂
的权限控制环境下,也许简单的描述信息确实很难有效的表达权限策略(虽然我从未遇到过),此时尝试一下规则引擎可能比在权限系统中强行塞入越来越多的约束
要好的多。
权限控制可以看作一个filter模式的应用, 这也符合AOP思想的应用条件。在一个简化的图象中,我们只需要将一个判别函数
isAllowed(subject, operation,
resource)插入到所有安全敏感的函数调用之前就可以了。虽然概念上很完美,具体实现的时候仍然有一些细节上的问题。基本的困难在于很难在最细的粒
度上指定权限控制规则(连续的?动态的?可扩展的?),因而我们只能在一些关键处指定权限规则,或者设置一些整体性的权限策略,然后通过特定的推理来推导
出细粒度的权限规则,这就引出结构的问题。我们需要能够对权限控制策略进行有效的描述(控制策略的结构),并且决定如何与程序结构相结合。
subject,
operation和resource为了支持推理,都可能需要分化出复杂的结构,而不再是简单的原子性的概念。而在与程序结构结合这一方面,虽然AOP
使得我们可以扩展任何函数,但这种扩展需要依赖于cutpoint处所能得到的信息,因而权限控制的有效实施也非常依赖于功能函数本身良好的设计。有的时
候因为需要对结构有过于明确的假定,权限控制的实现不得不牺牲一定的通用性。
下面我们将分别讨论一下operation, subject和resource的结构分解的问题。首先是operation。
说到推理结构,让人最先想起的就是决策树,树形结构,在面向对象语言中可以对应于继承。金字塔式的树形结构也正是在现实世界中我们应用最多的控制结构。通过层层分解,operation的结构可以组织为一棵树,
应用程序 ==> 各个子系统 ==> 每个子系统的功能模块 ==> 子功能模块
==> 每个模块的功能点(具有明确的业务含义) ==> 每个功能点对应的访问函数(程序实现中的结构)
一个常见的需求是根据权限配置决定系统菜单树的显示,一般控制用户只能看到自己有权操作的功能模块和功能按钮。这种需求的解决方法是非常直接的。首先,在
后台建立子系统到功能模块,功能模块到功能点以及功能点到实现函数之间的映射表(如果程序组织具有严格规范,这甚至可以通过自动搜集得到)。然后,在权限
配置时建立用户与功能点之间的关联。此时,通过一个视图,我们就可以搜集到用户对哪些功能模块具有访问权限的信息。
为了控制菜单树的显示,witrix平台中的SiteMap采用如下策略:
1. 如果用户对某个子功能具有操作权限,则所有父菜单项都缺省可用
2. 如果用户对某个功能具有操作权限,并且标记为cascade,则所有子菜单项都自动缺省可用
3. 如果明确指定功能不可用,则该菜单及子菜单都强制不可用
4. 如果明确指定功能对所有人可用,则不验证权限,所有子菜单自动缺省可用
4. 强制设定覆盖缺省值
5. 不可用的菜单缺省不可见
6. 明确标记为可见的菜单即使不可用也可见
7. 父菜单可见子菜单才可见
我们通过预计算来综合考虑这些相互影响的控制策略。尽量将推导运算预先完成也是解决性能问题的不二法门。
在witrix平台中,每一次网络访问的url都符合jsplet框架所要求的对象调用格式,需要指定objectName和objectEvent参
数,这就对应于功能点的访问函数。访问控制点集中在objectManager并且访问格式是标准的。使用spring等AOP方式实现细粒度访问控制,
困难似乎在于不容易引入外部配置信息(例如功能点信息等),而且控制点所对应的对象函数格式也不统一,因而多数需要在细粒度上一一指定。
在系统中发生的事情,抽象的说都是某个主体(subject)在某个资源(resource)上执行了某个操作(operation)。
subject --[operation]--> resource
所谓权限管理,就是在这条信息传递路径中加上一些限制性控制。
主体试图去做的 limited by 系统允许主体去做的 = 主体实际做的。
可以看到,权限控制基本对应于filter模式。subject试图去做的事情应该由业务逻辑决定,因而应该编码在业务系统中。
先考虑最粗粒度的控制策略,控制点加在subject处,即无论从事何种操作,针对何种资源,我们首先需要确认subject是受控的。只有通过认证的用
户才能使用系统功能,这就是authentication。boolean isAllowed subject)
稍微复杂一些,控制可以施加在subject和operation的边界处(此时并不知道具体进行何种操作),称为模块访问控制,即只有某些用户才能访问特定模块。isAllowed(subject, operation set)
第三级控制直接施加在operation上,即操作访问控制。operation知道resource和subject(但它尚没有关于resource
的细节知识),我们能够采取的权限机制是bool isAllowed(subject, operation, resource),
返回true允许操作,返回false则不允许操作。
最简单的情况下,subject与resource之间的访问控制关系是静态的,可以直接写成一个权限控制矩阵
for operationA:
resourceA resourceB
subjectA 1 0
subjectB 0 1
isAllowed(subjectA, resourceA)恒等于true
如果多个operation的权限控制都可以通过这种方式来表示,则多个权限控制矩阵可以叠加在一起
for operationA, operationB:
resourceA resourceB
subjectA 10 01
subjectB 01 11
当subject和resource的种类很多时,权限控制矩阵急剧膨胀,它的条目数是N*M。很显然,我们需要进行矩阵分解。这也是最基本的控制手段之一: 在系统中增加一个瓶颈,或者说寻找到隐含的结构。
subject_resource = subject_role * role_resource
这
样系统权限配置条目的数量为 N*R + R*M, 如果R的数目远小于subject和resource,则实现简化。这称为RBAC(role
based access control),它的一个额外好处是权限系统的部分描述可以独立于subject存在,即在系统中没有任何用户的时候,通过角色仍然可以表达部分权限信息。可以说角色是subject在权限系统中的代理(分解)。
有时候引入一个瓶颈还不过瘾,有人引入组的概念,与role串联,
subject_resource = subject_group_role * role_resource
或着group与role并联,
subject_resource = subject_group * group_resource
与role稍有不同,一般情况下group的业务含义更加明显,可能对应于组织结构等。将组织机构明确引入权限体系,有的时候比较方便,但对于权限系统自
身的稳定性而言,未见得有什么太大的好处。并联模式有些多余,串联模式又过于复杂,细节调整困难,特别是多条控制路径造成的冲突情况。一般情况下,我不提
倡将group引入权限控制中。
比操作控制更加深入的控制就是数据控制了,此时需要对于resource的比较全面的知识。虽然表面上,仍然是
boolean isAllowed(subject, operation,
resource),但控制函数需要知道resource的细节。例如行级控制(row-level)或者列级控制(column-level)的实现。
因为我们一般情况下不可能将每一个条目都建模为独立的resource,而只能是存在一个整体描述,例如所有密级为绝密的文档。在witrix平台中,数
据控制主要通过数据源的filter来实现,因为查询条件(数据的定位条件)已经被对象化为Query类,所以我们可以在合适的地方自由的追加权限控制条
件。
以上的讨论中,权限控制都是根据某些静态描述信息来进行的,但现实世界是多变的。最简单的,当subject从事不同业务时,对应于同一组资源,也可能对
应的权限控制并不同(在witrix平台中,对应于IDataSource的模式切换)。更复杂一些, 在不同的时刻,
我们需要根据其他附加信息来作出是否允许操作的判断, 即此时我们权限设置的不仅仅是一些静态的描述信息, 而是一个完整的控制函数,
这就是所谓的工作流权限控制,一种动态权限控制.
软件开发是从设计开始的,
而设计的产物是一堆描述性的文档. 我们总是希望这些描述能够尽量完备, 例如在一个用例描述中我们总是希望加入尽量多的异常流描述,
尽量把所有的相关情况都同时呈现出来. 当我们对系统进行了大量的分解和分析工作之后, 往往会遇到一种理解上和验证上的困难,
即我们如何才能确保某个use case的运行结果恰好能够满足另外一个use case的输入需求, 整个系统能否精密的配合在一起.
此时我们可以依赖一些整体架构设计的文档描述, 或者补充更多的系统连接上的说明, 但是无论如何,
要在思维中同时把握那么多条执行路径是一件艰难的事情.
设计文档可以说是对系统行为的一种抽象性的规约,
为了验证这种抽象描述的正确性, 在缺乏理论保证的情况下, 我们唯一的选择就是抽样检验, 即我们需要构造一些测试用例,
特别是那些描述了一个完整业务流程的全局性的测试用例(用户故事). 在测试用例中, 我们并不需要构造出所有完整的执行路径,
只需要对一些关键性的业务路径进行检验就可以了, 局部的异常流处理很多时候都可以通过局部的单元测试来检验.
测试用例最好以测试代码的方式提供,而不是一组文本描述. 我们应该尽量在开发的早期使得全局测试用例就能够运行起来,
使它成为系统演化的驱动力之一, 并根据系统开发的进展同步的进行调整. 测试驱动开发(Test Driven
Development)所指的绝不仅仅是对单个类所进行的单元测试(Unit Test).
Test的一个重要作用在于实例化所有必要的抽象约束条件, 通过sample来驱动系统的发展.
http://ajaxanywhere.sourceforge.net/index.html
AjaxAnywhere利用JSP标签把Web页面标注出可以动态装载的区域, 可以直接把任何JSP页面转化为AJAX感知组件而不需要进行复杂的Javascript编码.
<script> ajaxAnywhere.getZonesToLoad = function(url){ return "countriesList"; } </script>
<select size="10" name="language" onchange="ajaxAnywhere.submitAJAX();">
<%@ include file="/locales_options_lang.jsp"%>
</select>
<aa:zone name="countriesList">
<select size="10" name="country" >
<%@ include file="/locales_options_countries.jsp"%>
</select>
</aa:zone>
AjaxAnywhere的这种做法与witrix平台中的ajax方案有些类似, 例如
<select onchange="new
js.Ajax().setObjectEvent('changeLanguage').setParam(this).setTplPart('countriesList').replaceChildren('countriesList')">
...</select>
<div id="countriesList">
<tpl:define id="countriesList">
....
</tpl:define>
</div>
但是在AjaxAnywhere的方案中, 后台jsp页面总是要完整运行的, 它通过servlet filter机制缓存所有的jsp输出,
而aa:zone标签则把自己的bodyContent运行后的结果保存在request的attribute中, 最后servlet
filter根据调用参数决定返回那些zone的运行结果. 而在witrix平台中的方案中, 只有指定的tplPart才会被运行,
其他部分完全被忽略. 这种差异的根源在于Jsp Tag技术本身的局限性. Jsp Tag的设计是非常原始的,
基本上就是在字符串层面上进行操作, 在运行的时候缺乏对页面结构强有力的控制. 实际上, 在我看来, 所有基于jsp tag的技术都受制于jsp
tag的先天的局限性, 很难有深度的发展, 包括JSF技术.
|