这是松结对编程的第20篇(专栏目录)。
本文探讨Link访问权限的最佳实现方法,力求外观干净且封装良好。
这些代码将位于L型代码结构(参见松结对编程系列中的定义)的下层,调用者无需理解其原理。
顺便说一下,我们做的是管理信息系统,和互联网社区软件的一个区别是很多链接都是需要特定权限才能访问的,有些权限也不是非常直观能猜到应该具备何种条件才能访问,另外一些权限还会经常改动。因此一个容易使用、容易维护、不容易出错的权限机制尤为重要。
无权访问时该显示什么
在说实现方法之前,先说说如果链接访问条件不满足,应该显示什么。
实践发现显示文字不好,因为文字(这里应该是一段解释为何不能访问的文字)肯定比链接长,显示空间不好。
什么都不显示如何?也不太好,用户可能会误以为没有这样一个功能,或链接不在这个页面上而去其他页面寻找。
最后现在是显示一个灰色的链接,悬停时解释需要什么条件才能访问这个链接(这样用户如果想操作它但却没有权限,就会知道该怎么办)。
显示方法
方法1:散装代码
一般而言,如果要限制一个Link的访问权限,都是这样的:
if (condiction) { @link } else { @text //灰色代码,或干脆什么都不显示。 }
这样的最大坏处是,如果一个页面上有很多链接(比如导航页面),那么遍地都是if-else,眼花缭乱。
而且一旦权限修改了,就要到处修改所有可能引用过的地方。
方法2:封装Link
@MFCUI.ImageLink("只读树", "/ProductManagement/StoryTrees/IndexTree?" + Request.QueryString, displayAsBoldTextOnPage: this, title: "只读故事树,速度较快") @MFCUI.ImageLink("操作树", "/ProductManagement/StoryTrees/OperateTree?" + Request.QueryString, displayAsBoldTextOnPage: this, title: "可拖拽和执行所有操作,速度较慢", displayAsLink: product.IsProductManager(), grayTextTitle: "需要是此产品的产品经理才能操作。") @MFCUI.ImageLink("详情树", "/ProductManagement/StoryTrees/DetailsTree?" + Request.QueryString, displayAsBoldTextOnPage: this, title: "适合快速查看所有故事的详情") @MFCUI.ImageLink("编辑树", "/ProductManagement/StoryTrees/EditTree?" + Request.QueryString, displayAsBoldTextOnPage: this, title: "适合连续编辑多个故事的数据", displayAsLink: product.IsProductManager(), grayTextTitle: "需要是此产品的产品经理才能操作。")这个方法解决了前面提到的if-else满天飞的问题,但是要解决缺陷更改造成的代码改动还不行。
此外,一些解释性的语言如“需要时此产品的产品经理才能操作”也存在多处文字的统一问题;甚至即使在同一地方,如果displayAsLink修改了条件,而grayTextTitle没有相应修改,会造成用户的错误理解。
方法3:封装模型
@product.IndexTreeLink(this) @product.OperateTreeLink(this) @product.DetailsTreeLink(this) @product.EditTreeLink(this)Product类中代码如下:
public partial class Product : Item { public MvcHtmlString IndexTreeLink(WebViewPage page) { var queryString = page.Request.QueryString.ToString().Contains("rootID") ? page.Request.QueryString.ToString() : "rootID=" + ID; return MFCUI.ImageLink("只读故事树", "/ProductManagement/StoryTrees/IndexTree?" + queryString, displayAsBoldTextOnPage: page, title: "只读故事树,速度较快"); } public MvcHtmlString OperateTreeLink(WebViewPage page) { var queryString = page.Request.QueryString.ToString().Contains("rootID") ? page.Request.QueryString.ToString() : "rootID=" + ID; return MFCUI.ImageLink("操作故事树", "/ProductManagement/StoryTrees/OperateTree?" + queryString, displayAsBoldTextOnPage: page, title: "可拖拽和执行所有操作,速度较慢", displayAsLink: IsProductManager(), grayTextTitle: "需要是此产品的产品经理才能操作。"); } public MvcHtmlString DetailsTreeLink(WebViewPage page) { var queryString = page.Request.QueryString.ToString().Contains("rootID") ? page.Request.QueryString.ToString() : "rootID=" + ID; return MFCUI.ImageLink("详情故事树", "/ProductManagement/StoryTrees/DetailsTree?" + queryString, displayAsBoldTextOnPage: page, title: "适合快速查看所有故事的详情"); } public MvcHtmlString EditTreeLink(WebViewPage page) { var queryString = page.Request.QueryString.ToString().Contains("rootID") ? page.Request.QueryString.ToString() : "rootID=" + ID; return MFCUI.ImageLink("编辑故事树", "/ProductManagement/StoryTrees/EditTree?" + queryString, displayAsBoldTextOnPage: page, title: "适合连续编辑多个故事的数据", displayAsLink: IsProductManager(), grayTextTitle: "需要是此产品的产品经理才能操作。"); } }
刚才查找替换了一下,每个链接都出现过6次。通过改写后,原来有很多可能导致不一致的参数的调用都变成一个只传输(this)的参数了,未来维护会简单地多。
下面是一个具体的实现效果:
方法4:重写MVC的Authorize属性
[Authorize(Roles = "ProductManager")] public ActionResult OperateTree(int rootID, string whats, string whattypes) { ... var root = _repository.ReadItemAt(rootID); var product = root is Product ? root as Product : root.YoungestAncesstor<Product>(); if (!product.IsProductManager()) return Content("只有此产品的产品经理才可以操作。"); return OperateTreeView(...); }可惜有这么几个限制:
这些虽然听起来很多,但是还好之前为了存储问题,产品、团队、用户故事、缺陷……这些都是从基类UDCable(User Defined Column-able,“可被用户自定义字段的”)派生的,而刚才说的一大堆角色,都是一个个UDCType(User Defined Column Type,“用户自定义字段类型”),这样其实所有刚才说的判断,都是一种,就是问某人的Id是否等于某个UDCable的某个UDCType字段数值。
[Authorize(Roles = "ProductManager", UDCRoles="rootID, ProductManager")] public ActionResult EditTree(int rootID, string whats, string whattypes) { ... return OperateTreeView(...); }
UDCRoles="rootID, ProductManager"是说,用url的rootID数值来找UDCable,然后判断其"ProductManager"是否等于当前用户。
用这个属性后Action中可以减少3行(一共才5行,所以3行很多了),而整个代码中有很多这样的三行代码,估计现在有30处左右,都很容易写错造成漏洞。
剩下一个问题,@MFCUI.ImageLink怎么知道这些Action的访问权限呢?
现在的想法是在属性代码中用"Area/Controller/Action"作为Key,权限设置(就是“rootID, ProductManager”)作为值做一个静态缓存,ImageLink会根据自己传入的Url解析出“Area/Controller/Action”并去查找缓存的值,如果找到就根据权限进行判断是否显示为链接)。这样未来只要在Action前面写好属性,所有指向它的链接都会自动判断。
因为之前已经有很多可用的代码了(比如解析A/C/T的代码),所以估计两者加起来大约有10~15行代码就能实现。
估计这些代码两个月后才会排到足够优先级,写好了我共享一下。
总结
所有代码结构中的第一块积木是最难的,如果不是我们原来有一些复用了,上面这个访问权限问题解决起来可能需要上百行代码,很容易将就一下就硬编码过去了。
如果我们当年所有View里边的链接都是用<a></a>硬编码的,或用MVC中自带的Hmtl.Link()写的,那么我们也没有勇气和动力来用“这么复杂”的方式来解决这个访问权限问题了。但若干时间后,一旦访问权限变化了,肯定会因为各地的硬编码而出现无数问题,那时候就真的乱了。
UDCable和ImageLink这些都是接近一年半年前产生的,那时候完全没想到会与现在要做的权限控制相关。只能说,如果做对了事情,回报是迟早的。
所以应该随时随地把可复用的东西总结起来,这样反而不觉得累,才能不断在原来的基础上前进。