当前位置: 首页 > news >正文

搭建一个基于角色的权限验证框架

说明:基于角色的权限验证(Role-Based Access Control,RBAC)框架,是目前大多数服务端的登录校验框架。本文介绍如何快速搭建一个这样的框架,不用Shiro、Spring Security、Sa-Token这样的“大框架”实现。

RBAC

基于角色的权限验证,包含了三个实体,用户、角色、权限,三种关系如下:

在这里插入图片描述

一个用户可以有多个角色,一个角色有多个权限,例如,某用户张三,拥有超级管理员权限、普通用户这两个角色,而其中超级管理员具有删除用户、创建用户权限。

综上分析,三个实体,两个多对多关系,一般来说,我们需要以下五张表:

  • 用户表;

  • 角色表;

  • 权限表;

  • 用户角色表;

  • 角色权限表;

前三张表是一定要有的,后面两张关系表,可以放到前面表里面,作为一个字段存进去,但不建议,难以维护,查询也不方便。

在这里插入图片描述

搭建

分析完了,开始搭建这样一个框架。前面说了,这里不用Shiro、Spring Security、Sa-Token,这里介绍一个GitHub项目:

  • SpringBoot-Shiro-Vue

最初这位大佬应该是想做一个Shiro的Demo,做到最后发现没有Shiro也能实现基于角色的权限验证,就把Shiro依赖去掉了。我是无意中找到的,发现还不错,如果想搭建一个这样的权限验证框架,这个就可以了。简单的,就是好的。

首先,把项目Clone下来,打开,如下:

在这里插入图片描述

介绍

接着来介绍这个项目是如何实现RBAC的,RBAC要解决的是下面几个问题:

  • 当前用户的权限如何存储?

  • 如何实现当前用户对接口级别的权限校验?

  • 权限校验怎么实现?

  • ……

这是我能想到的几个问题?分析一下,

第一个问题是怎么存储用户的权限信息,可以存在数据库里,每次请求访问接口时,去查数据库,拿到这个用户的所有权限,但是这样效率低,访问数据库过于频繁,可以考虑存入到ThreadLocal、Caffine等本地缓存里(能存到Redis里吗?可以思考一下);

第二个问题是如何实现权限校验,可以像Spring Security那样,使用拦截器,在访问接口前先拦截下来,然后获取当前用户的所有接口权限,拿到可访问的接口列表,然后加以判断,看当前要访问的接口地址是否在这里面,不在就返回没有权限;

第三个问题是权限校验怎么实现,像我前面说的那样把用户可访问的所有接口地址拿出来,然后校验,也是一种方法,但最好的方法是用AOP+自定义注解,在Controller层的各个接口上打上注解,限定这个接口隶属哪个角色的哪个权限,然后在AOP里面去拿到当前用户的权限列表,看是否有这个权限。

我们来看下,这个项目是怎么做的。


登录

登录,校验用户名、密码,返回Token的同时,将当前用户信息(包括角色、权限)存入到Caffeine中,

(Controller)

    /*** 登录*/@PostMapping("/auth")public JSONObject authLogin(@RequestBody JSONObject requestJson) {CommonUtil.hasAllRequired(requestJson, "username,password");return loginService.authLogin(requestJson);}

(Service)

    /*** 登录表单提交*/public JSONObject authLogin(JSONObject jsonObject) {String username = jsonObject.getString("username");String password = jsonObject.getString("password");JSONObject info = new JSONObject();JSONObject user = loginDao.checkUser(username, password);if (user == null) {throw new CommonJsonException(ErrorEnum.E_10010);}String token = tokenService.generateToken(username);info.put("token", token);return CommonUtil.successJson(info);}

(校验SQL,就是单纯的根据用户名、密码查询,看是否有这条记录,非常简易,这个正式环境需要改造一下)

    <select id="checkUser" resultType="com.alibaba.fastjson.JSONObject">SELECT u.id userIdFROM sys_user uWHERE u.username = #{username}AND u.password = #{password}AND u.delete_status = '1'</select>

