WPF應用中一種比較完美的許可權控制設計方式

assassinx發表於2020-11-13

如題近段時間 需要在wpf應用中設計一個許可權控制 , 簡而言之的說 你懂的 對於IT人員來說都知道的 常見的軟體功能 首先要有使用者 使用者,然後使用者屬於哪個角色 ,然後各個角色都有自己的可供操作的一堆功能,當然還有其它的複雜的控制方式 我並不想弄 只搞這種比較通用的。

首先是許可權管理介面 以及資料操作 一堆功能的實現 比如 新增角色 設定許可權,這個其實沒啥好說的 就像你做傳統的winform或者web一樣 搞介面 訪問資料庫 做功能, 就是按部就班 。好吧 看下我用到的業務資料處理方法吧:

 1 public class UserLogic
 2 {
 3     internal UserInfo GetUserById(int id)
 4     {        }
 5 
 6     internal void AddOrSaveUser(UserInfo user)
 7     {        }
 8     internal List<UserInfo> GetDBUsers()
 9     {        }
10 
11     internal List<RoleInfo> GetRoles()
12     {        }
13 
14     internal List<AuthorizationInfo> GetAllAuths()
15     {        }
16 
17     internal RoleInfo GetRoleByID(int id)
18     {        }
19 
20     internal List<AuthorizationInfo> GetAuthsByRoleID(int rid)
21     {        }
22 
23     internal void SetAuthsByRoleID(int rid,List<EAuthorizationItem> auths)
24     {        }
25 
26     internal int AddOrSaveRole(RoleInfo ro)
27     {        }
28 
29     internal void DelRole(int id)
30     {        }
31 
32     internal RoleInfo GetRoleByName(string name)
33     {        }
34 }

刪除角色:

 1 internal void DelRole(int id)
 2 {
 3     using (MyContext db = new MyContext())
 4     {
 5         var existR = db.roles.FirstOrDefault(r => r.ID == id);
 6         db.roles.Remove(existR);
 7 
 8         var auths = db.auths.Where(r => r.RoleID == id).ToList();
 9         db.auths.RemoveRange(auths);
10 
11         //已經用到了此角色 的使用者 改為預設角色
12         var users = db.users.Where(r => r.RoleID == id).ToList();
13         var defRole = db.roles.FirstOrDefault(r => r.Name == nameof(RoleNameDefine.User));
14         for (int i = 0; i < users.Count; i++)
15         {
16             users[i].RoleID = defRole.ID;
17         }                               
18 
19         db.SaveChanges();
20     }
21 }

設定許可權:

 1 internal void SetAuthsByRoleID(int rid,List<EAuthorizationItem> auths)
 2 {
 3     using (MyContext db = new MyContext())
 4     {
 5         var existAuths= db.auths.Where(r=>r.RoleID==rid).ToList();
 6         db.auths.RemoveRange(existAuths);
 7 
 8         List<AuthorizationInfo> besave = new List<AuthorizationInfo>();
 9         for (int i = 0; i < auths.Count; i++)
10         {
11             besave.Add(new AuthorizationInfo()
12             {
13                 RoleID = rid,
14                 AuthE = auths[i]
15             });
16         }
17 
18         db.auths.AddRange(besave);
19         db.SaveChanges();
20     }
21 }

關於最終介面的樣子嘛,也沒美化就這樣:

 

然後就是 特定操作的 許可權  ,“許可權” 這個東西我們以什麼方式來描述  ,說白了就是 固定的字串 比如"Add_xxInfo" "Del_xxInfo" ,再怎麼我們的系統還是比較小 屬於比較保守的 ,說白了就那麼幾個功能。不可能敞著 ,我們還是得以固定程式碼的方式定義這些描述  要不字串 要不列舉。由於我自己借鑑了一種方式 可以比較方便的 完成 列舉資料 從程式碼 到資料庫  以及介面顯示 的交換。最終經過反覆斟酌 我們還是選用了列舉: 

 1 public enum EAuthorizationItem
 2 {
 3     [EnumDescription("印表機或自助機資訊更新")]
 4     PrinterOrTerminalUpdate,
 5     [EnumDescription("印表機或自助機刪除")]
 6     PrinterOrTerminalDel,
 7     [EnumDescription("資料介面管理")]
 8     DataSourceMgt,
 9     [EnumDescription("使用者資訊刪除")]
10     UserDel,
11     [EnumDescription("使用者資訊更新")]
12     UserUpdate,
13     [EnumDescription("許可權管理")]
14     RoleMgt
15 }

