搭建一个基于角色的权限验证框架
说明:基于角色的权限验证(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项目,可以快速搭建一个基于角色的权限验证框架。
相关文章:
搭建一个基于角色的权限验证框架
说明:基于角色的权限验证(Role-Based Access Control,RBAC)框架,是目前大多数服务端的登录校验框架。本文介绍如何快速搭建一个这样的框架,不用Shiro、Spring Security、Sa-Token这样的“大框架”实现。 R…...
下载chromedriver驱动
首先进入关于ChromeDriver最新下载地址:Chrome for Testing availability 进入之后找到与自己所匹配的,在浏览器中查看版本号,下载版本号需要一致。 下载即可,解压,找到 直接放在pycharm下即可 因为在环境变量中早已配…...
在STM32工程中使用Mavlink与飞控通信
本文讲述如何在STM32工程中使用Mavlink协议与飞控通信,特别适合自制飞控外设模块的项目。 需求来源: 1、增稳云台里的STM32单片机需要通过串口接收飞控传来的云台俯仰、横滚控制指令和相机拍照控制指令; 2、自制的有害气体采集器需要接收飞…...
【Elasticsearch】-7.17.24版本接入
官网 https://www.elastic.co/cn/downloads/elasticsearch 本项目基于windows环境下,其他环境操作类似 1、初始化配置 打开config/elasticsearch.yaml 添加如下配置 cluster.name: dams_clusternetwork.host: 127.0.0.1 http.port: 9200# 不开启geo数据库 inge…...
ShouldSniffAttr在自动化测试中具体是如何应用?
在自动化测试中,ShouldSniffAttr 这样的函数名通常暗示它是一个用于断言(assertions)的工具,用于检查某个元素或属性是否符合预期的条件。 虽然这不是一个标准的函数名,但我们可以根据命名推测其用途。 例如…...
前端vue3打印,多页打印,不使用插件(工作中让我写一个打印功能)
说下总体思路,创建一个组件,里面放多个span字段,然后根据父组件传入的参数,生成子组件,最好我们打印子组件的信息即可。通过我多次ai,探索最后成功了。 子组件代码 media print 这个我要讲一下ÿ…...
传感技术是如何实现实时监测和控制的呢
传感技术在力士乐拧紧系统中实现实时监测和控制的方式主要通过以下几个步骤进行: 一、传感器数据采集 1. 传感器种类: 力士乐拧紧系统中可能包含多种传感器,如力矩传感器、角度传感器和转速传感器等。这些传感器各自负责检测拧紧过程中的不…...
为什么mac打不开rar文件 苹果电脑打不开rar压缩文件怎么办
你是否遇到过这样的情况,下载了一个rar文件,想要查看里面的内容,却发现Mac电脑无法打开。rar文件是一种常见的压缩文件格式,它可以将多个文件或文件夹压缩成一个文件,节省空间和传输时间。如此高效实用的压缩文档&…...
linux下日志系统setvbuf接口及结构体 handle_file_t成员介绍
typedef struct handle_file_t {uint8_t *wkey;//用于存储写入文件时可能需要的加密密钥int cflag;//用于表示日志文件的某些配置标志,例如是否启用压缩、是否启用加密等char *file_path;//用于存储日志文件的路径FILE *…...
ESP8266+httpServer+GET+POST实现网页验证密码
1. 代码 #include "esp_http_server.h" #include "esp_log.h" #include "web_server.h"// 辅助宏,用于计算两个数中的较小值 #define MIN(a, b) ((a) < (b) ? (a) : (b))static const char *TAG "wifi web_server";c…...
git仓库修改ip,本地代码修改
只需求修改本地项目下面的.git文件夹下的config 替换ip即可...
轻便灵活,声学卓越,流动会场创新应用—轻空间
随着现代社会对高效、灵活场地需求的日益增加,传统建筑场馆的局限性逐渐显现。无论是大型会议、临时展览,还是文化活动,企业与组织往往需要一个既能快速搭建,又具备顶级声学效果的多功能场所。由轻空间打造的流动会场应运而生&…...
13 Midjourney从零到商用·进阶篇:灯光、角度与风格等精细控制方法
在前面我们了解了提示词的书写,那么如何利用提示词来精确控制生成画面的灯光、角度与风格 呢?在本篇文章中我么一起来探讨一下。 一、灯光 在摄影中,对灯光的要求都是非常高的。灯光能对人物、动物、物体、场景等进行修饰。每一种微小的的灯光…...
为什么要把raw转成yuv
将RAW图像数据转换为YUV格式在图像处理和视频编解码领域有多个重要的原因。以下是一些主要原因: 1. 标准化和兼容性 视频编解码标准:YUV格式是许多视频编解码标准(如H.264、H.265等)所使用的颜色空间。将RAW数据转换为YUV可以使…...
基于双向RRT算法的三维空间最优路线规划matlab仿真
目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 4.1 单向RRT算法 4.2 双向RRT算法 5.完整程序 1.程序功能描述 基于双向RRT(Randomly Exploring Random Trees, 随机探索随机树)算法的三维空间最优路径规划是一种解…...
若依-原理
1.代码生成器 1.1源码分析 代码生成器分为两个部分: 第一部分涉及将业务表结构导入到系统中 第二部分是点击生成按钮,系统将根据表结构生成相应的前后端代码,并提供下载。 1.表结构说明 gen_table:存储业务表的基本信息 &am…...
台球厅灯控系统如何布线 佳易王桌球计时计费管理系统操作教程
一、前言 台球厅灯控系统如何布线 佳易王桌球计时计费管理系统操作教程 佳易王台球灯控系统可外接灯控设备,用软件来控制灯的开关 开始计时的时候灯点亮,结账后灯自动关闭 二、计时灯控电路图 佳易王计时计费软件配套的灯控设备布线图,如上…...
安卓将本地日志上传到服务器
在安卓开发中,将本地日志上传到服务器是一个常见的需求,特别是在开发需要远程监控或调试的应用时。以下是一个基本的步骤和示例,说明如何实现这一功能: 1 本地日志上传到服务器 1.1 准备服务器 首先,你需要在服务器…...
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“可信云大会”上,中国信通院正式发布了2024年上半年音视频领域最新评估结果。阿里云超低延时直播,以首批卓越级,通过中国信通院超低延时直播性能及服务质量分级测试。 标准发布,权威量化直播体验质量 从直播元年发…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
mongodb源码分析session执行handleRequest命令find过程
mongo/transport/service_state_machine.cpp已经分析startSession创建ASIOSession过程,并且验证connection是否超过限制ASIOSession和connection是循环接受客户端命令,把数据流转换成Message,状态转变流程是:State::Created 》 St…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
STM32+rt-thread判断是否联网
一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...
Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)
目录 1.TCP的连接管理机制(1)三次握手①握手过程②对握手过程的理解 (2)四次挥手(3)握手和挥手的触发(4)状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...
连锁超市冷库节能解决方案:如何实现超市降本增效
在连锁超市冷库运营中,高能耗、设备损耗快、人工管理低效等问题长期困扰企业。御控冷库节能解决方案通过智能控制化霜、按需化霜、实时监控、故障诊断、自动预警、远程控制开关六大核心技术,实现年省电费15%-60%,且不改动原有装备、安装快捷、…...
深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...
srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