(发Token,同时将当前用户信息存入到本地缓存里,key是当前用户的Token)

    /*** 用户登录验证通过后(sso/帐密),生成token,记录用户已登录的状态*/public String generateToken(String username) {MDC.put("username", username);String token = UUID.randomUUID().toString().replace("-", "").substring(0, 20);//设置用户信息缓存setCache(token, username);return token;}

(setCache()方法,查找当前用户信息,存入本地缓存)

    @AutowiredCache<String, SessionUserInfo> cacheMap;/*** 将用户的信息以 token==SessionUser存入到本地缓存中* @param token* @param username*/private void setCache(String token, String username) {SessionUserInfo info = getUserInfoByUsername(username);log.info("设置用户信息缓存:token={} , username={}, info={}", token, username, info);cacheMap.put(token, info);}

(getUserInfoByUsername()方法,根据用户名查找用户信息)

    /*** 根据用户名查询用户信息,包括角色、权限列表* @param username* @return*/private SessionUserInfo getUserInfoByUsername(String username) {SessionUserInfo userInfo = loginDao.getUserInfo(username);if (userInfo.getRoleIds().contains(1)) {//管理员,查出全部按钮和权限码userInfo.setMenuList(loginDao.getAllMenu());userInfo.setPermissionList(loginDao.getAllPermissionCode());}return userInfo;}

(getUserInfo()方法,查找当前用户信息的SQL,可以看到关联到了前面提到的五张表)

    <select id="getUserInfo" resultMap="userInfo">SELECT u.id              userId,u.username,u.nickname,ur.role_id        roleId,p.menu_code       menuCode,p.permission_code permissionCodeFROM sys_user uLEFT JOIN sys_user_role ur on u.id = ur.user_idLEFT JOIN sys_role r ON r.id = ur.role_idLEFT JOIN sys_role_permission rp ON r.id = rp.role_idLEFT JOIN sys_permission p ON rp.permission_id = p.id AND rp.delete_status = '1'WHERE u.username = #{username}AND u.delete_status = '1'</select>

校验

在看下校验是如何实现的,除了登录、登出等几个接口没有权限,其他用户操作、业务操作的接口上都加了自定义注解,如下:

package com.heeexy.example.controller;import com.alibaba.fastjson.JSONObject;
import com.heeexy.example.config.annotation.Logical;
import com.heeexy.example.config.annotation.RequiresPermissions;
import com.heeexy.example.service.UserService;
import com.heeexy.example.util.CommonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;/*** @author: heeexy* @description: 用户/角色/权限相关controller* @date: 2017/11/2 10:19*/
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;/*** 查询用户列表*/@RequiresPermissions("user:list")@GetMapping("/list")public JSONObject listUser(HttpServletRequest request) {return userService.listUser(CommonUtil.request2Json(request));}@RequiresPermissions("user:add")@PostMapping("/addUser")public JSONObject addUser(@RequestBody JSONObject requestJson) {CommonUtil.hasAllRequired(requestJson, "username, password, nickname, roleIds");return userService.addUser(requestJson);}@RequiresPermissions("user:update")@PostMapping("/updateUser")public JSONObject updateUser(@RequestBody JSONObject requestJson) {CommonUtil.hasAllRequired(requestJson, " nickname, roleIds, deleteStatus, userId");return userService.updateUser(requestJson);}@RequiresPermissions(value = {"user:add", "user:update"}, logical = Logical.OR)@GetMapping("/getAllRoles")public JSONObject getAllRoles() {return userService.getAllRoles();}/*** 角色列表*/@RequiresPermissions("role:list")@GetMapping("/listRole")public JSONObject listRole() {return userService.listRole();}/*** 查询所有权限, 给角色分配权限时调用*/@RequiresPermissions("role:list")@GetMapping("/listAllPermission")public JSONObject listAllPermission() {return userService.listAllPermission();}/*** 新增角色*/@RequiresPermissions("role:add")@PostMapping("/addRole")public JSONObject addRole(@RequestBody JSONObject requestJson) {CommonUtil.hasAllRequired(requestJson, "roleName,permissions");return userService.addRole(requestJson);}/*** 修改角色*/@RequiresPermissions("role:update")@PostMapping("/updateRole")public JSONObject updateRole(@RequestBody JSONObject requestJson) {CommonUtil.hasAllRequired(requestJson, "roleId,roleName,permissions");return userService.updateRole(requestJson);}/*** 删除角色*/@RequiresPermissions("role:delete")@PostMapping("/deleteRole")public JSONObject deleteRole(@RequestBody JSONObject requestJson) {CommonUtil.hasAllRequired(requestJson, "roleId");return userService.deleteRole(requestJson);}
}

如查看用户列表接口上加的注解,如下:

    @RequiresPermissions(value = {"user:add", "user:update"}, logical = Logical.OR)

表示的是访问此接口需要拥有添加用户或者更新用户权限,后面的logical = Logical.OR也是自定义的

然后看AOP里面是怎么实现的,这是RBAC的精华,如下:

package com.heeexy.example.config.filter;import com.heeexy.example.config.annotation.Logical;
import com.heeexy.example.config.annotation.RequiresPermissions;
import com.heeexy.example.config.exception.UnauthorizedException;
import com.heeexy.example.dto.session.SessionUserInfo;
import com.heeexy.example.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;import java.util.Arrays;
import java.util.Set;/*** @author heeexy* @description: [角色权限]控制拦截器*/
@Aspect
@Slf4j
@Component
@Order(3)
public class PermissionAspect {@AutowiredTokenService tokenService;@Before("@annotation(com.heeexy.example.config.annotation.RequiresPermissions)")public void before(JoinPoint joinPoint) {log.debug("开始校验[操作权限]");SessionUserInfo userInfo = tokenService.getUserInfo();Set<String> myCodes = userInfo.getPermissionList();Signature signature = joinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;RequiresPermissions a = methodSignature.getMethod().getAnnotation(RequiresPermissions.class);String[] perms = a.value();log.debug("校验权限code: {}", Arrays.toString(perms));log.debug("用户已有权限: {}", myCodes);//5.对比[要求]的code和[用户实际拥有]的codeif (a.logical() == Logical.AND) {//必须包含要求的每个权限for (String perm : perms) {if (!myCodes.contains(perm)) {log.warn("用户缺少权限 code : {}", perm);throw new UnauthorizedException();//抛出[权限不足]的异常}}} else {//多个权限只需包含其中一种即可boolean flag = false;for (String perm : perms) {if (myCodes.contains(perm)) {flag = true;break;}}if (!flag) {log.warn("用户缺少权限 code= : {} (任意有一种即可)", Arrays.toString(perms));throw new UnauthorizedException();//抛出[权限不足]的异常}}}
}

这里的实现也很容易理解,先从本地缓存中取出当前用户的权限列表,然后再取出接口上面的权限列表,和逻辑运算符(AND还是OR),加以判断,看当前用户是否包含了这个接口所需要的权限。

以上,就是这个项目对RBAC的实现,没有用到第三方权限验证框架,短小精悍,很有启发。如果你需要搭建这样一个登录框架,可以看看这个项目,删删减减就可以拿来用了。另外,作者还提供了配套的前端Vue项目,也可以部署看看。

总结

本文介绍了Github作者Heeexy的SpringBoot-Shiro-Vue项目,可以快速搭建一个基于角色的权限验证框架。

相关文章:

搭建一个基于角色的权限验证框架

说明&#xff1a;基于角色的权限验证&#xff08;Role-Based Access Control&#xff0c;RBAC&#xff09;框架&#xff0c;是目前大多数服务端的登录校验框架。本文介绍如何快速搭建一个这样的框架&#xff0c;不用Shiro、Spring Security、Sa-Token这样的“大框架”实现。 R…...

下载chromedriver驱动

首先进入关于ChromeDriver最新下载地址&#xff1a;Chrome for Testing availability 进入之后找到与自己所匹配的&#xff0c;在浏览器中查看版本号&#xff0c;下载版本号需要一致。 下载即可&#xff0c;解压&#xff0c;找到 直接放在pycharm下即可 因为在环境变量中早已配…...

在STM32工程中使用Mavlink与飞控通信

本文讲述如何在STM32工程中使用Mavlink协议与飞控通信&#xff0c;特别适合自制飞控外设模块的项目。 需求来源&#xff1a; 1、增稳云台里的STM32单片机需要通过串口接收飞控传来的云台俯仰、横滚控制指令和相机拍照控制指令&#xff1b; 2、自制的有害气体采集器需要接收飞…...

【Elasticsearch】-7.17.24版本接入

官网 https://www.elastic.co/cn/downloads/elasticsearch 本项目基于windows环境下&#xff0c;其他环境操作类似 1、初始化配置 打开config/elasticsearch.yaml 添加如下配置 cluster.name: dams_clusternetwork.host: 127.0.0.1 http.port: 9200# 不开启geo数据库 inge…...

ShouldSniffAttr在自动化测试中具体是如何应用?

在自动化测试中&#xff0c;ShouldSniffAttr 这样的函数名通常暗示它是一个用于断言&#xff08;assertions&#xff09;的工具&#xff0c;用于检查某个元素或属性是否符合预期的条件。 虽然这不是一个标准的函数名&#xff0c;但我们可以根据命名推测其用途。 例如&#xf…...

前端vue3打印,多页打印,不使用插件(工作中让我写一个打印功能)

说下总体思路&#xff0c;创建一个组件&#xff0c;里面放多个span字段&#xff0c;然后根据父组件传入的参数&#xff0c;生成子组件&#xff0c;最好我们打印子组件的信息即可。通过我多次ai&#xff0c;探索最后成功了。 子组件代码 media print 这个我要讲一下&#xff…...

传感技术是如何实现实时监测和控制的呢

传感技术在力士乐拧紧系统中实现实时监测和控制的方式主要通过以下几个步骤进行&#xff1a; 一、传感器数据采集 1. 传感器种类&#xff1a; 力士乐拧紧系统中可能包含多种传感器&#xff0c;如力矩传感器、角度传感器和转速传感器等。这些传感器各自负责检测拧紧过程中的不…...

为什么mac打不开rar文件 苹果电脑打不开rar压缩文件怎么办

你是否遇到过这样的情况&#xff0c;下载了一个rar文件&#xff0c;想要查看里面的内容&#xff0c;却发现Mac电脑无法打开。rar文件是一种常见的压缩文件格式&#xff0c;它可以将多个文件或文件夹压缩成一个文件&#xff0c;节省空间和传输时间。如此高效实用的压缩文档&…...

linux下日志系统setvbuf接口及结构体 handle_file_t成员介绍

typedef struct handle_file_t {uint8_t *wkey;//用于存储写入文件时可能需要的加密密钥int cflag;//用于表示日志文件的某些配置标志&#xff0c;例如是否启用压缩、是否启用加密等char *file_path;//用于存储日志文件的路径FILE *…...

ESP8266+httpServer+GET+POST实现网页验证密码

1. 代码 #include "esp_http_server.h" #include "esp_log.h" #include "web_server.h"// 辅助宏&#xff0c;用于计算两个数中的较小值 #define MIN(a, b) ((a) < (b) ? (a) : (b))static const char *TAG "wifi web_server";c…...

git仓库修改ip,本地代码修改

只需求修改本地项目下面的.git文件夹下的config 替换ip即可...

轻便灵活,声学卓越,流动会场创新应用—轻空间

随着现代社会对高效、灵活场地需求的日益增加&#xff0c;传统建筑场馆的局限性逐渐显现。无论是大型会议、临时展览&#xff0c;还是文化活动&#xff0c;企业与组织往往需要一个既能快速搭建&#xff0c;又具备顶级声学效果的多功能场所。由轻空间打造的流动会场应运而生&…...

13 Midjourney从零到商用·进阶篇:灯光、角度与风格等精细控制方法

在前面我们了解了提示词的书写&#xff0c;那么如何利用提示词来精确控制生成画面的灯光、角度与风格 呢&#xff1f;在本篇文章中我么一起来探讨一下。 一、灯光 在摄影中&#xff0c;对灯光的要求都是非常高的。灯光能对人物、动物、物体、场景等进行修饰。每一种微小的的灯光…...

为什么要把raw转成yuv

将RAW图像数据转换为YUV格式在图像处理和视频编解码领域有多个重要的原因。以下是一些主要原因&#xff1a; 1. 标准化和兼容性 视频编解码标准&#xff1a;YUV格式是许多视频编解码标准&#xff08;如H.264、H.265等&#xff09;所使用的颜色空间。将RAW数据转换为YUV可以使…...

基于双向RRT算法的三维空间最优路线规划matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 4.1 单向RRT算法 4.2 双向RRT算法 5.完整程序 1.程序功能描述 基于双向RRT&#xff08;Randomly Exploring Random Trees, 随机探索随机树&#xff09;算法的三维空间最优路径规划是一种解…...

若依-原理

1.代码生成器 1.1源码分析 代码生成器分为两个部分&#xff1a; 第一部分涉及将业务表结构导入到系统中 第二部分是点击生成按钮&#xff0c;系统将根据表结构生成相应的前后端代码&#xff0c;并提供下载。 1.表结构说明 gen_table&#xff1a;存储业务表的基本信息 &am…...

台球厅灯控系统如何布线 佳易王桌球计时计费管理系统操作教程

一、前言 台球厅灯控系统如何布线 佳易王桌球计时计费管理系统操作教程 佳易王台球灯控系统可外接灯控设备&#xff0c;用软件来控制灯的开关 开始计时的时候灯点亮&#xff0c;结账后灯自动关闭 二、计时灯控电路图 佳易王计时计费软件配套的灯控设备布线图&#xff0c;如上…...

安卓将本地日志上传到服务器

在安卓开发中&#xff0c;将本地日志上传到服务器是一个常见的需求&#xff0c;特别是在开发需要远程监控或调试的应用时。以下是一个基本的步骤和示例&#xff0c;说明如何实现这一功能&#xff1a; 1 本地日志上传到服务器 1.1 准备服务器 首先&#xff0c;你需要在服务器…...

FloodFill(洪水灌溉)算法专题——DFS深搜篇

目录 1、图像渲染 1.1 算法原理 1.2 算法代码 2、岛屿数量 2.1 算法原理 2.2 算法代码 3、岛屿的最大面积 3.1 算法原理 3.2 算法代码 4、被围绕的区域 4.1 算法原理 4.2 算法代码 5、太平洋大西洋水流问题 5.1 算法原理 5.2 算法代码 6、扫雷游戏 6.1 算法原理…...

直播标准权威发布,阿里云RTS获首批卓越级评估认证

近期举办的2024“可信云大会”上&#xff0c;中国信通院正式发布了2024年上半年音视频领域最新评估结果。阿里云超低延时直播&#xff0c;以首批卓越级&#xff0c;通过中国信通院超低延时直播性能及服务质量分级测试。 标准发布&#xff0c;权威量化直播体验质量 从直播元年发…...

第19节 Node.js Express 框架

Express 是一个为Node.js设计的web开发框架&#xff0c;它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用&#xff0c;和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...

国防科技大学计算机基础课程笔记02信息编码

1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制&#xff0c;因此这个了16进制的数据既可以翻译成为这个机器码&#xff0c;也可以翻译成为这个国标码&#xff0c;所以这个时候很容易会出现这个歧义的情况&#xff1b; 因此&#xff0c;我们的这个国…...

PHP和Node.js哪个更爽?

先说结论&#xff0c;rust完胜。 php&#xff1a;laravel&#xff0c;swoole&#xff0c;webman&#xff0c;最开始在苏宁的时候写了几年php&#xff0c;当时觉得php真的是世界上最好的语言&#xff0c;因为当初活在舒适圈里&#xff0c;不愿意跳出来&#xff0c;就好比当初活在…...

条件运算符

C中的三目运算符&#xff08;也称条件运算符&#xff0c;英文&#xff1a;ternary operator&#xff09;是一种简洁的条件选择语句&#xff0c;语法如下&#xff1a; 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true&#xff0c;则整个表达式的结果为“表达式1”…...

高等数学(下)题型笔记(八)空间解析几何与向量代数

目录 0 前言 1 向量的点乘 1.1 基本公式 1.2 例题 2 向量的叉乘 2.1 基础知识 2.2 例题 3 空间平面方程 3.1 基础知识 3.2 例题 4 空间直线方程 4.1 基础知识 4.2 例题 5 旋转曲面及其方程 5.1 基础知识 5.2 例题 6 空间曲面的法线与切平面 6.1 基础知识 6.2…...

Linux-07 ubuntu 的 chrome 启动不了

文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了&#xff0c;报错如下四、启动不了&#xff0c;解决如下 总结 问题原因 在应用中可以看到chrome&#xff0c;但是打不开(说明&#xff1a;原来的ubuntu系统出问题了&#xff0c;这个是备用的硬盘&a…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计

随着大语言模型&#xff08;LLM&#xff09;参数规模的增长&#xff0c;推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长&#xff0c;而KV缓存的内存消耗可能高达数十GB&#xff08;例如Llama2-7B处理100K token时需50GB内存&a…...

【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的“no matching...“系列算法协商失败问题

【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的"no matching..."系列算法协商失败问题 摘要&#xff1a; 近期&#xff0c;在使用较新版本的OpenSSH客户端连接老旧SSH服务器时&#xff0c;会遇到 "no matching key exchange method found"​, "n…...

MySQL 8.0 事务全面讲解

以下是一个结合两次回答的 MySQL 8.0 事务全面讲解&#xff0c;涵盖了事务的核心概念、操作示例、失败回滚、隔离级别、事务性 DDL 和 XA 事务等内容&#xff0c;并修正了查看隔离级别的命令。 MySQL 8.0 事务全面讲解 一、事务的核心概念&#xff08;ACID&#xff09; 事务是…...