如何优雅的完成权限认证?-- 瞎扯文

前言

以前挺喜欢计算机的,出来工作后发现,现在更喜欢了嘿嘿~当然也是沉寂了一个月之久。终于又来冒泡了,实属不易啊。消失了这么久的时间,干嘛去了呢?很简单实习去了,当然不是某互联网公司哈。哎,没办法谁让自己当初比较单纯,非要去走一条不适合自己的路,只能拣点剩饭 。。。。。。 okey,回归主体,我们都知道,我们平时在做权限验证的时候,我们会用到我们的RABC模型,来表示到我们用户的一个角色和权限。

那么先前的话,我也是接到了一个项目,需要来设计一个权限管理。当时很快哈,我就梳理清楚了思路,直接用RABC模型开始梭哈:

啪的一下啊,很快哈,几张表就出来了。

当然,按照我们的这边的业务需求,我们实际上是这样的:

常规解决方案

那么接下来,我们一统操作之后,当用户请求登录过来的时候,我们可以使用第三方开源框架,例如shiro,security 来对我们的某个接口完成权限验证。

当然这里具体的流程大概是这样的: 这里的话,不管是shiro,还是security也好大体流程都是这样的。当然考虑实际需求,这里是前后端分离的一个项目,很多功能我是用不上的,于是我这里直接基于AOP切面实现对应的权限验证,当然流程都是类似的 这里代码就不提供了,相信各位老司机都懂。

那么问题来了,这套方案下来有什么不好的地方嘛? 其实最大的问题就在一个地方: 怎么快速查询到用户权限,拿到用户权限之后,我们对比是很快的,只需要对比一下权限描述就行 无论你怎么处理,你会发现,你都需要先查询一遍数据库,然会这个时候,当第一次查询到这个用户的权限的时候,我们可以选择把数据缓存到redis里面,然后加快查询速度。

但是这里的话有个问题:

  1. 如果使用redis做缓存,那么缓存的数据同步是个问题,我们需要实现一个可行的监听机制来完成权限的动态刷新。
  2. 在并发情况下,存在redis击穿的问题

那么我是这么解决的呢。当然首先,在我实习参加的这个项目当中呢,我用的当然是这个传统方案。并且我们的这项目是给公司内部部门用的,不存在高并发的情况。并且我们的这权限是在用户登录的时候,就放到了缓存里面,并且随着token一起过期。当用户权限被修改之后,我们会要求用户重新登录,重新获取权限。并且在用户登录的情况下,对某一个用户来说基本上没有并发的说法。并且对登录接口我们做了限流

用一张流程图可以这样表述:

当然具体的实现,这里面还有很多细节的。

那么这样做看起来好像没有什么问题了,好像也不错了,那么还有什么问题呢?问题当然就在这块我们在拿到权限的时候,你会发现,我这里其实有4个中间表要查询。如果用户量足够多,那么查询将变慢,当然一般情况下,一个项目,尤其是我当前的实习单位来说,可能连mysql的性能瓶颈都达不到。但是作为一个阿宅,我们得想想有没有更好的方式可以帮助我们完成这部分的操作,能不能只需要在用户表上面多加入一个字段就解决问题?最好就是几个数字就完成权限表示呢

Linux系统文件的权限

答案当然有,学过408熟读二进制表示的各位同学一定不会对Linux权限表示感到陌生吧。

我们会发现,我们的linux权限一般是这样表示的: -rw-r--r-- 对应的用户类型分别为:

  1. 文件所有者(user)
  2. 文件所属组(group)
  3. 其他用户(others)

并且你一定对这个指令非常熟悉吧:

bash 复制代码
chmod 777 HuteroxIsAwesome

其中:

4:表示读权限,用 chmod 400 file 命令设置:100 2:表示写权限,用 chmod 200 file 命令设置: 010 1:表示执行权限,用 chmod 100 file 命令设置: 001 0:表示没有任何权限,用 chmod 000 file 命令设置:000

当这几个加在一个的时候,是111-》7 于是就表示了具备全部权限。

那么这个时候,我们来回想一下我们的RABC模型:

  1. 角色
  2. 权限

我们假设我们有2个角色对应这样的权限:

干饭人:

  1. 吃饭

  2. 睡觉

  3. 打豆豆

  4. 打游戏

  5. 干老板

happy人:

  1. 凑热闹

  2. 看乐子

  3. 玩耍

然后我们一个int8来表示我们的权限