接下來的思路也是順水推舟:
登入的時候 就能夠確定所擁有的所有許可權 生成功能標識陣列,在登入結果裡返回到客戶端 ,客戶端功能介面處 傳入功能標識引數 通過一個統一的入口 與登入資訊裡的功能標識 陣列 匹配 進而確定介面此部分功能是否啟用。web那一套都熟悉 我們都知道怎麼做,說起來簡單 其實是琢磨了好久的,這是wpf。 首先要形成統一入口,不能到處編寫許可權判斷程式碼 否則就違揹我們的初衷了 哪怕複製貼上同樣的也不行, 我是用的mvvm方式 的, 如果我要做的話直接在viewModel裡面 編寫許可權判斷程式碼 很簡單 毫無難度。然後另一個 可以繫結command 他可以通過canexecute 來影響介面是否可用 ,也是不錯的方式 ,但是我由於一些特殊的原因 不能使用此方式。

說道此處最顯而易見的都知道了 IsEnable=”{binding}“ ,特別說一下 通過此次的使用 讓我對wpf的binding 有了一個更清晰的理解,binding 幾大要素 ,source 資料來源 沒有指定source的時候預設以當前dataContext 一級一級的向上找 ,這也是我們使用mvvm的基本支撐。然後 還有 path ,繫結方向 和 converter不用多說了。為了這玩意兒我們也是煞費苦心。首先確定的是binding 必須要用binding ,我們要用的就是它自動化計算的功能 ,什麼時候自動化計算 稍後再說。為了繫結功能標識傳入引數 ,於是我們首先想到從 source入手 讓其定位到一個static的東西 好處有二 ,首先static的 在一個地方統一編寫就行了統一引用 維護方便不易出錯,第二個有編輯提示 也就是.能.出東西來 比你硬編碼字串 不只是好點吧點。binding不都是動態值嗎 我們此處卻都是一個固定值 這感覺怪怪的,不要怪。我們上面說了利用他的動態計算功能 ,此處可以說明了 那就是converter ,通過熟讀wpf 繫結原理過程 觀察它走的路線你就會知道 最終是通過converter暴露的,對我們就在此處進行截獲 。對功能標識引數與當前使用者進行匹配 進而決定介面是否可用。說實話前面的你可以認為是傳進來的已知引數。

好 看看我們的繫結

<MenuItem Header="編輯所選項" Name="me_Update" Click="me_Update_Click" IsEnabled="{Binding  UserUpdate ,Source={x:Static cd:AuthorizationItemDefine.Default},Converter={StaticResource auCOnverter}  }"></MenuItem>

