基于 RBAC 权限设计实现

在这里插入图片描述

一、前言

首先我们需要理解什么是基于 RBAC 的权限设计,简单理解就是通过角色的控制达到权限的控制,用户分配不同的角色,角色关联不同的权限,最后实现整体的权限控制。

接下来我将介绍我这个通用管理系统的权限设计思路。

为了方便理解,我们从功能开始介绍。我们需要有用户管理、角色管理、权限管理三个模块

用户管理:对于用户的一些操作,比如增删改查,分配角色。

角色管理:对于角色的基础管理,增删改查,分配权限,关联用户。

权限管理:对于权限的维护,增删改查,关联角色。这里的权限我将分为菜单权限、接口权限(操作权限)、数据权限。我简单解释一下区别,菜单权限,即前端展示的菜单权限,会支持动态路由的生成。接口权限, 其实就是操作权限,用来判断哪些操作被允许。数据权限,通过设置查看的用户范围,做到数据隔离,这里会支持,全部,部门,用户,自定义的功能。

这里我先解释一下,避免大家误解,很多开源的管理项目将菜单和操作权限结合到了一起,将操作权限绑定到按钮上,然后在接口上添加对应的注解,最后通过 **AOP **方式,实现接口注解与用户所拥有的权限做对比,然后做权限校验。比如 ruoyielAdmin 等,我之前也是受其影响,导致我对于 **RBAC **的理解一直以为权限即为菜单,操作权限即为按钮的控制,其实这是不准确的,而且相信大家也发现,这种方式我需要每个接口都加上对应的注解,十分不方便,而且没法动态去维护。所以我们在开源的项目上会看到,注解 + 路由鉴权结合在一起使用,对于所有请求都需要通过路由鉴权,比如检查是否登录,对于特殊权限校验的通过注解方式,比如是否有删除权限等。

当然也不是说他们的设计方式不好,已经定义好的权限本身就不会经常改动,所以通过 AOP 的方式也完全没有问题。适合自己的才是最好的,我这套设计则是将各自的权限划分开,更易于理解,并且支持动态维护,通过过滤器或者拦截器进行统一校验。如果你不认可我这种方式,那后面的内容可以不用看了。

再次强调一遍,权限在这里是一个抽象的概念,菜单权限只负责菜单的展示权限,接口权限指的是是否具有接口访问权限,数据权限则为数据筛选时的判断权限。

  • 注解鉴权:优势在于直观性和可读性,开发者可以直接在代码中看到权限要求,增强代码的自文档性。
  • 接口路由鉴权:灵活,统一管理,易理解。

二、表结构设计

初步设计,尽量简化业务,只保留基础的字段,后续根据具体业务再做扩展。

从上到下依次为:

  • api 信息表
  • api 模块表
  • 部门表
  • 菜单信息表
  • 角色信息表
  • 角色-api 关联表
  • 角色-部门关联表
  • 角色-菜单关联表
  • 用户表
  • 用户-角色关联表

下面是建表语句:

避免占用大量篇幅这个是附件

itshare-dev.sql

三、代码实现

通过代码生成器,生成基础的一些类,这里就不赘述了。

基于基础代码,完成简单的增删改查操作,要能够对于完成这些基础数据的简单维护。

四、整合 Sa-Token

https://sa-token.cc/doc.html#/

4.1 快速入门案例

引入依赖:注意自己 springboot 的版本

<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
  <groupId>cn.dev33</groupId>
  <artifactId>sa-token-spring-boot3-starter</artifactId>
  <version>${sa-token.version}</version>
</dependency>

设置配置文件

############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
  # jwt秘钥
  jwt-secret-key: 45646311asddjasdjl
  # token前缀
  token-prefix: Bearer
  # token 名称(同时也是 cookie 名称)
  token-name: satoken
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
  timeout: 2592000
  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  active-timeout: -1
  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
  is-concurrent: false
  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
  is-share: true
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: uuid
  # 是否输出操作日志
  is-log: true

逻辑层

