自定义权限管理系统概述

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

今天我们来聊聊如何自定义一个简单易用的权限管理系统。

虽然市面上已经有很多出色的权限管理框架,比如Spring Security或者Shiro,但很多时候我们并不需要那么多功能,特别是Spring Security,学习成本太高了!我曾经有一位同事,为了做用户权限管理,硬是引入了当时大家都不太熟悉的Spring Security,然后整了一个多星期,合并代码后直接把整个组的接口都拦截了,我直接好家伙。关键是他也不知道哪里配错了,搞了很久还是没解决,最终只好回滚代码,耽误了大半天时间。

所以,技术选型绝对是一门学问,simple is better,不要过度设计,挑最趁手的干活即可。

说到一个系统的权限管理,大家肯定不陌生,可以用一句大白话概括为:你是谁,能访问什么页面(接口)。

我们可以从中抽象出三个要素:

  • 你:用户
  • 是谁:角色
  • 能访问什么:权限

也就是最经典的RBAC权限设计模型(Role-Based Access Control):

  • t_user
  • t_user_role
  • t_role
  • t_role_permission
  • t_permission

上面的关系,对于部分同学来说可能还有点复杂,我们由易到难逐步讨论。

登录拦截

最简单的设计方案其实是登录拦截:

  • 用户
  • 角色:游客和系统用户(只有两个角色)
  • 权限:部分接口需要登录才能访问

由于登录状态天然就能区分“游客”和“系统用户”两个角色,而这个两个角色拥有的权限是固定的:“系统用户”能访问部分受限接口,而“游客”只能访问开放接口。

此时,“用户-角色-权限”都是非常明确且不会变动的,不需要数据库存储三者之间的关系,只需要一张用户表即可,登录就能访问所有资源。

用户即角色

如果一个系统,可选的角色简单且数量固定,比如管理员、教师、学生、游客,并且未来也不会再有新的角色产生,此时也可以偷懒,直接在用户表里添加user_type字段:

`user_type` tinyint(1) unsigned NOT NULL COMMENT '用户类型 1-管理员 2-教师 3-学生 4-游客'

如果从RBAC的角度看,其实 用户表 = t_user + t_user_role + t_role,而且一个用户只能有一个角色,用户即角色。

此时,t_role_permission和t_permission还要不要呢?

答案是:可要可不要,我个人倾向于不要。

那么,没有表结构怎么表示对象关系呢?可以直接在代码中耦合。

比如,教师能访问新增课程的接口,那么我直接在代码里写死:

