作者:[email protected],2013-03-02,太原。
关键字:RBAC, 权限管理, 角色权限, 资源权限
前几天把自己总结的MIS/WebMIS架构介绍文件发到了网上,就有网友问权限管理实现方面的问题,尤其是这个问题:如何实现数据权限与字段权限?
我以为,业务性非常强,这是管理软件必须具有的基本特性,不能脱离业务而单纯的搞记录级别权限或者字段权限。刚从业那几年,确实曾经幻想过在自己的管理软件实现非常细的数据操作许可,后来逐渐认为那是自己思考问题的方法错了。那些纯粹针对数据表、视图,甚至存储过程的权限管理,是DBMS内部的功能啊,是纯粹软件技术了。大家可以试想一下,如果那个管理软件强大/细化到系统业务管理人员打开界面,挑选数据表的记录或者字段来给用户分配详可的程度,那产品使用人员也需要是程序员啊,那还是管理软件吗?我的总结就是,在管理软件权限管理界面里,只可能挑选业务对象来给用户分配业务操作许可。业务对象与业务操作才是使用软件的业务人员可以理解的啊。
我们管理软件开发者总是会面对两个层次的问题:(1)纯粹DBMS技术层次的数据与约束、(2)客户业务层次的业务实体与功能/操作,需要对它们做到恰当的权限控制。我的总结是:搞管理软件的权限功能,应该着眼于管理系统内的那些业务功能/操作上。做项目多了就会体会到,不同功能/操作针对的数据一般不同,涉及的字段也大多不同,通过控制操作/功能的许可来间接控制数据与字段许可,这应该就是管理软件实现了数据权限与字段权限了吧。这种思考方法就是在系统分析/设计阶段,要直接面向业务对象与业务操作,分析/设计恰当的许可控制模型,从而间接管理DBMS层次的技术性数据与约束。回想当年我在实现时的困惑,那是因为实践经验少,不能把二者分而治之,混在一起一锅粥乱抓挖,越抓越乱啊。
通过前面的文字,我也点出了管理软件权限模型Oriented的两种基本Object:(1)业务操作、(2)业务实体对象。查验多种管理软件产品,权限管理无不是在管理这两个玩意儿。业务操作,常常对应到具体的功能,在UI层次常常表现为窗口、命令按钮,或者网页链接等等。虽然UI下面的BLL/Biz层次也可能需要加强控制,但业内大多实现在UI层次上。对于业务操作的权限控制,通常可以提取出适用性很强的模型,基于角色的访问控制(RBAC,Role-Based Access Control)就是在网上很多介绍的。再说到业务实体/对象的访问许可,就比业务操作的访问许可更加复杂一些了,因为它不像前者那样,可以在许多不同管理系统内,提炼为较统一的数据库设计与代码实现。后者的复杂之处在于,不同业务系统内具体的业务实体很难相同。例如关联企业组织结构的权限控制就非常常见,有的系统则是对于某类别文档、表格、报表的控制,针对其规律提炼的模型就更加抽象。如果追求通用性强的详细设计与实现,则对于具体的系统,好象总会有“专业性太强到没法用”的客户意见。接下来,我从简到繁的谈谈一些个人体会。
必须谈谈基于角色的访问控制(RBAC,Role-Based Access Control),它的基本思想是:权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。在一个业务系统中,角色很像工作岗位,是为了完成各种工作而创造,用户则依据它的责任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色。角色可依新的需求和系统的合并而赋予新的权限,而权限也可根据需要而从某角色中回收。
举个财务系统例子,会计角色负责做凭证,出纳角色负责管现金,至于谁来干会计,谁来干出纳,随时分配人员都可以啊。
上面是从百度百科“RBAC”条目得到的一个原理图。其把用户、角色、操作、实体分层次来分而治之,又通过恰当的关联组成一个系统单元,在管理软件领域有着非常普遍的适用性。像上图中业务操作关联业务实体,体现了通过业务操作来控制业务实体访问许可的思想。顺便说下,业内在此类需求内引入了个叫“资源”的概念,主要就是之那些形形色色的业务实体的。
RBAC子版本或者变体很多,各有针对性的实际需求。例如有些实现把部分资源关联到角色,那个模型名字就叫作“基于角色 资源道的访问控制”。当然还有把资源直接关联到用户帐号的:功能许可分配给角色使得用户间接得到操作许可,但是真正能不能操作还看有没有为他分配资源许可。例如父公司的会计不一定允许玩子公司的凭证。
我在这里采用简化的RBAC模型:(1)没有设计资源许可,以后具体项目时再扩充或者修改吧。(2)权限(操作及其关联的界面元素,以及BLL层次的方法调用许可)在系统设计/研发阶段编码定死了,本人智商不够高,不去追求在使用时增加新的业务操作与界面对应。(3)角色可以拥有多项操作权限,这是多对多对应,由客户在产品使用中灵活设定。(4)每个用户可分配一个角色,从而获得该角色的所有操作许可。
有人设计/实现用户和角色为多对多的对应关系,例如一个雇员可以是办公室主任,同时兼任后勤部主任,就是同时拥有2个角色。我的模型实现,则是需要新建一个办公室主任兼后勤部主任的新复合角色。这种每个用户只对应一个角色的设计,也可满足前者那种多对多的需求。我这样做的好处是简化了实现,也能满足了不少应用场景;弊端虽然也有,但是一般体现不出来。
下面是一个最简单的业务层操作封装代码:
public static partial class FolkManager
{
public static readonly IFolkService dal = DataAccess.CreateFolkService();
public static void Add(MyUser userToken, Folk model) { ... }
public static void Delete(MyUser userToken, string code) { ... }
public static void Update(MyUser userToken, Folk model) { ... }
public static Folk GetModel(string code) { ... }
public static DataTable GetTable(string filter) { ... }
public static IList<Folk> GetList(string filter) { ... }
public static DataTable GetTablePage (out int rowsCount, out int pagesCount,
string where, string orderBy, int pageSize, int pageIndex)
{ ... }
public static IList<Folk> GetListPage (out int rowsCount, out int pagesCount,
string where, string orderBy, int pageSize, int pageIndex)
{ ... }
public static string GetCodeByTitle(string title) { ... }
}
这些方法每一个都是一个完整的业务操作,内部组合调用数据访问层的相应方法。
一些严格的操作组合必须使用企业服务来实现,比如购物车-订单-库存-在线支付的情况。如何需要给第三方合作者提供为调用接口,可以为每个方法加上许可验证、数据有效性验证/安全性措施等来再次封装为 Web Service或者其它OpenAPI。
这是我WebMIS架构的UI后端层(个人喜欢用无界面元素aspx页)一个最简单例子代码:
public partial class MisBaseV2_Behind_FolkManager : PageBase
{
protected void Page_Load(object sender, EventArgs e)
{
if (!ValidateUser()) return;
if (!ValidateRight("00.80.02.06")) return;
string reqKey = Request.Params["ReqKey"];
if (string.IsNullOrEmpty(reqKey))
ResponseObjectAsJson(this.Response, MyAjaxResult.ReqKeyEmpty);
else if (reqKey.Equals("GetActions"))
this.GetActions();
else if (reqKey.Equals("Add"))
this.Add();
else if (reqKey.Equals("Delete"))
this.Delete();
else if (reqKey.Equals("Update"))
this.Update();
else if (reqKey.Equals("GetModel"))
this.GetModel();
else if (reqKey.Equals("GetListPage"))
this.GetListPage();
else
ResponseObjectAsJson(this.Response, MyAjaxResult.ReqKeyInvalid);
}
public void GetActions()
{
string actions = "";
try
{
if (HasRight("00.80.02.06.01")) actions += "addRecord,";
if (HasRight("00.80.02.06.02")) actions += "deleteRecord,";
if (HasRight("00.80.02.06.03")) actions += "updateRecord,";
if (actions.EndsWith(",")) actions = actions.Substring(0, actions.Length - 1);
}
catch (Exception ex)
{
ResponseObjectAsJson(this.Response, new MyAjaxResult(-1, ex.Message));
return;
}
MyAjaxResult<string> ret = new MyAjaxResult<string>(1, "操作成功!", actions);
ResponseObjectAsJson(this.Response, ret);
}
public void Add()
{
if (!ValidateRight("00.80.02.06.01"))
return;
Folk model = new Folk(Request.Params["Code"], Request.Params["Title"],
Request.Params["Description"]);
if (string.IsNullOrEmpty(model.Code) || string.IsNullOrEmpty(model.Title))
{
ResponseObjectAsJson(this.Response, MyAjaxResult.ReqDataError);
return;
}
try { FolkManager.Add(theUser, model); }
catch (Exception ex)
{
ResponseObjectAsJson(this.Response, new MyAjaxResult(-1, ex.Message));
return;
}
ResponseObjectAsJson(this.Response, MyAjaxResult.Success);
}
public void Delete()
{
if (!ValidateRight("00.80.02.06.02"))
return;
...
}
public void Update()
{
if (!ValidateRight("00.80.02.06.03"))
return;
...
}
public void GetModel() { ... }
public void GetListPage() { ... }
}
其中:
(1)在页面Load事件中,首先使用ValidateUser()检查用户帐号的有效性。我的用户帐号信息是存在Session中的,会话失效则向客户端反馈相应消息。具体代码在PageBase父类里面:
public MyUser theUser
{
get
{
MyUser currentUser = Session["theUser"] as MyUser;
if (currentUser == null)
{
ResponseObjectAsJson(this.Response, MyAjaxResult.SessionExpired);
return null;
}
else
return currentUser;
}
}
public bool ValidateUser()
{
return theUser != null;
}
当然,帐号信息存储与验证可以使用更加高级的方法,我自己还是喜欢这种最简单的方法。
(2)页面Load事件中验证帐号有效性之后,ValidateRight("00.80.02.06")验证前端用户是否具有查看本页面相关数据的许可,无效则也是向前端反馈相应消息。具体代码也在PageBase父类里面:
public IList<Right> theUserRights
{
get
{
IList<Right> ret = Session["theUserRights"] as IList<Right>;
if (ret == null)
{
ret = MyUserManager.GetRights(theUser.Id);
Session["theUserRights"] = ret;
}
return ret;
}
}
public bool HasRight(string rightCode)
{
foreach (Right r in theUserRights)
{
if (r.Code.Equals(rightCode.Trim()))
return true;
}
return false;
}
public bool ValidateRight(string rightCode)
{
if (!HasRight(rightCode))
{
ResponseObjectAsJson(this.Response, MyAjaxResult.DenyByRights);
return false;
}
return true;
}
看明白了吗?MyUserManager.GetRights(theUser.Id)是根据当前用户ID查询其所有权限许可,是业务逻辑层的一个方法。
public IList<Right> theUserRights在会话中缓存了当前用户的所有操作许可。
public bool HasRight(string rightCode)方法判断当前用户是否具有某项操作权限。
public bool ValidateRight(string rightCode)则是验证当前用户是否具有某项操作权限,无效则直接向客户端反馈相应消息。
这些功能其实还可以使用XML文件来修改,将更加方便配置,以后有时间再改吧。
(3)页面Load事件中第3段代码,则是调度客户端的请求,传递给相应的服务方法。无效请求会直接反馈相应消息。
(4)页面Load事件之后是GetActions()方法,这个方法根据当前用户的操作权限,向客户端发出控制各项命令按钮显示/隐藏的标志。具体流程是:UI前端加载时,会使用AJAX向UI后端请求GetActions()方法;GetActions()方法反馈回各项命令按钮显示/隐藏标志;UI前端的js脚本根据接收到的标志来控制各命令按钮。
(5)接下来是各项具体的业务操作方法,以Add()方法为例:每个业务操作方法里首先会再次验证当前用户是否有调用此方法的权限,无效则又会直接反馈回客户端相应消息,通过才执行真正的业务处理。这种机制防止了恶意破解UI前端js代码对命令按钮的隐藏控制,保证权限管理的安全性。
桌面版的许可验证与WebMIS版稍有不同,一般将界面项控制与许可验证写在UI层就可以了,示例代码如下:
public partial class FolkManagerForm : XtraForm, IDataForm
{
private FolkManagerForm(Form mainForm, MyUser theUser) { ... }
public static void ShowMeChild(Form mainForm, MyUser theUser)
{
...
if (frm.RefreshData() && frm.DoRight())
frm.Show();
}
#region IDataForm 成员
...
public bool DoRight()
{
try
{
bool hasRight = false;
hasRight = MyUserManager.HasRight(this._CurrentUser.Id, "00800601");
this.tbtnNew.Visibility = hasRight ? BarItemVisibility.Always : BarItemVisibility.Never;
hasRight = MyUserManager.HasRight(this._CurrentUser.Id, "00800602");
this.tbtnDelete.Visibility = hasRight ? BarItemVisibility.Always : BarItemVisibility.Never;
hasRight = MyUserManager.HasRight(this._CurrentUser.Id, "00800603");
this.tbtnModify.Visibility = hasRight ? BarItemVisibility.Always : BarItemVisibility.Never;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
return true;
}
...
#endregion
...
}
在窗口加载时会调用DoRight()方法来根据操作许可控制命令按钮的隐藏显示。
以上就是我MIS/WebMIS架构中的多层次权限管理体系,欢迎大家拍砖!
再多扯点儿,象上面说的管理软件是基于数据共享的思想开发的,所以只有业务操作与业务实体两种基本目标。而基于工作流思想的开发,还需要增加流程环节为管理目标,以后再探讨哦。