本文讨论是权限设计的其中一种方向,有它自己的优缺点,不一定适用于所有系统。
一、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种权限。那么知道了这个知识点,我们如何将它设计进我们的权限系统呢?
三、权限的增删查
一个权限系统,必然会有三大基础操作:添加一个权限,删除一个权限,以及校验一个权限。
那我们如何对一个二进制数字表示的权限,进行增删查呢?这里就要利用位操作了:
- | 可以用来添加权限
- ^ 可以用来删除权限(已有权限时)
- & 可以用来校验权限
用代码来解释一下
/**
* 添加权限
*
* @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种权限类型,无法应用于复杂的权限场景,例如牵扯到权限,角色,用户。
如何选择,根据自己实际需要判断即可。
五、一个权限工具类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 WRITE = 1 << 1;
/**
* 读取权限
*/
public static final int READ = 1 << 2;
/**
* 修改权限
*/
public static final int UPDATE = 1 << 3;
/**
* 删除权限
*/
public static final int DELETE = 1 << 4;
/**
* 添加权限
*
* @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;
}
}