今天我们将用通俗易懂的语言,来讲解一个真实开发中的权限控制场景。
一、背景
我们在编写一个用户的模块时,增加了一个 是否管理员 的字段,当这个字段为 true
时,该用户拥有管理员权限,可以进行一些敏感操作,比如删除用户等。
java
@Entity
@Data
public class UserEntity extends BaseEntity<UserEntity> {
// 其他属性
@Column(columnDefinition = "tinyint UNSIGNED default 0 comment '是否管理员'")
private Boolean isAdmin;
}
然而好景不长,新的需求来了,需要给用户添加一个 是否会员 的标识,后端的同学大手一挥,加个字段不就解决了?
java
@Entity
@Data
public class UserEntity extends BaseEntity<UserEntity> {
// 其他属性
@Column(columnDefinition = "tinyint UNSIGNED default 0 comment '是否管理员'")
private Boolean isAdmin;
@Column(columnDefinition = "tinyint UNSIGNED default 0 comment '是否VIP'")
private Boolean isVip;
}
我猜你已经想到了接下来的场景,我们可能往用户表上添加各种各样标识的字段,比如 是否在线 是否禁用 等等等等。
于是用户表的字段个数一天比一天多,每次发新功能的版本都还要去修改数据库的表结构。
二、运维生气了
你就不能用一个字段来表达你这些所有的标识吗?我的工作就是帮你加字段吗???
确实挺麻烦的,我们得考虑一下技术方案了,老加字段确实不是个事。
三、原理分析
权限是否开启
依稀记得上学那会在物理课上学过,电路开关就两个,开和关,我们用一个开关来控制电路的开关:
- 有权限,那就亮灯;
- 没权限,那灯就不亮。
在计算机底层原理里也学过,这不就妥妥的二进制吗?
二进制就是 0
和 1
,0
表示关,1
表示开。
那我们就按是否亮灯的逻辑来处理,
1
代表亮灯(有权限),0
代表灯灭(无权限)
权限类别
我们按上面的一些需求,将二进制的 每一位 当成一个 灯,灯的状态就是权限状态。
是否删除 | 是否禁用 | 是否在线 | 是否 VIP | 是否管理员 |
---|---|---|---|---|
0 | 0 | 1 | 0 | 0 |
于是我们可以得到一个二进制数 00100
,这个二进制数就是权限的组合结果。
然后我们可以将这个二进制数字直接存入数据库的一个字段中了。
有了基础思路,那就可以开始设计了。
三、标识设计
设计权限
我们可以通过二进制来获得一些功能标识的开启和关闭,比如:
java
public class Flag {
// 灯全灭
private static final int NOTHING = 0;
// 第一个灯亮
private static final int IS_ADMIN = 1;
// 第二个灯亮(第一个左移1位)
private static final int IS_VIP = 1 << 1;
// 第三个灯亮(第一个左移2位)
private static final int IS_ONLINE = 1 << 2;
// 第四个灯亮(第一个左移3位)
private static final int IS_DISABLED = 1 << 3;
// 第五个灯亮(第一个左移4位)
private static final int IS_DELETED = 1 << 4;
}
这里我们使用了
<<
操作符,这个操作符是位运算符,它将一个数的二进制表示向左移动指定的位数,并返回结果。 也就是我们从右边第一个灯开始,依次左移来得到每个灯的标识的值
组合权限
如果我们需要 是管理员 同时 在线 ,那么也就是 IS_ADMIN
和 IS_ONLINE
两个灯同时亮。
我们只需要将 IS_ADMIN
和 IS_ONLINE
两个灯的标识值进行 按位或运算(or) 即可。
所以 IS_ADMIN | IS_ONLINE
的结果就是二进制的 00101
,也就是十进制的 5
。
为什么是按位或运算(or)?
照理说应该是表达 是管理员 并且 在线 ,不应该是 按位与运算(and) 吗?
这里其实不是将 是管理员 和 在线 进行 按位或运算(or) ,而是将 是管理员 和 在线 这两个灯表示的二进制进行 按位或运算(or)。
我们来看看两个二进制:
- 0 0 0 0 1 是管理员
- 0 0 1 0 0 在线
我们要表示 是管理员 并且 在线,需要的二进制是:
- 0 0 1 0 1 是管理员 并且 在线,这两个灯都亮。
我们知道,按位或运算(or) 是只要其中一个为 1,那么计算结果就为 1,正好得出了我们想要的结果。
而 按位与运算(and) 是都为 1 计算才为 1,显然不是我们要的结果。
这里的前提是,我们确认了每个权限只对应了二进制中的一个位,并且不同权限不会使用同一个位。
四、权限计算和判断
有了上面的设计,我们可以合理设计权限标识,也可以正常计算组合权限并存储权限了。
那么,如何来做权限计算和判断呢?
添加当前权限属性
java
public class Flag {
private static final int NOTHING = 0;
private static final int IS_ADMIN = 1;
// 省略其他权限标识
// 当前权限
private int currentFlag = 0;
}
添加权限
添加权限刚才已经讲过了,通过 按位或运算(or)
移除权限
移除权限就是把当前权限标识和 按位取反(~) 后的权限标识进行 按位与运算(and):
java
int newFlag = currentFlag & ~IS_ADMIN;
- 我们先把
IS_ADMIN
的权限灯通过 按位取反(~) 关掉 - 再通过 按位与运算(and) 计算当前权限状态和 关掉之后的
IS_ADMIN
的权限灯的运算结果得新的权限标识
我们还是用这个二进制例子来说明:
- 0 0 1 0 1 是管理员 且 在线
我们要取消掉管理员这个灯,最终的结果应该是
- 0 0 1 0 0
于是我们先对管理员权限标识进行按位取反,得到
- 0 0 0 0 1 管理员
~
- 1 1 1 1 0 按位取反后
然后再将原始状态和按位取反结果进行按位与运算:
- 0 0 1 0 1 原始
- &
- 1 1 1 1 0 按位取反后
即可得到最终结果:
- 0 0 1 0 0
于是这个就是我们最终的权限二进制标识了
权限标识类设计
添加权限我们上面已经说过了,其实就是继续组合其他权限,那么我们直接用 set
方法来设置权限:
java
public class Flag {
private static final int NOTHING = 0;
private static final int IS_ADMIN = 1;
// 省略其他权限标识
private int currentFlag = 0;
public void setAdmin(boolean flag) {
if (flag) {
// 用按位或运算来添加权限
currentFlag |= Flag.IS_ADMIN;
} else {
// 用按位与运算来移除权限
currentFlag &= ~Flag.IS_ADMIN;
}
}
public boolean isAdmin(){
// 用按位与运算来判断是否有该权限
return (currentFlag & Flag.IS_ADMIN) == Flag.IS_ADMIN;
}
public static void main(String[] args) {
Flag flag = new Flag();
flag.setAdmin(true); // 设置为管理员
System.out.println(flag.isAdmin()); // 打印 true
flag.setAdmin(false); // 取消设置为管理员
System.out.println(flag.isAdmin()); // 打印 false
}
}
如上面的测试,我们成功设置了一个权限标识位,实现了 设置权限 、移除权限 、判断权限。
五、完整代码
java
public class Flag {
private static final int NOTHING = 0;
private static final int IS_ADMIN = 1;
private static final int IS_VIP = 1 << 1;
private static final int IS_ONLINE = 1 << 2;
private static final int IS_DISABLED = 1 << 3;
private static final int IS_DELETED = 1 << 4;
private int currentFlag = 0;
public void setAdmin(boolean flag) {
if (flag) {
currentFlag |= Flag.IS_ADMIN;
} else {
currentFlag &= ~Flag.IS_ADMIN;
}
}
public boolean isAdmin() {
return (currentFlag & Flag.IS_ADMIN) == Flag.IS_ADMIN;
}
public void setVip(boolean flag) {
if (flag) {
currentFlag |= Flag.IS_VIP;
} else {
currentFlag &= ~Flag.IS_VIP;
}
}
public boolean isVip() {
return (currentFlag & Flag.IS_VIP) == Flag.IS_VIP;
}
public void setOnline(boolean flag) {
if (flag) {
currentFlag |= Flag.IS_ONLINE;
} else {
currentFlag &= ~Flag.IS_ONLINE;
}
}
public boolean isOnline() {
return (currentFlag & Flag.IS_ONLINE) == Flag.IS_ONLINE;
}
public void setDisabled(boolean flag) {
if (flag) {
currentFlag |= Flag.IS_DISABLED;
} else {
currentFlag &= ~Flag.IS_DISABLED;
}
}
public boolean isDisabled() {
return (currentFlag & Flag.IS_DISABLED) == Flag.IS_DISABLED;
}
public void setDeleted(boolean flag) {
if (flag) {
currentFlag |= Flag.IS_DELETED;
} else {
currentFlag &= ~Flag.IS_DELETED;
}
}
public boolean isDeleted() {
return (currentFlag & Flag.IS_DELETED) == Flag.IS_DELETED;
}
public static void main(String[] args) {
Flag flag = new Flag();
flag.setAdmin(true); // 设置为管理员
flag.setDeleted(true);// 设置已删除
System.out.println(flag.isAdmin()); // 打印 true
System.out.println(flag.isDeleted()); // 打印 true
System.out.println("当前标识: " + flag.currentFlag); // 打印 17
flag.setAdmin(false); // 取消设置为管理员
System.out.println(flag.isAdmin()); // 打印 false
System.out.println(flag.isDeleted()); // 打印 true
System.out.println("当前标识: " + flag.currentFlag); // 打印 16
}
}
通过上面的代码,我们可以随意的 设置权限 ,移除权限 ,判断权限 ,也可以直接拿出 currentFlag 的值存储到数据库中。
一个字段存储所有的权限,这不就搞定了吗~
六、问题和总结
当然,这个方案还不是最完美的,你能发现什么问题吗?欢迎在评论区讨论~
感谢阅读,再见了您嘞。