引言
最近自己在做一套權限系統,進展還不錯,在這里我要感謝兩個人對我權限系統UI上的幫助:第一個是@微軟高級php工程師在博客園看到這位牛人怎么扣界面的,非常膜拜啊。原文地址:
大濕教我寫.net通用權限框架(1)之菜單導航篇 但是看了個一知半解,幸好沒多久又出現了一位牛人@wolfy,經過他這兩篇文章:
看過《大濕教我寫.net通用權限框架(1)之菜單導航篇》之后發生的事
看過《大濕教我寫.net通用權限框架(1)之菜單導航篇》之后發生的事(續)——主界面
跟着大師們的步伐,我從登陸界面到主界面,不厭其煩的F12,發現並不像微軟高級php工程師說得那么簡單,有些東西抓過來並不真的就可以用,布局會亂掉,JS也會有很多地方報錯。
所以我對像我一樣的菜鳥同行建議,人家方法指出來了,我們也還是要提高一下自身的修為,要學習一下html、div+css布局、javascript這些東西,不然層次差太遠,人家說稍微高深一點的東西我們就不懂了。
當然自己懂得多一點也可以發現一些牛人們犯的錯誤,牛人畢竟不是聖人。
大半套UI做下來我發現自己頂多只有4分之1的時間是在寫有用的代碼,其它時間基本是在學一些基本的前端知識,當我現在再回過頭去看前面提到的那幾篇博客時收獲要多很多。
特別是微軟高級php工程師的動態拼接html,用的真是太好了。我把那些東西用到自己的開發中發現解決了很多以前解決不掉的問題。
費話少說曬一下我的成果,這個主界面就是完全仿的微軟高級php工程師的(當然微軟高級php工程師看到了別跟我急,這是練練手,肯定不敢拿了商用,這點我懂的)

