当前位置: 首页 > 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;权威量化直播体验质量 从直播元年发…...

iOS 知识点记录

王巍 博客地址:OneVs Den git地址:onevcat (Wei Wang) GitHub 江湖人称喵神,目前就职于line。喵神的博客涉及方面比较广, 有Obejctive-C, Swift, SwiftUI, Unity等等。博客内容很有深度,非常值得关注。 戴铭 博客地址:戴铭的博客 git地址:ming1016 (戴铭) GitHub 《i…...

C++系列-STL中搜索相关算法

STL中search相关算法 &#x1f4a2;search相关算法&#x1f4a2;&#x1f4a2;search算法举例&#x1f4a2;&#x1f4a2;search_n算法举例&#x1f4a2;&#x1f4a2;binary_search算法举例 &#x1f4a2; lower_bound&#x1f4a2; upper_bound&#x1f4a2; lower_bound和up…...

5.C++程序中的注释

我们来看上节所写的程序 #include <iostream> using namespace std;void prnt() //打印A {cout << "printA" << endl; }int main() {prnt();return 0; } 上面的程序中“//打印A”&#xff0c;表示说明当前函数是打印内容的函数&#xff0c;具体…...

com.kingbase8.util.KSQLException: ERROR: permission denied for table xxx

前言 在信创改造中&#xff0c;数据库替换为国产数据库是不可缺少的一部分。而可替换选项中多数选项无非是人大金仓和达梦数据库二选一。本文将介绍人大金仓在使用过程的问题以及解决办法。 问题 在使用人大金仓数据库后&#xff0c;程序运行报错 com.kingbase8.util.KSQLEx…...

开发小程序

由于之前购入的阿里云ECS放着落灰&#xff0c;碰巧又看到个有趣的项目&#xff0c;于是就做了个生成头像的小程序…由于第一次完整发布小程序&#xff0c;记录一下遇到的问题 小程序名称&#xff1a;靓仔创意头像 &#x1f602; 关于小程序 接口请求&#xff0c;在开发过程中…...

JS考核答案

1.请简述var, let, const的区别&#xff1f; &#xff08;1&#xff09;块级作用域&#xff1a;块作用域由 { }包括&#xff0c;let和const具有块级作用域&#xff0c;var不存在块级作用域。块级作用域解决了ES5中的两个问题&#xff1a; 内层变量可能覆盖外层变量 用来计数的…...

高德地图2.0 绘制、编辑多边形覆盖物(电子围栏)

1. 安装 npm i amap/amap-jsapi-loader --save移步&#xff1a;官方文档 2. map组件封装 <script lang"ts" setup> import AMapLoader from amap/amap-jsapi-loader import { onMounted, ref } from vue import { propTypes } from /utils/propTypesdefineO…...

MySQL底层为什么选择用B+树作为索引

首先&#xff0c;我们来想想为什么这么多数据结构&#xff0c;为什么要用树这种数据结构&#xff1f; 众多的数据结构在逻辑层面可分为&#xff1a;线性结构 和 非线性结构。 线性结构有&#xff1a;数组、链表&#xff0c;基于它们衍生出的有哈希表&#xff08;哈希表也称散…...

MATLAB系列05:自定义函数

MATLAB系列05&#xff1a;自定义函数 5. 自定义函数5.1 MATLAB函数简介5.2 在MATLAB中传递变量&#xff1a;按值传递机制5.3 选择性参数5.4 用全局内存分享数据5.5 在函数两次调用之间本地数据的存储5.6 函数的函数(function functions)5.7 子函数和私有函数5.8 总结 5. 自定义…...

C++速通LeetCode简单第20题-多数元素

方法一&#xff1a;暴力解法&#xff0c;放multiset中排序&#xff0c;然后依次count统计&#xff0c;不满足条件的值erase清除。 class Solution { public:int majorityElement(vector<int>& nums) {int ans 0;multiset<int> s;for(int i 0;i < nums.s…...