本文討論是權限設計的其中一種思路,有它自己的優缺點,不一定適用於所有系統。
一、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 個比特位,我們就可以存儲 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) {
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 |
取反(~):0變1,1變0 | ~0 = 1 | ~1 = 0 |
根據位運算的特點,我們可以發現:
- 給執行權限里,添加寫入權限,本質應該是將
執行權限(0001)
的第二個0,變成1,那么只要或(|)
一個寫入權限(0010)
就好了。(或一個數,就代表將這個數表示的權限,添加到原來的數字里。無論原來的數字有沒有權限都會加進去):
- 在執行、寫入權限里,刪除寫入權限,本質應該是將
執行、寫入權限(0011)
的第二個1,變成0,那么只要異或(^)
一個寫入權限(0010)
就好了。(異或一個數,就代表將這個數表示的權限,從原來的數字里刪除。只有在原來的數字已有這個權限時才會刪除,否則是添加):
- 在執行、寫入權限里,判斷是否有寫入權限,其本質應該是判斷
執行、寫入權限(0011)
的第二位,是否是1,那么只要與(&)
一個寫入權限(0010)
,再將結果和寫入權限(0010)
自身比較一下是否相等
就好了。(與一個數,如果還等於這個數,就代表原來的數字有這個數表示的權限):
四、特殊的“異或”刪除操作
這里特別說一下上面“異或”刪除權限的操作,上面括號里也說了,只有原來的數字里已經有該權限了,才可以刪除。
因為從異或運算的規則中可以發現,異或運算其實是無則增,有則減的操作。
那么如果我想,無論原來的數字有沒有權限,都刪除權限(無論原來是0或1,都變成0),該怎么操作呢?
有三個方法:
第一個方法是,異或操作前,先判斷下有無權限,有權限時再刪除,無權限自然也不需要刪除權限。
/**
* 從已有權限里,刪除權限
*
* @param currentPermission 原已有權限
* @param permissions 需要刪除的權限集合
* @return 刪除完權限后的十進制數字
*/
public static int removePermissions1(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 = addPermissions(currentPermission, permission);
currentPermission ^= permission;
}
return currentPermission;
}
第三個方法是,先“取反”運算,再“與”運算
/**
* 刪除權限
*
* @param currentPermission 原權限
* @param permissions 需要刪除的權限集合
* @return 刪除完權限后的十進制數字
*/
public static int removePermissions3(int currentPermission, int... permissions) {
for (int permission : permissions) {
currentPermission &= ~permission;
}
return currentPermission;
}
第三種這個理解起來比較困難,我解釋一下。還是以在執行、寫入權限里,刪除寫入權限為例,先對要刪除的寫入權限(0010)
“取反”
再將執行、寫入權限(0011)
“與”一個上面取反的結果
可以發現,結果是一樣的。這樣的好處就是,不用判斷原來的數字里,是否含有權限了,也不用先改變原來數字的權限,但沒有第一種和第二種方法那么簡單易懂。
這里推薦使用第三種方法,無它,更簡潔,逼格更高。
五、總結
優點:
既然是二進制存儲,位運算操作,肯定有節省空間,效率極高的優點,當然同時也是逼格滿滿。
缺點:
權限種類有限,如果用 int 存儲,最多只能有 32 種權限類型,用 long 型則最多可以有 64 種權限類型。所以本文的這種設計最好還是用於權限種類不太多的情況比較好。
六、舉一反三
本文的知識點也不僅僅用於權限系統。
例如你寫了個接口,提供了大量可選操作,比如校驗參數,記錄日志,使用緩存,發送通知等等,如果有幾十個這種,是或否含義的選項,你會怎么設計你的接口參數呢?
是通過一個對象封裝,把每個選項都設計成一個 boolean 類型字段,然后在接口里到處判斷每個字段的值是 true 還是 false 嗎?這樣固然可以實現,但就是顯得有點普通平庸了,代碼寫出來也會非常凌亂,體現不出來你的水平。而且調用方傳參也會很痛苦。
而通過本文的方法,就可以將眾多表達 true/false 概念的參數,通過一個短短的 int 類型值傳遞到系統或接口里,節省空間和提高效率,拉高代碼的逼格,讓人看了拍案叫絕。
七、一個權限工具類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;
/**
* 使用緩存
*/
public static final int USE_CACHE = 1 << 8;
/**
* 添加權限
*
* @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 removePermissions1(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 = addPermissions(currentPermission, permission);
currentPermission ^= permission;
}
return currentPermission;
}
/**
* 刪除權限
*
* @param currentPermission 原權限
* @param permissions 需要刪除的權限集合
* @return 刪除完權限后的十進制數字
*/
public static int removePermissions3(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;
}
/**
* 獲取所有權限都沒有
*
* @return 沒有所有權限的十進制數字,其實就是0
*/
public static int getNotAllPermission() {
return NOT_ALL;
}
/**
* 只獲取需要的權限,除了需要的,其他權限都沒有
*
* @param permissions 需要的權限
* @return 所有需要的權限的十進制數字
*/
public static int getNeedPermission(int... permissions) {
return addPermissions(NOT_ALL, permissions);
}
/**
* 排除不需要的權限,除了不需要的,其他權限都有
*
* @param permissions 不需要的權限
* @return 所有不需要的權限的十進制數字
*/
public static int getNotNeedPermission(int... permissions) {
return removePermissions3(HAVE_ALL, permissions);
}
}