干饭人:

  1. 吃饭 00000001

  2. 睡觉 00000010

  3. 打豆豆 00000100

  4. 打游戏 00001000

  5. 干老板 00010000

happy人:

  1. 凑热闹 00000001

  2. 看乐子00000010

  3. 玩耍 00000100

然后,我用00011111 和 00000111 分别对应十进制的 31,7 就可以表示,这个人具备怎么样的权限。在验证的时候也非常简单,位运算即可。

这个问题好像看起来就解决了,但是一个int64好像只能表示64个权限。想要表示更多权限好像只能想办法增大位数。而且当前好像只是解决了从角色到权限的问题,还没有解决从角色到权限组到权限,或者更高层级的问题。当然我们姑且认为够用了,实在不行我们用大数处理嘛

并且,你现在要表示这两个角色,你还得传递两个数字过来,存到数据库里面,你还得存入字符串这是在是太麻烦了,我们需要更加精简!

那么怎么能够用一个数字表示317呢?--》9t 32进制当然,这里我们是在使用到int8的时候。如果使用到int64那么对应的就是 2的64进制的数字,这当然太大了。因为这实在是太大了,但是这个方案是否不可行呢?答案显然应该是否定的,因为我们实际上使用的是位数的运算,它可以表示的范围很大,但是实际上我们只需要遍历一遍位数即可。那么这个时候,我们需要做的显然就是控制位数的情况下去进行编码。这样一来我们就能很好解决这些问题不过如何编码确实又是一个很麻烦的问题,好吧,看来太存粹的方式没有了。因为随着层级的增加我们需要的进制数将变大。如果你需要用具体的数字表示,那么这个数字将非常大。此时又蜕变为直接使用符号表示。

好吧,现在我们通过一番讨论最终发现好像又回到了直接使用字符串数组表示权限的时候。并且我们发现不如直接使用编码压缩

编码压缩

写到这里我想我已经不知道要说啥,现在的问题转化为了如何对权限字符序列进行压缩的问题了。于是我们想到了编码压缩的方式。 压缩算法主要分为这两大类:

那么在这里由于本人学识有限,只能来搞一搞哈夫曼编码器。 哈夫曼编码是一种常用的无损压缩算法,它通过构建最优前缀编码来实现数据的压缩。哈夫曼编码的原理是根据符号出现的频率来构建一个最优的二叉树(哈夫曼树),并将出现频率高的符号用较短的编码表示,出现频率低的符号用较长的编码表示。那么这个的话,相信各位都非常熟悉了,我们只需要设定权重,然后我们使用一个堆就可能创建我们的树了。这里我们提供Java和Python版本的实现。 那么这个哈夫曼编码的具体步骤是这样的:

  1. 统计输入数据中每个符号的出现频率。
  2. 根据频率构建哈夫曼树。首先创建一个包含所有符号的叶子节点集合,每个节点的权重为符号的频率。然后重复以下步骤直到只剩下一个根节点:
  3. 从节点集合中选择两个权重最小的节点,作为左右子节点创建一个新的父节点。
  4. 将新节点的权重设为左右子节点权重之和。
  5. 将新节点加入节点集合。
  6. 从节点集合中删除原先选出的两个节点。
  7. 根据哈夫曼树为每个符号分配唯一的编码。从根节点出发,沿着左子树走为0,沿着右子树走为1,记录下路径上的0和1即为符号的编码。
  8. 使用生成的编码对输入数据进行压缩。将每个符号替换为对应的编码。
  9. 将压缩后的数据以及编码表(记录每个符号的编码)一起保存,以便解压缩时使用。

那么我们解码的时候,拿到这棵树,按照我们刚刚定制的左0右1的规则,去找到这棵树的节点即可。

Python版本

那么首先我们要实现这个编码器。那么我们需要先定义节点。

python 复制代码
class Node:
    def __init__(self, weight, value=None):
        self.weight = weight
        self.value = value
        self.left = None
        self.right = None

这里面有权重和值,这个值就可以是我们的权限名字,那么权重的话,我们可以随意给,就默认按照下标来也行。

之后我们构建这棵树。

python 复制代码
def build_huffman_tree(nodes):
    heap = nodes[:]  # 复制节点数组作为初始堆

    heapq.heapify(heap)  # 将数组转换为堆结构

    while len(heap) > 1:
        node1 = heapq.heappop(heap)
        node2 = heapq.heappop(heap)
        merged_weight = node1.weight + node2.weight
        merged_node = Node(merged_weight)
        merged_node.left = node1
        merged_node.right = node2
        heapq.heappush(heap, merged_node)

    root = heap[0]  # 得到最终的根节点

    # 构建编码字典
    code_dict = {}
    get_code(root, "", code_dict)

    return root, code_dict


