对于一个框架的访问控制机制(或安全机制),我认为要满足以下两项:
1、最大的灵活性
业务流程与访问控制应该是平行的两条线,一方面我们可以灵活的在业务线任何点上拆装访问控制,不应该受业务流程的约束,同时,对于业务流程来说,访问机制也是透明的。可比喻为如果业务流程是高速公路上的汽车,访问机制就是无形的栏杆,对于司机正常行驶来说看不到栏杆,但是他一旦超速,那栏杆立刻的显现出来。
2、最小的颗粒度
访问控制的控制颗粒度越细越好,就像一个办公大楼,仅仅大门有门卫不行的,各屋子、甚至各柜子也要有锁。对于对象编程,我认为访问机制的最小颗粒度要能细化在每个对象的方法上,也就是对任意对象的、任意方法我们能加上不同的安全门。
在上面要求的基础上,我们看消息编程框架的访问控制机制的实现。
在模块工厂的配置文件中:
<module name="webauth" classfile=".servletutils.application.TLWAuth" configfile="auth_config.xml"/>
TLWAuth模块类为我们的访问机制控制类,我们给它的名称“webauth”。我们给它指定了配置文件auth_config.xml,现在看配置文件来了解功能。
<?xml version="1.0" encoding="UTF-8" ?>
<moduleConfig>
<params>
<defaultPolicy value="member" />
<defaultDenyMsg value="loginDeny" />
</params>
<!-- 拒绝消息表-->
<msgTable>
<msgid value="anonymousDeny" >
<msg action="anonymousDeny" destination="authModule"/>
</msgid>
<msgid value="loginDeny" >
<msg action="loginDeny" destination="authModule" />
</msgid>
<msgid value="refusedUserMsg" >
<msg action="IfUserFreezen" destination="memberModule" />
</msgid>
<msgid value="adminDenyMsg" >
<msg action="noAuth" destination="managerControl" />
</msgid>
</msgTable>
<!-- 安全策略定义 -->
<policies>
<policy name="anonymous"
ip="*"
denyMsg="anonymousDeny"
/>
<policy name="member"
user="*"
refusedUserMsg="refusedUserMsg"
role="member"
tokenExpire="check"
ip="*"
denyMsg="loginDeny"
/>
<policy name="admin"
user="*"
role="admin"
ip="*"
denyMsg="adminDenyMsg"
/>
</policies>
<!-- 对于每个模块定义策略 -->
<authModules>
<module name="clientJsonToMsg" defaultPolicy="anonymous" />
<module name="managerControl" defaultPolicy="admin" denyMsg="adminDenyMsg" login="anonymous" loginInput="anonymous" />
<module name="adminMemberControl" defaultPolicy="admin" />
<module name="homepage" defaultPolicy="anonymous" denyMsg="" http="member" getuser="member" />
<module name="servletDbTest" defaultPolicy="member" />
<module name="infomationControl" defaultPolicy="member" />
<module name="memberControl" defaultPolicy="member" />
<module name="servletDbTest" defaultPolicy="anonymous" />
</authModules>
<!--根据消息ID 定义策略,如app发送的json数据传来msgid -->
<authMsgids>
<msgid name="postInfo" policy="member" denyMsg="" />
<msgid name="getPublicKey" policy="anonymous" />
<msgid name="regist" policy="anonymous" />
<msgid name="login" policy="anonymous" />
</authMsgids>
<!--认证tag -->
<authTags>
<tag name="test" policy="admin" denyMsg="" />
</authTags>
</moduleConfig>
首先我们定义安全策略 policy,当前配置文件里面定义了三种安全策略 ,分别是 anonymous、member、admin,代表匿名用户、会员、管理员,当然策略可以根据自己的需求随意定制,策略名字也可以随意的起。每个策略里定义了访问审核要求,例如role定义了限制用户的角色,ip定义了所要求的ip,在member里有一项tokenExpire,这定义了需要token过期检查,意味着这member策略用于token用户。denyMsg为审核拒绝后的执行的消息ID,在消息表里msgTable定义了消息ID的内容。
<authModules>项目里定义了不同的模块的审核机制,基本从从语义可以看出意义,如
<module name="managerControl" defaultPolicy="admin" denyMsg="adminDenyMsg" login="anonymous" loginInput="anonymous" />
对应managerControl模块, 它的默认策略是admin,但是login和loginInput方法的策略是anonymous 。因为用户登录的页面是允许匿名的。除了login和loginInput方法,该模块的其他方法策略都是admin,也就是说要求管理员角色。如果访问拒绝,则执行拒绝消息"adminDenyMsg",在msgTable表中:
<msgid value="adminDenyMsg" >
<msg action="noAuth" destination="managerControl" />
</msgid>
adminDenyMsg消息的目的是"managerControl"模块,方法是"noAuth"。如果模块策略中没有定义denyMsg,则执行策略中的denyMsg,如果策略中也没有定义,则执行params项定义的默认拒绝消息defaultDenyMsg。
如果访问模块没有在这里定义,但还加了访问控制检查,那么执行默认的访问策略,params中的 defaultPolicy定义,本配置为member。
由此看来,我们访问控制机制的颗粒度落实到模块的方法级别上,对于模块的不同方法可以定义不同的安全策略。
authMsgids项是针对web服务或json客户端,针对客户端传来的消息msgId而定义不同策略,在上面配置中,登录login与注册regist策略为匿名,但是发信息postInfo则要求会员member。
authTags项定义了安全标签策略。某些时候对于方法里面需求安全审计时可以发送安全标签,通过安全标签对应的策略来审核。虽然我们的访问机制实现了安全tag,但我不太赞成这样,因为这需要在业务流程里面加访问控制代码,违反了上面说的透明原则。如果需要在代码里面加安全审计,我认为将一个方法分为两个方法,通过方法层面的访问控制会更灵活。
访问策略配置完毕,下面如何将这些策略应用到模块上,就像我们说的,安全机制对于模块是透明的,也就是模块自己不知道。这非常简单,在每个模块的配置文件项目中beforeMsgTable中添加一项消息即可,如对informationModule模块的getReply方法需要审核控制,则配置:
<beforeMsgTable>
<action value="getReply" >
<msg action="authInModule" destination="webauth"/>
</action>
</beforeMsgTable>
那么 当模块执行getReplay方法之前启动访问审核。如果对该模块所有的方法都审核 ,则将action设置为“*”,如下:
<action value="*" >
<msg action="authInModule" destination="webauth"/>
</action>
虽然可以给任意模块加审核机制,但实际应用来说,特别对于MVC框架,我们一般无需给后端的业务逻辑模块加审核,审核一般放在与用户接口的控制端。对于我们的框架,urlmap模块将url解析为控制模块,因此访问控制加在这里从而对所有的控制模块审核。看urlmap配置项:
<beforeMsgTable>
<action value="toServlet">
<msg action="auToLogin" destination="webUser" useInputMsg="false" usePreReturnMsg="false"/>
<msg action="authInUrlMap" destination="webauth" useInputMsg="false" usePreReturnMsg="false"/>
</action>
</beforeMsgTable>
urlmap模块的toServlet方法将url解析后的msg转到响应的控制模块,因此在该方法前加入访问控制。
如果我们需要在代码里面启动安全tag审核,则发出审核消息即可,如:
private void wtest1(Object fromWho, TLMsg msg) {
TLMsg returnMsg =putMsg("webauth" ,createMsg().setAction("authTag")
.setParam("tag","test"));
if(returnMsg!=null) //审核没有通过
return;
outData odata = creatOutDataMsg("test");
odata.addData("message","test----------:");
putOutData(odata);
}
通过给审核模块webauth发送消息authTag启动审核,审核tag标识为“test”,在上面配置中,该tag对应的策略是 admin,即管理员权限。审核通过后继续执行后续代码。前面说过我不赞同将安全机制放到业务代码里,例如上面代码,审核机制加入后将变得僵化,审核机制对业务流程失去了透明性,同时我们无法灵活的拆装这个审核点。上面的方法可以拆分成两个方法:
private void wtest1(Object fromWho, TLMsg msg) {
getMsg(this ,createMsg().setAction("realtest"));
}
private void realtest(Object fromWho, TLMsg msg){
outData odata = creatOutDataMsg("test");
odata.addData("message","test----------:");
putOutData(odata);
}
对要审核的方法调用不能直接调用,也要采用消息机制,这样审核模块才能发挥作用。在模块配置中加入对realtest的审核机制:
<beforeMsgTable>
<action value="realtest" >
<msg action="authInModule" destination="webauth"/>
</action>
</beforeMsgTable>
最后修改审核模块配置项,加入realtest的审核策略:
<module name="homepage" defaultPolicy="anonymous" denyMsg="" realtest="admin" http="member" getuser="member" />
那么将由审核模块自动启动审核机制。
由于我们的框架能在运行中动态配置,因此审核机制的部署、修改也能立刻发生作用,也体现了访问控制的及时性。
业务流程与安全访问机制分开并行,除了灵活性以外,我认为对项目开发来说也能提高效率。业务开发人员可专心负责业务流程的开发,安全审核责任人负责业务流程中的安全审核点。