@PostMapping("/saveCourse")
public Result<Boolean> saveCourse(@RequestBody Course course) {
    // 权限校验
    User currentUser = (User) session.getAttribute(WebConstant.CURRENT_USER_IN_SESSION);
    if (!UserTypeEnum.TEACHER.getType().equals(currentUser.getUserType()) {
        throw new BizException(ExceptionCodeEnum.PERMISSION_ERROR);
    }
        
    return Result.success(courseService.saveCourse(course));
}

或者逼格高一点,使用注解:

@PermissionRequired(UserTypeEnum.TEACHER)
@PostMapping("/saveCourse")
public Result<Boolean> saveCourse(@RequestBody Course course) {    
    return Result.success(courseService.saveCourse(course));
}

在“够用”的前提下,这种方案最大的好处就是:直观且编码简单(和登录拦截相比只是用户表多了一个user_type,拦截逻辑改改即可)。

之所以能这么设计,还是因为需求允许。

我们来看一下RBAC五张表在当前模式中是什么情况。

用户表t_user肯定不能删,保留着。但由于用户只能有一种角色:

  • 把t_role表的数据移到代码中,用UserTypeEnum表示(反正角色就几个,就没必要存表里),所以t_role可以删除
  • 在t_user中新增user_type字段,直接绑死t_user和t_role的对应关系,所以t_user_role也不需要了

用编码的方式手动指定“角色-权限”关系,消除了t_role_permission和t_permission:

  • @PermissionRequired(UserTypeEnum.ADMIN)注解的接口:只能管理员访问
  • @PermissionRequired(UserTypeEnum.TEACHER)注解的接口:只能教师访问
  • ...

所以此时RBAC只剩一张t_user表。

至于有些接口多种角色都能访问怎么办呢?

@PermissionRequired(value = {UserTypeEnum.ADMIN, UserTypeEnum.TEACHER}, logical = Logical.OR)
@PostMapping("/saveCourse")
public Result<Boolean> saveCourse(@RequestBody Course course) {    
    return Result.success(courseService.saveCourse(course));
}

相信大家都能看懂。

RBAC

但是很多时候,我们还是期望将系统做成可扩展的,此时就需要借助RBAC来搞一下了。

很多人对于RBAC其实挺陌生的,特别是非科班转行的同学,听到“RBAC”脑子全是糊的,可能也从来没搞懂过,只能支支吾吾说几句:我记得好像有什么用户表、角色表、权限表啥的...

我个人把实现RBAC的过程分为两部分:

  • RBAC五张表的增删改查(简单)
  • 基于五张表做权限控制(不简单)

表的增删改查

什么意思呢?相对设计促销活动、拉新活动什么的,RBAC的表设计及增删改查几乎是没有任何难度的:

t_user

id

name

password

create_time

update_time

deleted

1

bravo1988

123456

2021-02-14 15:28:12

2021-02-14 15:28:14

0

2

毛晓彤

654321

2021-02-14 15:28:32

2021-02-14 15:28:34

0

t_user_role

id

user_id

role_id

1

1

1

2

1

2

3

2

1

t_role

id

name

info

create_time

update_time

deleted

1

ADMIN

管理员

2021-02-14 15:28:12

2021-02-14 15:28:14

0

2

TEACHER

教师

2021-02-14 15:28:32

2021-02-14 15:28:34

0

3

STUDENT

学生

2021-02-14 15:28:32

2021-02-14 15:28:34

0

t_role_permission

id

role_id

permission_id

1

1

1

2

1

2

3

2

3

4

2

4

5

2

5

t_permission

id

module

name

create_time

update_time

deleted

1

用户

新增用户

2021-02-14 15:28:12

2021-02-14 15:28:14

0

2

用户

拉黑用户

2021-02-14 15:28:32

2021-02-14 15:28:34

0

3

课程

新增课程

2021-02-14 15:28:32

2021-02-14 15:28:34

0

4

课程

修改课程

2021-02-14 15:28:32

2021-02-14 15:28:34

0

5

课程

删除课程

2021-02-14 15:28:32

2021-02-14 15:28:34

0

这些表数据,都要提供对应的CRUD接口,但都很简单。t_user、t_role、t_permission就是单表CRUD就不说了,它们之间的关系维护也不难,以t_user_role为例:

@PermissionRequired(UserTypeEnum.ADMIN)
@PostMapping("/assignRoles")
public Result<Boolean> assignRoles(@RequestBody UserRoleDTO  userRoleDTO) {
    List<Long> roleIds =  userRoleDTO.getRoleIds();

    roleIds.forEach(roleId->{
        UserRole userRole = new UserRole();
        userRole.setUserId(user.getUserId());
        userRole.setRoleId(roleId);
        userRoleMapper.insert(userRole);
    });

    return Result.success(Boolean.TRUE);
}

@Data
static class UserRoleDTO {
    private Long userId;
    private List<Long> roleIds;
}

动态权限控制

RBAC真正的难点是如何基于这五张表动态地进行权限控制,核心是“动态”。比如,上面讲的在t_user表中增加user_type并直接使用注解的方案,其实属于硬编码。它的缺点是,如果运营告诉你,由于图书信息录入错误率较高,希望让学生也参与校正(对学生开放updateBook接口),你只能改代码,把接口上面的:

@PermissionRequired(value={UserTypeEnum.TEACHER})

改为:

@PermissionRequired(value={UserTypeEnum.TEACHER, UserTypeEnum.STUDENT}, logical = Logical.OR)

然后重新打包部署,如果你们公司没有做持续集成啥的,可能这个需求还要等到夜深人静没什么用户时才能停机重启...

所以RBAC最难的不是表的维护,而是权限控制,且重点是支持“动态权限分配”!

所谓“动态权限分配”是怎么回事呢?其实只要能解决本小节开头那个问题就算实现动态权限分配了。

一起来考虑实现思路吧~

让我们重新把关注点放在刚才的问题及解决办法上:我们通过修改@PermissionRequired的属性,让学生这个角色也能访问updateBook接口。

那么,不修改这个注解的属性可以吗?有没有其他办法呢?

不知道大家有没有意识到,现在@PermissionRequired这个注解代表的其实是角色(因为属性是UserType),而接口是资源,也就是权限,把代表角色的@PermissionRequired直接放在代表权限的接口上,从某种意义上来说,已经耦合了(当前资源只能被这个角色访问),所以后期要让这个资源被其他角色访问,就必须修改@PermissionRequired注解,也就必须修改代码。

有个“小聪明”可以解决这个问题:“用户-角色-权限”是两两关联的,“角色-权限”虽然被绑死了,但可以通过更改“用户-角色”来弥补。

把原本是学生的小李改为user_type=2,那么小李就从学生变为教师,接口没改,但是数据库表的关联关系已经变了,下次小李请求这个接口时,查询t_user_role得知小李是教师角色,所以小李也能改图书信息。

但这样做合适吗?

正确的处理办法是,@PermissionRequired不应该代表角色,而应该代表权限本身,如此一来代表资源的接口和代表权限的@PermissionRequired可以看做同一类事物,不存在硬编码绑死的问题(其实还可以优化,后面会提到)。至于如何动态更改接口访问权限,还是老办法,通过往中间表插入数据,让学生这个角色也拥有“编辑图书”的权限即可。

t_role

id

name

info

create_time

update_time

deleted

3

STUDENT

学生

2021-02-14 15:28:32

2021-02-14 15:28:34

0

t_role_permission(其他两张表都不用动,只修改中间表,分配权限即可)

id

role_id

permission_id

1

3

1

2

3

2

t_permission

id

module

name

create_time

update_time

deleted

1

课程

提交作业

2021-02-14 15:28:12

2021-02-14 15:28:14

0

2

课程

修改课程信息

2021-02-14 15:28:32

2021-02-14 15:28:34

0

正所谓磨刀不误砍柴工,希望这篇文章能帮大家理清一些基本的概念及思路,为后面的权限管理实战打好基础。部分同学对于RBAC望而却步,可能是因为之前看到过RBAC还涉及到树形结构等,看起来很复杂,有畏难情绪。其实没必要关心前端的展现形式,只要站在后端的角度提供好相应的接口接口。况且我们已经学过树形结构,前端要什么给什么,不带怕的。

后面几篇我们就按照:登录拦截、用户及角色、RBAC的顺序学习。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/261083.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Markdown语法 in Typora

Typora 是个好东西&#xff0c;如果不收费的话就更好了&#xff1b; Typora 破解 其实一直点击15天试用也是可以一直用一直用的&#xff1b; 数学公式 在Markdown扩展语法这里要选一下&#xff0c;才可以使用行内数学公式&#xff1b; 行内公式 $f(x) 2x^25x3$&#xff…

500平左右需要用建筑模板多少张?

为了计算500平方米&#xff08;㎡&#xff09;的建筑面积需要多少张模板&#xff0c;我们首先需要知道每张模板的面积。你提供了两种尺寸的模板&#xff1a;915毫米 x 1830毫米 和 1220毫米 x 2440毫米。我们先将这些尺寸从毫米转换为米&#xff0c;然后计算每张模板的面积&…

springboot整合rabbitmq附源码

前提是对rabbitmq有一定的了解&#xff0c;比如虚拟主机&#xff0c;交换机&#xff0c;队列&#xff0c;信道&#xff0c;绑定&#xff0c;路由键&#xff0c;direct&#xff0c;fanout&#xff0c;topic等 我使用的是docker部署的rabbitmq&#xff0c;看到简书的这个&#x…

如何通过宝塔面板搭建一个MySQL数据库服务并实现无公网ip远程访问?

文章目录 前言1.Mysql服务安装2.创建数据库3.安装cpolar3.2 创建HTTP隧道 4.远程连接5.固定TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址 前言 宝塔面板的简易操作性,使得运维难度降低,简化了Linux命令行进行繁琐的配置,下面简单几步,通过宝塔面板cp…

WEB渗透—PHP反序列化(五)

Web渗透—PHP反序列化 课程学习分享&#xff08;课程非本人制作&#xff0c;仅提供学习分享&#xff09; 靶场下载地址&#xff1a;GitHub - mcc0624/php_ser_Class: php反序列化靶场课程&#xff0c;基于课程制作的靶场 课程地址&#xff1a;PHP反序列化漏洞学习_哔哩…

69 排序高手

数据以字符串形式输入&#xff0c;期间转到数组内 #include <iostream> #include <string> #include <vector>using namespace::std; using std::cout; using std::cin; int pxgs(vector<int>& nums) {int nnums.size();int l0,rn-1;while(l<…

博客3万访问量了……

博客有3万访问量了呢。自从第一次用了赠送的1500的流量券&#xff0c;粉丝了从零突破了&#xff0c;到现在有150个粉丝了。 之前预想的写博客的初衷&#xff0c;也是记录自己的学习过程&#xff0c;毕竟好记忆不如烂笔头&#xff0c;记录下来就是长长久久的&#xff0c;随时可以…

Java-Security-1

JWT详解​​​​​​​ 1、SpringSecurity 1.1 简介 Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架 Shiro &#xff0c;它提供了更丰富的功能&#xff0c;社区资源也比 Shiro 丰富。 一般来说中大型的项目都是使用 SpringSecurity 来做安全框…

金蝶EAS打印凭证,数据量多点的就会出错

金蝶EAS打印凭证&#xff0c;数据量多点的就会出错&#xff0c;约过100页&#xff0c;提示数据源有问题 经咨询工程师需修改java虚拟机内存。 打开eas客户端目录&#xff0c;运行set-url.bat 看到原来java虚拟机只配置了512M内存&#xff0c;把虚拟机内存修改为4096&#xff0…

基于SSM的学生在线考试系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

sql学习笔记(四)

1. 避免字段名与关键字冲突 1️⃣ 2️⃣ as 设置别名 2. 同比与环比 「同比」&#xff1a;与历史「同时期&#xff3d;比较&#xff0c;例如2011年3月份与2010年3月份相比&#xff0c;叫「同比」。 「环比」&#xff1a;与「上一个」统计周期比较&#xff0c;例如2011年4…

Postman测试文件上传接口

文章目录 一、Postman测试文件上传接口1、创建一个请求→选择请求方式→填写请求的URL→填写请求的参数值2、选择 "Body" → "form-data" → "Test" → "File"3、点击 "Select Files"4、选择要上传的文件5、点击 Send 进行测…

10 Vue3中v-html指令的用法

概述 v-html主要是用来渲染富文本内容&#xff0c;比如评论信息&#xff0c;新闻信息&#xff0c;文章信息等。 v-html是一个特别不安全的指令&#xff0c;因为它会将文本以HTML的显示进行渲染&#xff0c;一旦文本里面包含一些恶意的js代码&#xff0c;可能会导致整个网页发…

Unresolved plugin: ‘org.apache.maven.plugins‘解决报错

新建springboot项目报Unresolved plugin: ‘org.apache.maven.plugins:maven-surefire-plugin:3.1.2’ 缺什么插件 引入什么插件的依赖就行 <dependency><groupId>org.apache.maven.plugins</groupId><artifactId>maven-install-plugin</artifact…

设计模式03结构型模式

结构型模式 参考网课:黑马程序员Java设计模式详解 博客笔记 https://zgtsky.top/ 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式&#xff0c;前者采用继承机制来组织接口和类&#xff0c;后者釆用组合或聚合来组合对象。 由于…

【Unity】【WebRTC】如何用Unity而不是浏览器接收远程画面

【背景】 之前几篇我们讨论了如何设置信令服务器&#xff0c;如何发送画面给远端以及如何用浏览器查看同步画面&#xff0c;今天来讨论如何实现Unity内部接收画面。 看本篇之前请先看过之前将web服务器设置和基本远程画面功能的几篇博文。&#xff08;同专栏下查看&#xff09…

链接未来:深入理解链表数据结构(一.c语言实现无头单向非循环链表)

在上一篇文章中&#xff0c;我们探索了顺序表这一基础的数据结构&#xff0c;它提供了一种有序存储数据的方法&#xff0c;使得数据的访 问和操作变得更加高效。想要进一步了解&#xff0c;大家可以移步于上一篇文章&#xff1a;探索顺序表&#xff1a;数据结构中的秩序之美 今…

springboot整合kafka附源码

前提&#xff1a;确保kafka环境 我使用的方案是docker 我使用的镜像为&#xff1a;wurstmeister/kafka 我使用的镜像为&#xff1a;wurstmeister/zookeeper docker安装kafka和zk教程&#xff1a;点这里手把手教你使用Docker搭建kafka【详细教程】 使用kafka前&#xff0c;要确…

字符串函数内存函数(从零到一)【C语言】

长度不受限制的字符串函数&#xff1a;strcpy,strcat,strcmp 长度受限制的字符串函数&#xff1a;strncpy,strncat,strncmp strlen strlen函数是库函数中我们最常使用的函数&#xff0c;它可以求出字符串的长度(不包含‘\0’) 使用方法 通过前面对strlen函数的模拟实现我们知…

考研小白助力宝典(2)

前言 考研&#xff0c;是一场耗时长久的脑力之战&#xff0c;刻苦勤奋的态度和披荆斩棘的精神外&#xff0c;往往取决于谁抓好了信息利剑&#xff01;合理得当利用好信息平台&#xff0c;就已经快人一步战胜了大部分的竞争对手了&#xff01; 目录 着重学习练习 考研相关简介 …