def get_code(node, code, code_dict):
    if node.value is not None:
        code_dict[node.value] = code
        return

    get_code(node.left, code + "0", code_dict)
    get_code(node.right, code + "1", code_dict)
java 复制代码
nodes = [Node(2, 'a'), Node(3, 'b'), Node(7, 'c'), Node(10, 'd')]

root,code = build_huffman_tree(nodes)

此时就会返回根节点,这个就是我们的树。也是我们这个对应解码器接下来需要的东西。

之后是我们的解码器,这里一行代码搞定:

ini 复制代码
encoded_data = "".join([code_dict[ch] for ch in data])

Java版本

那么接下来就是我们的Java版本

java 复制代码
class Node implements Comparable<Node> {
    int weight;
    Character value;
    Node left;
    Node right;

    public Node(int weight, Character value) {
        this.weight = weight;
        this.value = value;
        this.left = null;
        this.right = null;
    }

    @Override
    public int compareTo(Node other) {
        return Integer.compare(this.weight, other.weight);
    }
}

public class HuffmanTreeBuilder {

    public static Node buildHuffmanTree(List<Node> nodes) {
        PriorityQueue<Node> heap = new PriorityQueue<>(nodes); // 使用优先队列作为堆结构

        while (heap.size() > 1) {
            Node node1 = heap.poll();
            Node node2 = heap.poll();
            int mergedWeight = node1.weight + node2.weight;
            Node mergedNode = new Node(mergedWeight, null);
            mergedNode.left = node1;
            mergedNode.right = node2;
            heap.offer(mergedNode);
        }

        return heap.poll(); // 返回最终的根节点
    }

    public static void getCode(Node node, String code, Map<Character, String> codeDict) {
        if (node.value != null) {
            codeDict.put(node.value, code);
            return;
        }

        getCode(node.left, code + "0", codeDict);
        getCode(node.right, code + "1", codeDict);
    }

    public static void main(String[] args) {
        String data = "abcdbcccd";
        Map<Character, Integer> counter = new HashMap<>();
        for (char ch : data.toCharArray()) {
            counter.put(ch, counter.getOrDefault(ch, 0) + 1);
        }

        List<Node> nodes = new ArrayList<>();
        for (Map.Entry<Character, Integer> entry : counter.entrySet()) {
            nodes.add(new Node(entry.getValue(), entry.getKey()));
        }

        Node root = buildHuffmanTree(nodes);

        Map<Character, String> codeDict = new HashMap<>();
        getCode(root, "", codeDict);

        System.out.println("编码字典是:");
        for (Map.Entry<Character, String> entry : codeDict.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

        StringBuilder encodedData = new StringBuilder();
        for (char ch : data.toCharArray()) {
            encodedData.append(codeDict.get(ch));
        }

        System.out.println("原始数据: " + data);
        System.out.println("编码结果: " + encodedData.toString());

        StringBuilder decodedData = new StringBuilder();
        Node currentNode = root;
        for (char bit : encodedData.toString().toCharArray()) {
            if (bit == '0') {
                currentNode = currentNode.left;
            } else if (bit == '1') {
                currentNode = currentNode.right;
            }

            if (currentNode.value != null) {
                decodedData.append(currentNode.value);
                currentNode = root;
            }
        }

        System.out.println("解码结果: " + decodedData.toString());
    }
}

总结

okey,以上就是全部内容,好像还是瞎扯了一波,照应标题,好的~

相关推荐
goTsHgo12 分钟前
在 Spring Boot 的 MVC 框架中 路径匹配的实现 详解
spring boot·后端·mvc
waicsdn_haha23 分钟前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
Q_192849990634 分钟前
基于Spring Boot的摄影器材租赁回收系统
java·spring boot·后端
良许Linux38 分钟前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网
求知若饥1 小时前
NestJS 项目实战-权限管理系统开发(六)
后端·node.js·nestjs
左羊1 小时前
【代码备忘录】复杂SQL写法案例(一)
后端
gb42152872 小时前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶2 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
颜淡慕潇2 小时前
【K8S问题系列 |19 】如何解决 Pod 无法挂载 PVC问题
后端·云原生·容器·kubernetes