这是松结对编程的第20篇(专栏目录)。

本文探讨Link访问权限的最佳实现方法,力求外观干净且封装良好。

这些代码将位于L型代码结构(参见松结对编程系列中的定义)的下层,调用者无需理解其原理。

顺便说一下,我们做的是管理信息系统,和互联网社区软件的一个区别是很多链接都是需要特定权限才能访问的,有些权限也不是非常直观能猜到应该具备何种条件才能访问,另外一些权限还会经常改动。因此一个容易使用、容易维护、不容易出错的权限机制尤为重要。

无权访问时该显示什么

在说实现方法之前,先说说如果链接访问条件不满足,应该显示什么。

实践发现显示文字不好,因为文字(这里应该是一段解释为何不能访问的文字)肯定比链接长,显示空间不好。

什么都不显示如何?也不太好,用户可能会误以为没有这样一个功能,或链接不在这个页面上而去其他页面寻找。

最后现在是显示一个灰色的链接,悬停时解释需要什么条件才能访问这个链接(这样用户如果想操作它但却没有权限,就会知道该怎么办)。

显示方法

方法1:散装代码

一般而言,如果要限制一个Link的访问权限,都是这样的:

if (condiction)
{
@link
}
else
{
@text //灰色代码,或干脆什么都不显示。
}

这样的最大坏处是,如果一个页面上有很多链接(比如导航页面),那么遍地都是if-else,眼花缭乱。

而且一旦权限修改了,就要到处修改所有可能引用过的地方。

方法2:封装Link

下面是我们原来封装的Link,里边有两个参数:
displayAsLink,即何种条件下应该显示为Link,否则将显示为灰色文字。比如product.IsProductManager()是问当前用户(括号内无值时自动采用当前用户)是否是产品经理。是,才显示链接。
grayTextTitle,显示为灰色文字时,以悬停文字解释为何不能访问。
注意还有一个displayAsBoardTextOnPage:this(this是当前Page)是问当前Page是否就是链接所在地,如果是就显示为黑体文字而不在显示连接了。
        @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的写操作权限的,那么如果用product自己来判断,那么代码就会集中在product内部,由专业维护此类的人员来确定权限和解释。
这个是现在为止最佳的选择。
当前View调用处的代码如下:
        @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属性

方法3虽然很好,但是只是隐藏了链接而已,真正要访问,手工输入链接仍然可以。
asp.net 为我们封装了一个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(...);
}

可惜有这么几个限制:

1. 只提供Users(常量的用户名,基本没什么用)和Roles(上例中的)两种。
比如上例中“此产品的产品经理”,就只能编码实现。
2. (似乎)没有地方查看某个Action具体访问权限是什么
也就是说,不能在别处生成指向此Action的链接时,自动根据所限定的Users或Roles来自动决定显示为链接或文字。
重写Authorize可以根据自己的条件来限制访问,唯一的问题是“自己的条件”如果太多,那么会很复杂。
还好,到现在为止火星人中就用了两种限制:
1. 某人是某个角色。
比如SiteUsersController只有Admin才可以访问,这个是站点的用户管理功能。
2. 某人是某物的某个角色。
现在火星人中,“某物”包括产品(的产品经理)、团队(的项目经理、项目助理经理即项目经理不在时应急用的代理人、项目组员)、用户故事(的负责人、当前负责人、创建者)、缺陷(的创建人、当前负责人)……,未来还有部门(的经理、部门助理经理、部门人员)……这些。

