本文討論是許可權設計的其中一種方向,有它自己的優缺點,不一定適用於所有系統。
一、Linux檔案許可權
大家都知道,Linux上有三種檔案許可權:
- r:表示讀取,對應的數字為 4;
- w:表示寫入,對應的數字為 2;
- x:表示執行,對應的數字為 1;
當然還有一種是特殊的是:
- -:表示無許可權,對應的數字為0;
通過這四個數字以及他們的組合,就可以表示任意一種許可權:
1+2=3:有執行、寫入許可權,沒有讀取許可權。
1+4=5:有執行、讀取許可權,沒有寫入許可權。
2+4=6:有寫入、讀取許可權,沒有執行許可權。
1+2+4=7:有執行、寫入、讀取許可權。
不知道大家有沒有想過,為什麼許可權的標識要是0/1/2/4這幾個數字呢?為什麼不是0/1/2/3?為什麼不是6/7/8/9?為什麼0/1/2/4組合就可以表示所有許可權呢?
有些人會解釋說,如果用0/1/2/3,那3表示的是3本身呢?還是1+2呢?意義不明確呀。用其他的數字又太大,不方便計算,沒有0124簡單。其實這些都不是真正的原因,Linux檔案許可權設計之所以要用0124,是因為它其實是用二進位制表示許可權的。
二、用二進位制位元位表示許可權
二進位制是什麼想必不用我多說,我們都知道二進位制表示的數字,只有0和1兩種數。那麼我們就想,如果我用0表示沒有許可權,用1表示有許可權,這樣豈不是很簡單?
我繼續用Linux檔案許可權舉例。
用第一個位元位表示是否有執行許可權,第二個位元位表示是否有寫入許可權,第三個位元位表示是否有讀取許可權。每個位元位的0表示沒有許可權,1表示有許可權。(後文所說的第幾位,都是指從右向左數)
那麼我們試一試,如何表示只有寫入許可權,沒有讀取和執行許可權呢?
按照上面的規則,應該是0010對吧,將這個二進位制數字轉成十進位制數字,剛好是2。以此類推,就有了0/1/2/4這四個數字。
分別表示,無許可權(0000)、執行(0001)、寫入(0010)、讀取(0100)
。
再驗證一下,如何表示有執行、讀取許可權,沒有寫入許可權呢?應該是0101,將這個二進位制數字轉成十進位制數字,剛好是5,符合我們上面說的。
所以,只要我們提前約定好每個位元位代表的是什麼許可權,我們就可以通過一個二進位制數字,表示大量的許可權及其自由組合,而且非常的節省儲存空間,1個位元組,我們就可以儲存8種許可權。在Java語言中的int型別有4個位元組,一個int值,就可以儲存32種許可權,以及2的32次方種許可權組合。那麼知道了這個知識點,我們如何將它設計進我們的許可權系統呢?
三、許可權的增刪查
知道了如何儲存許可權,但是我們還需要操作許可權。而一個許可權系統,必然會有三大基礎操作:
- 新增一個許可權
- 刪除一個許可權
- 以及校驗一個許可權
那我們如何對一個二進位制數字表示的許可權,進行增刪查呢?這裡就要利用位操作了:
- | 新增許可權
- ^ 刪除許可權(已有許可權時)
- & 校驗許可權
用程式碼來解釋一下
/**
* 新增許可權
*
* @param currentPermission 原許可權
* @param permissions 需要新增的許可權集合
* @return 新增完許可權後的十進位制數字
*/
public static int addPermissions(int currentPermission, int... permissions) {
for (int permission : permissions) {
currentPermission |= permission;
}
return currentPermission;
}
/**
* 從已有許可權裡,刪除許可權
*
* @param currentPermission 原已有許可權
* @param permissions 需要刪除的許可權集合
* @return 刪除完許可權後的十進位制數字
*/
public static int removePermissions(int currentPermission, int... permissions) {
for (int permission : permissions) {
if (!hasPermission(currentPermission, permission)) {
continue;
}
currentPermission ^= permission;
}
return currentPermission;
}
/**
* 校驗許可權
*
* @param currentPermission 原許可權
* @param permission 要校驗的許可權
* @return 是否含有許可權
*/
public static boolean hasPermission(int currentPermission, int permission) {
return (currentPermission & permission) == permission;
}
為什麼上述三個操作可以做到新增、刪除、查詢許可權呢?我們需要來複習一下位運算。
位運算 | ||||
---|---|---|---|---|
或(|):兩個都為0,結果才為0,否則為1 | 0 | 0 = 0 | 0 | 1 = 1 | 1 | 0 = 1 | 1 | 1 = 1 |
異或(^):兩個相同為0,不相同為1 | 0 ^ 0 = 0 | 0 ^ 1 = 1 | 1 ^ 0 = 1 | 1 ^ 1 = 0 |
與(&):兩個都為1,結果才為1,否則為0 | 0 & 0 = 0 | 0 & 1 = 0 | 1 & 0 = 0 | 1 & 1 = 1 |
根據位運算的特點,我們可以發現:
- 給執行許可權裡,新增寫入許可權,本質應該是將
執行許可權(0001)
的第二個0,變成1,那麼只要或
一個寫入許可權(0010)
就好了。(或一個數,就代表將這個數表示的許可權,新增到原來的數裡,無論原來的數字有沒有許可權都會加進去):
- 在執行、寫入許可權裡,刪除寫入許可權,本質應該是將
執行、寫入許可權(0011)
的第二個1,變成0,那麼只要異或
一個寫入許可權(0010)
就好了。(異或一個數,就代表將這個數表示的許可權,從原來的數字裡刪除):
- 在執行、寫入許可權裡,判斷是否有寫入許可權,其本質應該是判斷
執行、寫入許可權(0011)
的第二位,是否是1,那麼只要與
一個寫入許可權(0010)
,再與寫入許可權(0010)
自身比較一下是否相等
就好了。(與一個數,如果還等於這個數,就代表原來的數有這個數表示的許可權):
這裡特別說明一下“異或”刪除許可權的操作,只有原來的數字裡已經有許可權了,才可以刪除。從異或運算的規則中可以發現,異或運算其實是無則增,有則減的操作。那麼如果我想,無論原來的數字有沒有許可權,都刪除許可權(有或無,都減),該怎麼操作呢?
有兩個方法
第一個方法就是我上面程式碼裡的第二個方法所示,異或操作前,先判斷下有無許可權,有許可權時再刪除,無許可權自然也不需要刪除。
第二個方法是先“取反”運算,再“與”運算
/**
* 刪除許可權
*
* @param currentPermission 原許可權
* @param permissions 需要刪除的許可權集合
* @return 刪除完許可權後的十進位制數字
*/
public static int removePermissions2(int currentPermission, int... permissions) {
for (int permission : permissions) {
currentPermission &= ~permission;
}
return currentPermission;
}
還是以在執行、寫入許可權裡,刪除寫入許可權為例,先對要刪除的寫入許可權(0010)
“取反”
再將執行、寫入許可權(0011)
“與”一個上面取反的結果
可以發現,結果是一樣的。這樣的好處就是,不用判斷原來的數字裡,是否含有許可權了,但沒有上面先校驗許可權的方法那麼簡單易懂。
四、總結
優點:既然是二進位制儲存,位運算操作,肯定有節省空間,效率極高的優點,當然同時也是逼格滿滿。
缺點:許可權種類有限,如果用int儲存,最多隻能有32種許可權型別,用long型則最多有64種許可權型別。當然這個也是有解決方案的,反正是在位元位上用二進位制儲存許可權,那麼也不用侷限於整數型,也可以將二進位制數字用字串表示,但這樣運算起來比較麻煩,所以這種設計最好還是用於許可權種類不太多的情況比較好。
舉一反三,本文的知識點也不僅僅用於許可權系統。
所有需要做大量判斷的系統或介面,都可以通過本文的方法,將眾多表達true/false概念的引數,通過一個短短的數字傳遞到系統或介面裡,可以節省空間和提高效率。
五、一個許可權工具類demo
/**
* 許可權工具類demo
*
* @author dijia478
* @date 2021-11-20 16:52:10
*/
public class PermissionUtils {
/**
* 所有許可權都沒有
*/
public static final int NOT_ALL = 0;
/**
* 所有許可權都有
*/
public static final int HAVE_ALL = -1;
/**
* 執行許可權
*/
public static final int EXECUTE = 1 << 0;
/**
* 新建許可權
*/
public static final int CREATE = 1 << 1;
/**
* 查詢許可權
*/
public static final int SELECT = 1 << 2;
/**
* 修改許可權
*/
public static final int UPDATE = 1 << 3;
/**
* 刪除許可權
*/
public static final int DELETE = 1 << 4;
/**
* 進行欄位校驗
*/
public static final int CHECK_ITEM = 1 << 5;
/**
* 記錄操作日誌
*/
public static final int OPERATE_LOG = 1 << 6;
/**
* 傳送通知
*/
public static final int SEND_MSG = 1 << 7;
/**
* 新增許可權
*
* @param currentPermission 原許可權
* @param permissions 需要新增的許可權集合
* @return 新增完許可權後的十進位制數字
*/
public static int addPermissions(int currentPermission, int... permissions) {
for (int permission : permissions) {
currentPermission |= permission;
}
return currentPermission;
}
/**
* 從已有許可權裡,刪除許可權
*
* @param currentPermission 原已有許可權
* @param permissions 需要刪除的許可權集合
* @return 刪除完許可權後的十進位制數字
*/
public static int removePermissions(int currentPermission, int... permissions) {
for (int permission : permissions) {
if (!hasPermission(currentPermission, permission)) {
continue;
}
currentPermission ^= permission;
}
return currentPermission;
}
/**
* 刪除許可權
*
* @param currentPermission 原許可權
* @param permissions 需要刪除的許可權集合
* @return 刪除完許可權後的十進位制數字
*/
public static int removePermissions2(int currentPermission, int... permissions) {
for (int permission : permissions) {
currentPermission &= ~permission;
}
return currentPermission;
}
/**
* 校驗許可權
*
* @param currentPermission 原許可權
* @param permission 要校驗的許可權
* @return 是否含有許可權
*/
public static boolean hasPermission(int currentPermission, int permission) {
return (currentPermission & permission) == permission;
}
/**
* 獲取所有許可權
*
* @return 擁有所有許可權的十進位制數字,其實就是-1
*/
public static int getAllPermission() {
return HAVE_ALL;
}
}