public void login(String username, String password) {
    // 根据用户名查询用户信息
    AdminUser adminUser = adminUserService.getOne(new LambdaQueryWrapper<AdminUser>()
                                                  .eq(AdminUser::getUsername, username));
    if (adminUser == null || !PasswordEncoderUtil.matches(password, adminUser.getPassword())) {
        throw new ITShareException(ResCodeEnum.LOGIN_FAIL);
    }
    StpUtil.login(adminUser.getUserId(), SaLoginConfig
                  .setExtra("username", adminUser.getUsername()));
}

4.2 集成 jwt 和 Redis

  • jwt:生成一串包含一些信息的字符串(Base64编码),可以防篡改
  • Redis: 提升校验效率,减少数据库压力

使用官方提供的插件

https://sa-token.cc/doc.html#/up/integ-redis

https://sa-token.cc/doc.html#/plugin/jwt-extend

参考官方文档,文档写的很清楚。

4.3 自定义权限认证逻辑

这里我的权限列表不同于官方案例,我这里权限列表即为用户所拥有的接口数据,便于测试,这里先写死,后续会存入缓存,通过缓存优化。

定义获取权限列表逻辑

@Component
public class StpInterfaceImpl implements StpInterface {


    /**
     * 返回一个账号所拥有的权限码集合
     * @param loginId 账号id
     * @param loginType 账号体系标识
     * @return permissionList
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 结合 数据库或缓存操作
        // 这里和官方案例有所不同,这里我放入的是所有接口的路由数据。
        return List.of("/admin/user/page", "/admin/menu/add", "/admin/menu/edit", "/admin/menu/delete");
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     * @param loginId 账号id
     * @param loginType 账号体系标识
     * @return roleList
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 暂时不需要,后续再做,我认为权限即为最小控制单位,不要搞这些特殊操作,越权,最后难以维护。
        // 如果角色需要操作权限,那么应该给他绑定对应权限,然后通过权限码来控制。
        return null;
    }
}

定义路由鉴权拦截器,实现路由校验逻辑

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    // Sa-Token 整合 jwt (Simple 简单模式)
    @Bean
    public StpLogic getStpLogicJwt() {
        return new StpLogicJwtForSimple();
    }

    // 注册拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器
        registry.addInterceptor(new SaInterceptor(handle -> {
                // 登录校验。
                StpUtil.checkLogin();
                // 校验权限
                checkPermission();
            }).isAnnotation(false)) // 指定关闭掉注解鉴权能力,这样框架就只会做路由拦截校验了
            .addPathPatterns("/**")
            .excludePathPatterns(
                "/doc.html",
                "/swagger-resources/**",
                "/v3/api-docs/**",
                "/webjars/**",
                "/favicon.ico",
                "/auth/login");
    }

    private void checkPermission() {
        // 当前请求URI
        String requestPath = SaHolder.getRequest().getRequestPath();
        // 判断当前用户是否含有路由权限
        List<String> permissionList = StpUtil.getPermissionList();
        boolean hasPermission = permissionList.stream().anyMatch(requestPath::startsWith);
        if (!hasPermission) {
            throw new ITShareException(ResCodeEnum.NOT_PERMISSION);
        }
    }
}

图方便,我直接写在了之前的配置类里,这里还要排除一下** swagger** 相关的请求路径。

4.4 将角色和权限数据放入缓存

  • 项目启动初始化的时候,将角色和权限的关系缓存进 Redis
    • 缓存角色-操作权限对应关系数据
    • 缓存角色-菜单权限对应关系数据
    • 这样我们只要知道用户的角色就可以获取到用户的所有权限信息了
  • 登录的我们不仅要生成 Token 返回给前端,还需要缓存当前用户角色信息,方便我们后面鉴权时从 Redis 中获取权限校验。
  • 请求过来后,会进入我们之前配置的 Sa-Token 拦截器里,在这里我们通过 SaHolder 可以获取当前的请求URL,通过 StpUtil 可以获取用户的所有权限列表,这样我们就可以很容易判断出是否有权限了。

五、总结

其实对于登录认证的实现方式有很多,我们应该有自己的思考,适合自己的方案才是最好的,其实本质不过就是,认证和授权,认证就是告诉我你是谁,通过我的验证,我给你授权访问我。至于细节根据自己项目需要做一些调整即可,Sa-Token 这点上就和好,非常灵活。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

曹申阳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值