这些虽然听起来很多,但是还好之前为了存储问题,产品、团队、用户故事、缺陷……这些都是从基类UDCable(User Defined Column-able,“可被用户自定义字段的”)派生的,而刚才说的一大堆角色,都是一个个UDCType(User Defined Column Type,“用户自定义字段类型”),这样其实所有刚才说的判断,都是一种,就是问某人的Id是否等于某个UDCable的某个UDCType字段数值。
现在还没时间写这个Authroize(主要是不会写,呵呵),估计写好后使用方法如下:
        [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这些都是接近一年半年前产生的,那时候完全没想到会与现在要做的权限控制相关。只能说,如果做对了事情,回报是迟早的。

所以应该随时随地把可复用的东西总结起来,这样反而不觉得累,才能不断在原来的基础上前进。

L型代码结构案例:Link访问权限(上)的更多相关文章

  1. 敏捷开发松结对编程系列:L型代码结构案例StatusFiltersDropdownList(中)

    这是松结对编程的第22篇(专栏目录). 接前文 业务代码 比较长,基本上就是看被注释隔开的三大段,先显示状态群筛选链接,然后是单个状态筛选,然后是显示下拉框的当前选中项,最后显示下拉框. public ...

  2. 3.GO-项目结构、包访问权限、闭包和值传递引用传递

    3.1.goland中项目结构 (1)在goland中创建标准Go项目 (2)goland配置 创建项目Learn-Go file-settings-go-GOPATH-添加 在项目目录下创建src目 ...

  3. go 基础 结构体 接口 访问权限

    package School type SchoolModel struct { Name string Address string StudentCount int Is985 bool } ty ...

  4. 初读"Thinking in Java"读书笔记之第六章 --- 访问权限控制

    包:库单元 包内包含有一组类,他们在单一的名字空间下被组织在一起. 通过import ***.***.*可以将某个包下的所有类导入到当前文件中. 每个Java源文件最多只能有一个public类,且名称 ...

  5. Linux中ls -l(ll)返回结果中的文件访问权限-rw-r--rw-

    linux文件访问权限(像rw-r--rw-是什么意思)   Linux的文件访问权限分为 读.写.执行三种 r:可读(4) w:可写(2)对目录来说则可新建文件 x:可执行(1)对目录来说则可进入该 ...

  6. SYS_数据访问权限Operation Unit和Ledger的访问设定(案例)

    2014-06-01 Created By BaoXinjian

  7. [apue] linux 文件访问权限那些事儿

    前言 说到 linux 上的文件权限,其实我们在说两个实体,一是文件,二是进程.一个进程能不能访问一个文件,其实由三部分内容决定: 文件的所有者.所在的组: 文件对所有者.组用户.其它用户设置的权限访 ...

  8. CentOS 6.5系统上安装SVN服务器端的方法及目录访问权限配置(转总结)

    SVN其实就是Subversion,分为服务器端和客户端.之前在网上搜了很多方法,都有各种问题,经过自己搜集整理以及实际尝试,总算有个比较靠谱的方法.本文主要介绍CentOS 6.5系统上安装SVN服 ...

  9. Unix/Linux文件类型及访问权限

    在Linux系统中,有7种文件类型. 普通文件 (regular file) 目录文件 (directory) 链接文件 (symbolic link) 管道文件 (FIFO) 套接字文件 (sock ...

随机推荐

  1. redis在spring中的配置及java代码实现

    1.建一个redis.properties属性文件 # Redis Setting redis.addr = 127.0.0.1 redis.port = 6379 redis.auth = mast ...

  2. UVA1471( LIS变形)

    这是LIS的变形,题意是求一个序列中去掉某个连续的序列后,能得到的最长连续递增序列的长度. 用DP的解法是:吧这个序列用数组a来记录,再分别用两个数组f记录以i结尾的最长连续递增序列的长度,g[i]记 ...

  3. JavaScript 运动框架 Step by step

    http://blog.csdn.net/rsj217/article/details/7986905 关于offsetLeft:http://www.cnblogs.com/JackJiang/ar ...

  4. 0603 python 基础02

    作业1:ANSI和utf8的区别? ASCII是用来表示英文字符的一种编码规范,每个ASCII字符占用1个字节(8bits). 可以表示的最大字符数是256,一般只用前128个(最高位为0),其中包括 ...

  5. javascript closure 闭包 事件绑定

    先来一个基本的例子 <!-- 实现一段脚本,使得点击对应链接alert出相应的编号 --> <meta http-equiv="Content-Type" con ...

  6. C++ 面向对象学习1

    #include "stdafx.h" #include <iostream> //不要遗漏 否则不能使用cout using namespace std; class ...

  7. Win32 SecuritySetting

    http://flylib.com/books/en/2.21.1.207/1/ http://blogs.technet.com/b/heyscriptingguy/archive/2011/11/ ...

  8. 关于 OnCloseQuery: 顺序、不能关机等(所有的windows的广播消息都是逐窗口传递的)——如果一个窗体的OnCloseQuery事件中如果写了代码那么WM_QUERYENDSESSION消息就传不过去了msg.result会返回0,关机事件也就停止了

    系统关闭窗体的事件顺序为: OnCloseQuery ----> OnClose ----> OnDestroy 下面的代码说明问题: unit Unit3; interface uses ...

  9. PHP第一章学习——了解PHP(上)

    计划开启PHP学习教程,情况如下: 1.采用教程35章48个视频文件 2.时间4月29日-5月6日 共计8天 3.具体划分每天学习章节数不少于5个,预留5-6号时间为五一假期出玩情况 4.要求认真学习 ...

  10. http://download.qt-project.org/archive/qt/4.5/qt-all-opensource-src-4.5.2.tar.bz2

    Index of /archive/qt/4.5 http://download.qt-project.org/archive/qt/4.5/qt-all-opensource-src-4.5.2.t ...