当前位置: 首页 > 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项目,可以快速搭建一个基于角色的权限验证框架。


http://www.mrgr.cn/news/29360.html

相关文章:

  • 使用 Grafana api 查询 Datasource 数据
  • Hbase Shell
  • 开源 2 + 1 链动模式、AI 智能名片、S2B2C 商城小程序在用户留存与品牌发展中的应用研究
  • 【MinIO】Python 运用 MinIO 实现简易文件系统
  • AndroidStudio-常用布局
  • 秃姐学AI系列之:样式迁移 + 代码实现
  • Qt快捷键说明与用法
  • 【大模型技术教程】FastGPT一站式解决方案[1-部署篇]:轻松实现RAG-智能问答系统
  • 网络安全中GET和POST区别在哪?
  • 虚拟私有云VPC详解和设置方法
  • 抖音生活服务常见玩法及收益情况详解!普通人如何把握机会?
  • Prometheus监控k8s环境构建
  • 图像超分辨率技术代码分享
  • 第14章 存储器的保护
  • 直流电源纹波噪声的测量
  • 基于yolov8的舌苔识别检测系统python源码+onnx模型+评估指标曲线+精美GUI界面
  • 阳光乳业业绩下滑:核心业务承压显著,区域之困如何突围?
  • mac提示“文件已损坏“的处理方法
  • 微信小程序跳转路径的方式
  • 【github】.gitignore不能忽略一些文件的时候
  • redis 在企业开发实践中注意事项
  • 激发组织数字化转型活力,开启发展新征程
  • 一、Numpy使用
  • 工控自动化人必知的7种PLC通讯协议!
  • 从数据仓库到数据中台再到数据飞轮:我了解的数据技术进化史
  • SpringBoot如何在使用MongoRepository时启用@Created