wolfy已經在他的文章里講了如何實現,有興趣的可以參照博文《大濕教我寫.net通用權限框架(1)之菜單導航篇》 我這里就不多費筆墨了。
我主要說一下帶出菜單項后如何對頁面的權限進行控制
數據庫設計
菜單權限表:
CREATE TABLE [dbo].[BPMS_UserMenu]( [UserMenuId] [varchar](50) NOT NULL, [UserId] [varchar](50) NULL, [MenuId] [varchar](50) NULL, [CreateDate] [datetime] NULL, [CreateUserId] [varchar](50) NULL, [CreateUserName] [varchar](50) NULL, CONSTRAINT [PK_BPMS_USERMENU] PRIMARY KEY NONCLUSTERED ( [UserMenuId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO SET ANSI_PADDING OFF GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'用戶菜單關系主鍵' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenu', @level2type=N'COLUMN',@level2name=N'UserMenuId' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'用戶主鍵' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenu', @level2type=N'COLUMN',@level2name=N'UserId' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'菜單主鍵' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenu', @level2type=N'COLUMN',@level2name=N'MenuId' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'發生時間' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenu', @level2type=N'COLUMN',@level2name=N'CreateDate' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'創建用戶主鍵' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenu', @level2type=N'COLUMN',@level2name=N'CreateUserId' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'創建用戶' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenu', @level2type=N'COLUMN',@level2name=N'CreateUserName' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'用戶菜單關系' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenu' GO ALTER TABLE [dbo].[BPMS_UserMenu] ADD CONSTRAINT [DF_BPMS_UserMenu_CreateDate] DEFAULT (getdate()) FOR [CreateDate] GO
這里很簡單,就是把用戶表跟菜單表的中間表,用戶用哪些菜單的權限都保存在這個表里面
按鈕權限表:
CREATE TABLE [dbo].[BPMS_UserMenuButton]( [UserMenuButtonId] [varchar](50) NOT NULL, [UserId] [varchar](50) NULL, [MenuId] [varchar](50) NULL, [ButtonId] [varchar](50) NULL, [CreateDate] [datetime] NULL, [CreateUserId] [varchar](50) NULL, [CreateUserName] [varchar](50) NULL, CONSTRAINT [PK_BPMS_USERMENUBUTTON] PRIMARY KEY NONCLUSTERED ( [UserMenuButtonId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO SET ANSI_PADDING OFF GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'用戶菜單按鈕關系主 鍵' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenuButton', @level2type=N'COLUMN',@level2name=N'UserMenuButtonId' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'用戶主鍵' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenuButton', @level2type=N'COLUMN',@level2name=N'UserId' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'菜單主鍵' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenuButton', @level2type=N'COLUMN',@level2name=N'MenuId' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'按鈕主鍵' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenuButton', @level2type=N'COLUMN',@level2name=N'ButtonId' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'發生時間' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenuButton', @level2type=N'COLUMN',@level2name=N'CreateDate' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'創建用戶主鍵' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenuButton', @level2type=N'COLUMN',@level2name=N'CreateUserId' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'創建用戶' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenuButton', @level2type=N'COLUMN',@level2name=N'CreateUserName' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'用戶菜單按鈕關系' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'BPMS_UserMenuButton' GO ALTER TABLE [dbo].[BPMS_UserMenuButton] ADD CONSTRAINT [DF_BPMS_UserMenuButton_CreateDate] DEFAULT (getdate()) FOR [CreateDate] GO
現在我處理權限就是通過這種中間表的形式來處理的,給用戶分配角色,然后再給角色分配權限。
權限表中記錄一下角色ID和要進行權限控制對象ID的對應關系。進行權限驗證的時候直接跟這個表來匹配。
這樣的話,按鈕權限、數據權限等等,都可以控制得到。當然肯定還會有其它更好的方法,以后再慢慢來完善。
權限的控制
我的權限控制是用了緩存,用戶登陸的時候根據用戶ID取得用戶的所有角色。
然后取用戶角色權限的合集加載到緩存中,進行權限判斷的時候直接跟緩存匹配。
取角色菜單SQL語句
SELECT M.MenuId, M.Code , M.FullName , M.Img , M.Category, M.Description, M.SortCode, M.ParentId, UM.MenuId AS IsExist FROM BPMS_SysMenu M LEFT JOIN BPMS_UserMenu UM ON M.MenuId = UM.MenuId AND UserId = @UserId ORDER BY M.SortCode
下面這段代碼是把權限讀入緩存
using DotNet.Kernel; using DotNet.Utilities; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; namespace BPMS.Service { /// <summary> /// 使用服務器緩存 - 存儲權限 /// </summary> public class StorePermission { private readonly BPMS_PermissionDAL dal = new BPMS_PermissionDAL(); private static StorePermission item; public static StorePermission Instance { get { if (item == null) { item = new StorePermission(); } return item; } } /// <summary> /// 將【模塊權限】保存在服務器緩存中,提高性能。這樣就不用每次去數據庫讀 /// </summary> /// <param name="UserId">用戶主鍵</param> /// <param name="list"></param> /// <returns></returns> public void SetModulePermission(string UserId, IList list) { CacheHelper.Insert("Module" + UserId, list); } /// <summary> /// 將【操作按鈕權限】保存在服務器緩存中,提高性能。這樣就不用每次去數據庫讀 /// </summary> /// <param name="UserId">用戶主鍵</param> /// <param name="list"></param> /// <returns></returns> public void SetButtonPermission(string UserId, IList list) { CacheHelper.Insert("Button" + UserId, list); } /// <summary> /// 獲取【模塊權限】在服務器緩存中,提高性能。這樣就不用每次去數據庫讀 /// </summary> /// <param name="UserId">用戶主鍵</param> /// <param name="list"></param> /// <returns></returns> public object GetModulePermission(string UserId) { string strKey = "Module" + UserId; if (!CacheHelper.IsExist(strKey)) {this.SetModulePermission(UserId, dal.GetModulePermission(UserId)); } return CacheHelper.GetCache(strKey); } /// <summary> /// 獲取【操作按鈕權限】在服務器緩存中,提高性能。這樣就不用每次去數據庫讀 /// </summary> /// <param name="UserId">用戶主鍵</param> /// <param name="list"></param> /// <returns></returns> public object GetButtonPermission(string UserId) { string strKey = "Button" + UserId; if (!CacheHelper.IsExist(strKey)) { this.SetButtonPermission(UserId, dal.GetButtonPermission(UserId)); } return CacheHelper.GetCache(strKey); } } }
登陸成功后進入主界面,這里做了兩步操作:
1、調用jajax獲取權限,將權限存入緩存。
2、根據角色對應的菜單數據動態拼接html,構造出一個個的<a href,點擊過后就可以在右側的框架頁中打開。
3、這里只會加載出用戶合法分配的菜單項,如果手動輸入頁面地址的話也會經過驗證。有效防止非法訪問。
載入權限及動態拼接菜單代碼:
var AccordionMenuJson = ""; function GetAccordionMenu() { var index = 0; var html = ""; getAjax("Frame.ashx", "action=LoadFirstMenu", function (data) { AccordionMenuJson = eval("(" + data + ")"); $.each(AccordionMenuJson, function (i) { if (AccordionMenuJson[i].ParentId == '9f8ce93a-fc2d-4914-a59c-a6b49494108f') { if (index == 0) { html += "<li><a style=\"border-top: 0px solid #ccc;\"><img src=\"/Themes/Images/32/" + AccordionMenuJson[i].Img + "\">" + AccordionMenuJson[i].FullName + "</a>"; } else { html += "<li><a><img src=\"/Themes/Images/32/" + AccordionMenuJson[i].Img + "\">" + AccordionMenuJson[i].FullName + "</a>"; } html += GetSubmenu(AccordionMenuJson[i].MenuId); html += "</li>"; index++; } }); }) $(".accordion").append(html); }
URL權限驗證:
這段代碼最好寫在頁面基類里面,每個頁面都去繼承它,任何URL申請都會拿去跟緩存中的權限去匹配一下。
非法請求就直接被拒絕。
#region URL權限驗證,加強安全驗證防止未授權匿名不合法的請求 /// <summary> /// URL權限驗證,加強安全驗證防止未授權匿名不合法的請求 /// </summary> public void IsUrlPermission() { bool IsOK = false; //獲取當前訪問頁面地址 string requestPath = RequestHelper.GetScriptName; string[] filterUrl = { "/Frame/HomeIndex.aspx", "/RMBase/SysUser/UpdateUserPwd.aspx" };//過濾特別頁面 for (int i = 0; i < filterUrl.Length; i++) { if (requestPath == filterUrl[i]) { IsOK = true; break; } } if (!IsOK) { string UserId = RequestSession.GetSessionUser().UserId; IList list = (IList)StorePermission.Instance.GetModulePermission(UserId); IList itemNode = IListHelper.IListToList<BPMS_ModulePermission>(list).FindAll(t => t.NavigateUrl == requestPath); if (itemNode.Count == 0) { StringBuilder strHTML = new StringBuilder(); strHTML.Append("<div><script type=\"text/javascript\">alert('很抱歉!您的權限不足,訪問被拒絕!')</script>"); strHTML.Append("</div>"); HttpContext.Current.Response.Write(strHTML.ToString()); HttpContext.Current.Response.End(); } } } #endregion
做到這里我還沒有做前台控制權限的界面,手動從后台去掉當前用戶的幾個菜單權限,果然顯示的菜單就少了:

這里只將實現權限控制的主要代碼貼出來分享一下。說得可能不是很好,大家可以去微軟高級php工程師的文章《大濕教我寫.net通用權限框架(1)之菜單導航篇》中有提供相關鏈接,希望自己動手也嘗試一下。
總結
好東西確實值得好好去學習,以前我也是個單純的伸手黨,東西拿到手沒有好好研究。遇到問題也不知道怎么解決,自已多動動手真的很好,駕馭的感覺真不錯。
在這里我非常感謝微軟高級php工程師和吉日嘎啦,自己第一次做權限框架,很多思想都是參考他們的,如果有想自己造輪子的可以好好地去參考一下他們的博客。這里就不再提供源碼下載了,涉及作者的版權問題。需要的還請購買正版。呵呵。
