前天已发过文章分享了刚完成的一个主数据系统,受到了不少朋友的关注,这篇文章主要是对主数据权限设计方案的讲解,希望对大家有所帮助。源码下载与运行说明请查看
权限管理一般为分授权、验权两大块,另外还有验权测试,这是在系统测试阶段要完成的工作。这里重点要讲的是授权,验权会讲一部分。
一、主要数据表设计
这是权限分组表,设计它是为了在管理权限时更加清晰,没其它特别的意义。
这是权限项表,这个表是重点,其中:
Code是对应系统中的权限码(例如删除用户:delete_user),最终验权的时候是权限这个权限码来获取权限值的。
DisplayStyle是权限项的显示样式,除了CheckBox是简单true/false权限外,TextBox、DropDownList、TreeView这三种都是自己定义数据型权限,也是难点,更是本设计方案的特色,当然还可以扩展其它类型的权限。
JsonDataUrl是支持远程权限值的初始化,JsonDataConst是支持静态权限值的初始化,Json数据格式如下:
下面看看添加权限项的界面:
然后看看授权的界面:
这是角色表,每个系统都有自己的多个角色,每个角色都有自己的权限,其中PermissionJsonData就是用于保存权限的。
这是用户个人的永久权限表,其中PermissionJsonData就是用于保存权限的。
这是用户个人的临时权限表,其中PermissionJsonData就是用于保存权限的,BeginDate和EndDate保存权限的有效日期。
这个表是用于保存用户与角色的关系的,一个用户可以拥有一个或多个角色。
二、受权的代码实现
首先,我们需要按需求来定义合理的数据模型(主要给系统的表示层使用的),其中DbModels里放的是和数据表对应的数据模型,ExtendedModels里放的是特别需求扩展的数据模型,JsonModels里放的是Json数据序列化需要的数据模型。其中PermissionDropDownListOption是为DropDownList类型权限设计的,PermissionTreeViewNode是为TreeView类型权限设计的,代码如下:
[DataContract] [Serializable] public class PermissionDropDownListOption { [DataMember] public string text { get; set; } [DataMember] public string value { get; set; } [DataMember] public bool selected { get; set; } public PermissionDropDownListOption() : this(string.Empty, string.Empty, false) { } public PermissionDropDownListOption(string text, string value) : this(text, value, false) { } public PermissionDropDownListOption(string text, string value, bool selected) { this.text = text; this.value = value; this.selected = selected; } }
[DataContract] [Serializable] public class PermissionTreeViewNode { [DataMember] public string id { get; set; } [DataMember] public bool isParent { get; set; } [DataMember] public string name { get; set; } [DataMember] public bool @checked { get; set; } [DataMember] public string icon { get; set; } [DataMember] public string iconOpen { get; set; } [DataMember] public string iconClose { get; set; } [DataMember] public Listchilds { get; set; } public PermissionTreeViewNode() : this(string.Empty, false, string.Empty) { } public PermissionTreeViewNode(string id, bool isParent, string name) { this.id = id; this.isParent = isParent; this.name = name; this.@checked = false; this.icon = string.Empty; this.iconOpen = string.Empty; this.iconClose = string.Empty; this.childs = new List (); } }
写到这,大家先看一下授权的页面,初始化控件和保存权限都算是一个难点,在显示权限控件方面,其实只要获取相应系统的所有权限项,根据类型来输出html就行了,难的是DropDownList和TreeView控件,下面两个方法是返回它们所需的Json数据:
////// 获取树视图Json数据 /// [Action] public void GetTreeViewPermissionJsonData(Guid systemId, Guid permissionItemId, string[] checkedIds) { PermissionItemModel permissionItem = PermissionItemService.GetById(permissionItemId); // 转为对象 PermissionTreeViewJsonData nodes = PermissionUtils.ParsePermissionControlJsonData(this, systemId, permissionItem); // 初始已选数据 PermissionUtils.InitTreeViewCheckedNodes(nodes, checkedIds); JsonResult(nodes); } /// /// 获取下拉框Json数据 /// [Action] public void GetDropDownListPermissionJsonData(Guid systemId, Guid permissionItemId, string selectedValue) { PermissionItemModel permissionItem = PermissionItemService.GetById(permissionItemId); // 先转为对象 PermissionDropDownListJsonData options = PermissionUtils.ParsePermissionControlJsonData(this, systemId, permissionItem); // 初始已选数据 PermissionUtils.InitDropDownListSelectedOptions(options, selectedValue); JsonResult(options); }
重点工作都交给了PermissionUtils助手类来处理了,请看下面的代码
public static class PermissionUtils { ////// 获取提交的权限 /// /// /// ///public static Dictionary GetPostedPermissionValues(NameValueCollection formValues, List permissionGroupItems) { Dictionary permissionValues = new Dictionary (StringComparer.OrdinalIgnoreCase); foreach (var groupItem in permissionGroupItems) { foreach (var item in groupItem.Items) { string value = formValues.GetString(item.Code); if (!string.IsNullOrEmpty(value)) { if (item.DisplayStyle == PermissionItemDisplayStyle.CheckBox) { value = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)[0]; } PermissionValueModel permissionValue = new PermissionValueModel { Code = item.Code, DisplayName = item.DisplayName, DisplayStyle = item.DisplayStyle, Value = value }; permissionValues.Add(permissionValue.Code, permissionValue); } } } return permissionValues; } public static void ServerModelsToPermissionTreeViewNodes(List nodesToSave, Action alterNode, IEnumerable serversToConvert, Guid serverParentId) { // 过虑并排序 var sortedServers = serversToConvert.Where(s => s.ParentId == serverParentId).OrderBy(s => s.Order); foreach (ServerModel server in sortedServers) { PermissionTreeViewNode node = new PermissionTreeViewNode(server.Id.ToString(), server.IsGroup, server.Name); alterNode(node); nodesToSave.Add(node); if (server.IsGroup) { // 递归,继续初始化孩子节点 List nodes = nodesToSave[nodesToSave.Count - 1].childs; IEnumerable servers = serversToConvert.Except(sortedServers); ServerModelsToPermissionTreeViewNodes(nodes, alterNode, servers, server.Id); } } } #region 权限控件Json数据初始化 /// /// /// ////// /// /// /// public static T ParsePermissionControlJsonData (Mode mode, Guid systemId, PermissionItemModel permissionItem) where T : class, new() { string jsonData = permissionItem.JsonDataConst; // 如果设置了地址 string jsonDataUrl = permissionItem.JsonDataUrl.Trim().Replace("\\", "/"); if (!string.IsNullOrEmpty(jsonDataUrl)) { // 如果是相对地址 if (jsonDataUrl.IndexOf("://") == -1) { // 确保为完整地址 jsonDataUrl = mode.Url.Content("~/" + jsonDataUrl.TrimStart('/'), true); } // 附加参数 systemId、permissionCode string tail = string.Empty; if (jsonDataUrl.IndexOf('#') > -1) { string[] array = jsonDataUrl.Split('#'); jsonDataUrl = array[0]; tail = array[1]; } if (jsonDataUrl.IndexOf('?') == -1) { jsonDataUrl = string.Concat(jsonDataUrl, "?systemId=", systemId, "&permissionCode=", permissionItem.Code); } else { jsonDataUrl = string.Concat(jsonDataUrl.TrimEnd('&'), "&systemId=", systemId, "&permissionCode=", permissionItem.Code); } if (!string.IsNullOrEmpty(tail)) { jsonDataUrl = string.Concat(jsonDataUrl, "#", tail); } // 获取远程Json数据 jsonData = WebRequestHelper.HttpGet(jsonDataUrl, mode.Request.Url.AbsoluteUri, Encoding.UTF8); } // 转为对象 return ObjectSerializer.ConvertFromJsonStringEx (jsonData); } /// /// /// /// /// public static void InitDropDownListSelectedOptions(IEnumerableoptions, string selectedValue) { // 先全不选 foreach (PermissionDropDownListOption option in options) { option.selected = false; } // 初始化已选 if (selectedValue != null) { foreach (PermissionDropDownListOption option in options) { option.selected = (option.value == selectedValue); } } } /// /// /// /// /// public static void InitTreeViewCheckedNodes(IEnumerablenodes, string[] checkedIds) { // 先全不选 CheckTreeViewNodes(nodes, false); if (checkedIds != null && checkedIds.Length > 0) { // 初始化已选节点 InitTreeViewCheckedNodesImpl(nodes, checkedIds); // 如果父节点全选,要确保以后新增孩子节点时也是要是已选状态的 MakeSureTreeViewCheckAllNodes(nodes); } } private static void CheckTreeViewNodes(IEnumerable nodes, bool @checked) { foreach (var node in nodes) { node.@checked = @checked; // 递归 CheckTreeViewNodes(node.childs, @checked); } } private static void InitTreeViewCheckedNodesImpl(IEnumerable nodes, string[] checkedIds) { if (checkedIds != null) { foreach (var node in nodes) { if (checkedIds.Any(id => id == node.id)) { node.@checked = true; } // 递归 InitTreeViewCheckedNodesImpl(node.childs, checkedIds); } } } private static void MakeSureTreeViewCheckAllNodes(IEnumerable nodes) { foreach (var node in nodes) { // 如果父节点全选,要确保以后新增孩子节点时也是要是已选状态的 if (node.isParent && node.@checked) { CheckTreeViewNodes(node.childs, true); } // 递归 MakeSureTreeViewCheckAllNodes(node.childs); } } #endregion }
显示控件处理完了,那保存权限值呢?获取权限值的实现也放在PermissionUtils助手类里了,第一个方法GetPostedPermissionValues就是获取提交的权限值。
三、验权的代码实现
每个用户有多个角色,又有永久和临时权限,那总要处理权限的合理继承吧,本方案是这样处理的:
1. 单选框 与 树视图 类型,在 角色权限、永久权限、临时权限(有效时期内) 中任何已勾选的权限都会一直会继承下去;
2. 文本框 与 下拉框 类型,是按 临时权限 -〉永久权限 -〉角色权限(按排序的顺序) 的顺序,前者的权限为空或没有选择时才会继承后者的权限值;
这些处理都在用户登录成功的时候处理的,并保存权限值到一个字典里,实现代码在MDMS.UserApiModule项目中的UserApiService类:
private DictionaryGetUserPermissions(Guid systemId, Guid userId, List roles) { Dictionary permissions = new Dictionary (StringComparer.OrdinalIgnoreCase); // 先初始化原始权限 List rowPermissionItems = DataProviderManager.Get ().GetAllBySystemId(systemId); foreach (PermissionItemModel item in rowPermissionItems) { permissions.Add(item.Code, new PermissionValueModel { Code = item.Code, DisplayName = item.DisplayName, DisplayStyle = item.DisplayStyle, Value = (item.DisplayStyle == PermissionItemDisplayStyle.CheckBox ? "false" : string.Empty) }); } // 个人权限继承说明: // 1. 单选框 与 树视图 类型,在 角色权限、永久权限、临时权限(有效时期) 中任何已勾选的权限都会一直会继承下去; // 2. 文本框 与 下拉框 类型,是按 临时权限 -〉永久权限 -〉角色权限(按排序的顺序) 的顺序,前者的权限为空或没有选择时才会继承后者的权限值; // 临时权限 UserTempPermissionModel userTempPermission = DataProviderManager.Get ().GetByUserIdAndSystemId(userId, systemId); if (userTempPermission.UserId == userId && userTempPermission.SystemId == systemId && userTempPermission.Enabled && userTempPermission.BeginDate <= DateTime.Now && DateTime.Now <= userTempPermission.EndDate) { // 存在且启用且在有效期内时才合并 UnionPermissions(permissions, userTempPermission.PermissionJsonData); } // 永久权限 UserPermissionModel userPermission = DataProviderManager.Get ().GetByUserIdAndSystemId(userId, systemId); if (userPermission.UserId == userId && userPermission.SystemId == systemId) { // 存在时才合并 UnionPermissions(permissions, userPermission.PermissionJsonData); } // 合并所有角色中的权限 foreach (RoleModel role in roles) { UnionPermissions(permissions, role.PermissionJsonData); } return permissions; } private void UnionPermissions(Dictionary to, string fromJsonData) { if (string.IsNullOrEmpty(fromJsonData)) { return; // 没权限需要处理 } Dictionary temp = ObjectSerializer.ConvertFromJsonStringEx >(fromJsonData); foreach (KeyValuePair pair in temp) { PermissionValueModel value; if (to.TryGetValue(pair.Key, out value)) { // 已存在,更新权限值 switch (value.DisplayStyle) { case PermissionItemDisplayStyle.CheckBox: // 没有权限就继承 if (Utils.StringToBool(value.Value, false) == false) { value.Value = pair.Value.Value; } break; case PermissionItemDisplayStyle.TextBox: case PermissionItemDisplayStyle.DropDownList: // TextBox:如果为空则继承后者 // DropDownList:如果没选择则继承后者 if (value.Value.IsNullOrTrimedEmpty()) { value.Value = pair.Value.Value; } break; case PermissionItemDisplayStyle.TreeView: // 合并不重复的权限 string tempString; HashSet toSave = new HashSet (); HashSet toUnion = new HashSet (); value.Value.Split(',').ForEach(s => { if ((tempString = s.Trim()) != string.Empty) { toSave.Add(tempString); } }); pair.Value.Value.Split(',').ForEach(s => { if ((tempString = s.Trim()) != string.Empty) { toSave.Add(tempString); } }); // save the result unioned value.Value = string.Join(",", toSave.Union(toUnion).ToArray()); break; default: throw new Exception("Unknown PermissionItemDisplayStyle."); } // 保存权限值 to[pair.Key] = value; } else { // 不存在,添加新权限值 to.Add(pair.Key, pair.Value); } } }
要验证权限,那首先要做的就是获取权限值,在MDMS.UserApiModule项目中的ServiceContextBase类中实现所有类型的权限值获取方法,所以子系统只要继承这个类,就可以轻松的进行权限的验证了,请看获取权限值的方法实现:
#region Permission Services private ReadFreeCachePermissionValueCache { get { if (User.Profile.Id == Guid.Empty) { throw new Exception("No logined user."); } HttpContext context = HttpContext.Current; if (context == null) { // 非 Web 系统 return permissionValueCache; } else { ReadFreeCache cache = context.Session["___PermissionValueCache"] as ReadFreeCache ; if (cache == null) { cache = ReadFreeCache .Create(StringComparer.OrdinalIgnoreCase); context.Session["___PermissionValueCache"] = cache; } return cache; } } } /// /// /// /// ///public PermissionValueModel GetPermissionValue(string permissionCode) { permissionCode.ThrowsIfNullOrEmpty("permissionCode"); PermissionValueModel permission; if (User.Permissions.TryGetValue(permissionCode, out permission)) { return permission; } throw new Exception("PermissionCode \"" + permissionCode + "\" does not exist."); } /// /// /// /// ///public bool GetCheckBoxPermissionValue(string permissionCode) { permissionCode.ThrowsIfNullOrEmpty("permissionCode"); PermissionValueModel permission = GetPermissionValue(permissionCode); if (permission.DisplayStyle != PermissionItemDisplayStyle.CheckBox) { throw new Exception("DisplayStyle of permission code \"" + permissionCode + "\" is not CheckBox."); } return Utils.StringToBool(permission.Value, false); } /// /// /// ////// /// public T GetTextBoxPermissionValue (string permissionCode) { return GetTextBoxPermissionValue(permissionCode, default(T)); } /// /// /// ////// /// /// public T GetTextBoxPermissionValue (string permissionCode, T defaultValue) { permissionCode.ThrowsIfNullOrEmpty("permissionCode"); string permissionCodeKey = string.Format("{0}_{1}", User.Profile.UserName, permissionCode); // TextBox权限使用缓存,不必每次处理数据转换 object permissionValue = PermissionValueCache.Get(permissionCodeKey, key => { PermissionValueModel permission = GetPermissionValue(permissionCode); if (permission.DisplayStyle != PermissionItemDisplayStyle.TextBox) { throw new Exception("DisplayStyle of permission code \"" + permissionCode + "\" is not TextBox."); } return Utils.StringTo (permission.Value, defaultValue); }); return (T)permissionValue; } /// /// /// ////// /// public T GetDropDownListPermissionValue (string permissionCode) { return GetDropDownListPermissionValue(permissionCode, default(T)); } /// /// /// ////// /// /// public T GetDropDownListPermissionValue (string permissionCode, T defaultValue) { permissionCode.ThrowsIfNullOrEmpty("permissionCode"); string permissionCodeKey = string.Format("{0}_{1}", User.Profile.UserName, permissionCode); // DropDownList权限使用缓存,不必每次处理数据转换 object permissionValue = PermissionValueCache.Get(permissionCodeKey, key => { PermissionValueModel permission = GetPermissionValue(permissionCode); if (permission.DisplayStyle != PermissionItemDisplayStyle.DropDownList) { throw new Exception("DisplayStyle of permission code \"" + permissionCode + "\" is not DropDownList."); } return Utils.StringTo (permission.Value, defaultValue); }); return (T)permissionValue; } /// /// /// ////// /// public List GetTreeViewPermissionValue (string permissionCode) { return GetTreeViewPermissionValue(permissionCode, (Func
, List >)null); } /// /// /// ////// /// 为防止父节点被选后,后来新增的子节点没有被加进来,请根据具体情况修正权限值列表 /// public List GetTreeViewPermissionValue (string permissionCode, Func
, List > rectifyValues) { permissionCode.ThrowsIfNullOrEmpty("permissionCode"); string permissionCodeKey = string.Format("{0}_{1}_{2}", User.Profile.UserName, permissionCode, (rectifyValues == null ? string.Empty : "rectifyValues")); // TreeView权限使用缓存,不必每次处理数据转换 object permissionValue = PermissionValueCache.Get(permissionCodeKey, key => { PermissionValueModel permission = GetPermissionValue(permissionCode); if (permission.DisplayStyle != PermissionItemDisplayStyle.TreeView) { throw new Exception("DisplayStyle of permission code \"" + permissionCode + "\" is not TreeView."); } List values = null; if (string.IsNullOrEmpty(permission.Value)) { values = new List (); } else { List tempValues = new List (); foreach (string value in permission.Value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { tempValues.Add(Utils.StringTo (value)); } if (rectifyValues == null) { rectifyValues = v => v; } values = rectifyValues(tempValues); } return values; }); return (List )permissionValue; } #endregion
另外值得说的是,GetTreeViewPermissionValue方法有个Func<List<T>, List<T>> rectifyValues参数,它的作用是,如果在授权时勾选了某个根节点,那表示此根节点下的所有权限都是有的,包括以后在该根节点新加的子节点,所以需要额外根据根节点获取所有的子节点,以确保获取的权限是完整的。
在主数据系统设计中,主数据系统也被当成自己的一个子系统来处理了,下面看看主数据页面基类是怎么获取权限的?
#region 验证权限 ////// 选择框权限是否无效 /// /// ///protected bool IsCheckBoxPermissionInvalid(string permissionCode) { return (ServiceContext.Current.GetCheckBoxPermissionValue(permissionCode) == false); } /// /// 树视图权限是否无效 /// /// ///需要验证的自定义数据 ///protected bool IsTreeViewPermissionInvalid(string permissionCode, params Guid[] ids) { permissionCode.ThrowsIfNullOrEmpty("permissionCode"); if (ids == null || ids.Length == 0 || ids[0] == Guid.Empty/* id为空时不用检验 */) { return false; } // 获取权限值 List scopeList = ServiceContext.Current.GetTreeViewPermissionValue (permissionCode, list => { List retList = list; // 如果有已选数据,根据情况进行数据矫正 if (list.Count > 0) { switch (permissionCode.ToLower()) { case "manage_role_scope": if (list.Exists(id => id == Guid.Empty)) { // 全选情况:返回所有ID,包括后来新增的数据 retList = (from p in SystemService.GetAll() select p.Id).ToList(); } break; case "manage_permission_scope": if (list.Exists(id => id == Guid.Empty)) { // 全选情况:返回所有ID,包括后来新增的数据 retList = (from p in SystemService.GetAll() select p.Id).ToList(); } break; default: throw new Exception("Unknown permissionCode: " + permissionCode); } } return retList; }); // 验证权限 bool isInvalid = false; if (scopeList.Count == 0) { // 没有选中数据 isInvalid = true; } else { // 是否有不在范围的? foreach (Guid id in ids) { if (scopeList.Exists(i => i == id) == false) { // 没有权限,不再继续判断 isInvalid = true; break; } } } return isInvalid; } #endregion
有了上面的页面基类,最后的权限验证就变得简洁方便了
写到这就算结束了,设计中的内容比较多,不好处处仔细的解说。本方案中,用json来保存权限使实现变得简单了很多,而且是采用key/value的字典格式,所以子系统在以后需要新增新的权限项或删除权限项,都不会影响到用户的权限(意思是不用解决权限版本问题,随意添加删除权限都可以正常动作),时间问题只能以这种方式来讲解了,需要深入了解本方案设计的朋友请来看,如果本文对你有帮助,请点击推荐以示支持,谢谢。
在线demo:
用户/密码:test1/test1 test2/test2 (注:同一用户在另一浏览器登录,另一用户在session失效后会被逼下线)