來複習下wpf的繫結原理 source是讓其定位到一個靜態變數 而不是當前自動分配的datacontext, 然後繫結到裡面的RoleMgt屬性。Source={x:Static 這個是wpf設計很nice的地方 ,我們通過一個static的靜態變數 但是類是new出來的 也就是單例模式,到處繫結 。

靜態繫結定義:

 1 public class AuthorizationItemDefine : PropertyChangedBase
 2 {
 3     public static AuthorizationItemDefine Default { get { return m_Default; } }
 4     private static AuthorizationItemDefine m_Default = new AuthorizationItemDefine();
 5     AuthorizationItemDefine()
 6     {
 7     }
 8 
 9     public void RiseProperty()
10     {
11         OnPropertyChanged(() => PrinterOrTerminalUpdate);
12         OnPropertyChanged(() => PrinterOrTerminalDel);
13         OnPropertyChanged(() => DataSourceMgt);
14         OnPropertyChanged(() => UserDel);
15         OnPropertyChanged(() => UserUpdate);
16         OnPropertyChanged(() => RoleMgt);
17     }
18     public EAuthorizationItem PrinterOrTerminalUpdate
19     {
20         get
21         { return EAuthorizationItem.PrinterOrTerminalUpdate; }
22     }
23     public EAuthorizationItem PrinterOrTerminalDel
24     {
25         get
26         { return EAuthorizationItem.PrinterOrTerminalDel; }
27     }
28     public EAuthorizationItem DataSourceMgt
29     {
30         get
31         { return EAuthorizationItem.DataSourceMgt; }
32     }
33 
34     public EAuthorizationItem UserDel
35     {
36         get
37         { return EAuthorizationItem.UserDel; }
38     }
39     public EAuthorizationItem UserUpdate
40     {
41         get
42         { return EAuthorizationItem.UserUpdate; }
43     }
44     public EAuthorizationItem RoleMgt
45     {
46         get
47         { return EAuthorizationItem.RoleMgt; }
48     }
49 }

頁面需要引入,以及定義converter:

1 xmlns:cd="clr-namespace:Common.Define;assembly=Common"
2 xmlns:cc="clr-namespace:AutoPrintClient"
3 <UserControl.Resources>
4     <cc:AuthConverter x:Key="auCOnverter"/>
5 </UserControl.Resources>

轉換器很簡單,就是看登入資訊裡有無對應的功能標識:

 1 class AuthConverter : IValueConverter
 2 {
 3     public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
 4     {
 5         if (value == null)
 6             return false;
 7         if (Runtime.Default.loginInfo == null || Runtime.Default.loginInfo.Auths == null)
 8             return false;
 9         string machAu = value.ToString();
10         if (Runtime.Default.loginInfo.Auths.Contains(machAu))
11         {
12             return true;
13         }
14         else
15         {
16             return false;
17         }
18     }
19 
20     public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
21     {
22         throw new NotImplementedException();
23     }
24 }

我們在使用者登入之處就已經把功能標識陣列 附在返回的登入資訊裡(Auths):

 1 //使用者登入
 2 var user = db.users.FirstOrDefault(r => r.LoginName == loginName && r.Password == password);
 3 if (user != null)
 4 {
 5     RoleInfo ptrU = db.roles.FirstOrDefault(r => r.ID == user.RoleID);
 6     log = new LoginInfo();
 7     log.LoginName = loginName;
 8     log.Password = password;
 9     log.LoginAt = DateTime.Now;
10     if (ptrU != null)
11     {
12         log.RoleID = ptrU.ID;
13         log.RoleName = ptrU.Name;
14         log.RoleDescription = ptrU.Description;
15 
16         var auths= db.auths.Where(r => r.RoleID == log.RoleID).ToList();
17         for (int i = 0; i < auths.Count; i++)
18         {
19             log.Auths.Add(auths[i].AuthE.ToString());
20         }
21     }
22 }
23 else
24 {
25     errorMsg = "資料庫未找到對應使用者記錄";
26 }

問題又來了 ,何時進行更新

測試發現介面一直沒有更新置灰 ,最後我們跟蹤發現原來是usercontrol一出現的時候 就通過converter完成了binding ,而這時候其實我們還沒有登入,而converter又是一個很特殊的玩意兒。我們是無法程式碼手動去觸發他的,通過複習binding過程 推斷 還是隻得從值本身出發 , 這樣converter就會觸發了,去更新這個"其實是一直不變"的值 是不是一種很詭異的感覺 哈哈哈哈哈哈。。通過以前的知識我們知道 onPropertyChange 會觸發依賴屬性更新介面 。好咧 那就是他了 我們在前面的程式碼里加上RiseProperty方法 在裡面重新整理所有屬性。其實上面已經是完整形式的程式碼了 ,就是上面貼出來的RiseProperty()方法這裡就不貼了。思路順水推舟 我們接下來做的自然是在 登入時進行 許可權重新整理 各處的介面重新整理,通過與上面的結合 真是神來之筆。

登入重新整理呼叫程式碼:

1 private void Click_login(object sender, RoutedEventArgs e)
2 {
3     if (vm.Login(passwordBox.Password) == true)
4     {
5         dlg_login.Visibility = Visibility.Hidden;
6         this.uct_Onlines.LoadFirstPage();
7         AuthorizationItemDefine.Default.RiseProperty();
8     }
9 }

最終的不同使用者登入效果:

 

 

 

測試發現只需再登入成功後統一重新整理一下就可以了 ,各處都會 按設想的工作 ,完美。乾淨手段解決問題的方式 你會發現  真的 真的 真的 很爽。

 

相關文章