|
接口(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技術.
|