巧妙使用位运算来解决真实开发中的权限控制场景

今天我们将用通俗易懂的语言,来讲解一个真实开发中的权限控制场景。

一、背景

我们在编写一个用户的模块时,增加了一个 是否管理员 的字段,当这个字段为 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;
}

我猜你已经想到了接下来的场景,我们可能往用户表上添加各种各样标识的字段,比如 是否在线 是否禁用 等等等等。

于是用户表的字段个数一天比一天多,每次发新功能的版本都还要去修改数据库的表结构。

二、运维生气了

你就不能用一个字段来表达你这些所有的标识吗?我的工作就是帮你加字段吗???

确实挺麻烦的,我们得考虑一下技术方案了,老加字段确实不是个事。

三、原理分析

权限是否开启

依稀记得上学那会在物理课上学过,电路开关就两个,开和关,我们用一个开关来控制电路的开关:

  • 有权限,那就亮灯;
  • 没权限,那灯就不亮。

在计算机底层原理里也学过,这不就妥妥的二进制吗?

二进制就是 010 表示关,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_ADMINIS_ONLINE 两个灯同时亮。

我们只需要将 IS_ADMINIS_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 的值存储到数据库中。

一个字段存储所有的权限,这不就搞定了吗~

六、问题和总结

当然,这个方案还不是最完美的,你能发现什么问题吗?欢迎在评论区讨论~

感谢阅读,再见了您嘞。

相关推荐
乔大将军1 小时前
项目准备(flask+pyhon+MachineLearning)- 1
后端·python·flask
码熔burning1 小时前
(十 九)趣学设计模式 之 中介者模式!
java·设计模式·中介者模式
_TokaiTeio1 小时前
微服务100道面试题
java·微服务
eqwaak02 小时前
基于Spring Cloud Alibaba的电商系统微服务化实战:从零到生产级部署
java·分布式·微服务·云原生·架构
m0_672656542 小时前
【Rabbitmq篇】高级特性----TTL,死信队列,延迟队列
java·rabbitmq·java-rabbitmq
胡图蛋.2 小时前
Spring 中哪些情况下,不能解决循环依赖问题?
java·后端·spring
24k小善2 小时前
Flink Oceanbase Connector详解
java·大数据·flink
ChinaRainbowSea2 小时前
8. Nginx 配合 + Keepalived 搭建高可用集群
java·运维·服务器·后端·nginx
猫头鹰~2 小时前
Ubuntu20.04安装Redis
java·数据库·redis
夏末秋也凉2 小时前
力扣-动态规划-674 最长连续递增序列
算法·leetcode·动态规划