RuoYi RBAC学习

639 阅读5分钟

Ruoyi RBAC权限

rbac简介

RBAC是Role Based Access Control,基于角色的访问控制。

事实上就是在以前的用户直接关联权限的基础上,中间再抽象出一角色层,实现用户和权限的解耦,使之更易扩展与维护(试想一个员工入离职业务人员就要手动勾选取消几十个权限..)

常见名词:

  • User(用户):每个用户都有唯一的UID识别,并被授予不同的角色
  • Role(角色):不同角色具有不同的权限
  • Permission(权限):访问权限
  • 用户-角色映射:用户和角色之间的映射关系
  • 角色-权限映射:角色和权限之间的映射
  • 资源:属于某个角色允许访问的,如菜单、按钮

ruoyi RBAC

若依是经典的RBAC设计,有 用户-角色-菜单(资源权限)

image-20230228155124436.png 数据库设计

image-20230227101540160-16775705515411.png 主要有

  1. 用户表
  2. 角色表
  3. 菜单表
  4. 部门表

基本使用

菜单

image-20230228155402870.png ry的菜单又细分了三个维度

  • 目录(顶级菜单)
  • 菜单(子级菜单)
  • 按钮

数据库设计

image.png

image.png

  • 父组件的parent_id为0,component组件路径为空, perms权限标识为空
  • 比较关键的perms字段,后期做权限校验,就是将登录用户的所有perm加载到缓存中,用户要访问某一资源时,再去判断其是否拥有该资源perm
  • 按钮没有路由标识,只有权限标识

用户

image.png

此时除了在sys_user表保存用户信息,还会在sys_user_role表保存用户所关联的角色。这样通过用户->角色->菜单这样的关系,就可以定义用户所有的权限

登录我们所创建的用户,只能看见角色对应的资源

image.png

角色

image.png

可以给一个角色分配菜单权限,对应修改sys_role_menu

image.png

原理分析

前端

当用户登录成功后,会将用户信息存到redis中。其中就包含了角色和权限字符串的信息,前端在登录成功后,也会调用getInfo接口将用户权限信息在vuex存储

image.png 前端自定义了两个指令:hasPermihasRole,前者是根据自定义的roleKey权限字符判断是否可见,后者是通过角色判断是否可见。基本上每一个按钮新建的时候都会指定权限字符,所以做到了按钮级的权限管理

        <el-button
          type="warning"
          plain
          icon="el-icon-download"
          size="mini"
          @click="handleExport"
          v-hasPermi="['system:dict:export']"
        >导出</el-button>
export default {
  inserted(el, binding, vnode) {
    const { value } = binding
    const all_permission = "*:*:*";
    const permissions = store.getters && store.getters.permissions

    if (value && value instanceof Array && value.length > 0) {
      const permissionFlag = value

      const hasPermissions = permissions.some(permission => {
        return all_permission === permission || permissionFlag.includes(permission)
      })
		
      if (!hasPermissions) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error(`请设置操作权限标签值`)
    }
  }
}

前端做了可见性判断,后端也需要做,不然就可以直接调用接口获取数据了。

后端

后端使用SpringSecurity框架,主要依靠@PreAuthorize注解对接口进行权限校验。

@PreAuthorize("@ss.hasPermi('system:role:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysRole role)
    {
        startPage();
        List<SysRole> list = roleService.selectRoleList(role);
        return getDataTable(list);
    }
 /**
     * 验证用户是否具备某权限
     * 
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    public boolean hasPermi(String permission)
    {
        if (StringUtils.isEmpty(permission))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
        {
            return false;
        }
        PermissionContextHolder.setContext(permission);
        return hasPermissions(loginUser.getPermissions(), permission);
    }

loginUser的数据是通过拦截器前置存入

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
        {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

ruoyi 数据隔离

基本使用

不仅要让没有权限的角色看不到菜单、按钮,在数据上也需要做隔离。

ry在分配角色的时候可以指定其可见数据范围

image.png

自定义权限可更细粒度隔离数据

image.png

原理分析

在我们创建角色信息的时候,会保存所勾选的权限范围

image.png

/**
     * 全部数据权限
     */
    public static final String DATA_SCOPE_ALL = "1";

    /**
     * 自定数据权限
     */
    public static final String DATA_SCOPE_CUSTOM = "2";

    /**
     * 部门数据权限
     */
    public static final String DATA_SCOPE_DEPT = "3";

    /**
     * 部门及以下数据权限
     */
    public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";

    /**
     * 仅本人数据权限
     */
    public static final String DATA_SCOPE_SELF = "5";

    /**
     * 数据权限过滤关键字
     */
    public static final String DATA_SCOPE = "dataScope";

当调用接口需要做数据隔离的时候,ry自定义DataScope注解,将其加在方法上。

/**
 * 数据权限过滤注解
 * 
 * @author ruoyi
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
    /**
     * 部门表的别名
     */
    public String deptAlias() default "";

    /**
     * 用户表的别名
     */
    public String userAlias() default "";

    /**
     * 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来
     */
    public String permission() default "";
}
/**
     * 查询部门管理数据
     * 
     * @param dept 部门信息
     * @return 部门信息集合
     */
    @Override
    @DataScope(deptAlias = "d")
    public List<SysDept> selectDeptList(SysDept dept)
    {
        return deptMapper.selectDeptList(dept);
    }

去寻找这个注解的切面

@Before("@annotation(controllerDataScope)")
    public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable
    {
        clearDataScope(point);
        handleDataScope(point, controllerDataScope);
    }

    protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
    {
        // 获取当前的用户
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNotNull(loginUser))
        {
            SysUser currentUser = loginUser.getUser();
            // 如果是超级管理员,则不过滤数据
            if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
            {
                String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());
                dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
                        controllerDataScope.userAlias(), permission);
            }
        }
    }

调用dataScopeFilter方法动态将参数放到BaseEntity的扩展字段里面。

 /**
     * 数据范围过滤
     *
     * @param joinPoint 切点
     * @param user 用户
     * @param deptAlias 部门别名
     * @param userAlias 用户别名
     * @param permission 权限字符
     */
    public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission)
    {
        StringBuilder sqlString = new StringBuilder();
        List<String> conditions = new ArrayList<String>();

        for (SysRole role : user.getRoles())
        {
            String dataScope = role.getDataScope();
            if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope))
            {
                continue;
            }
            if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions())
                    && !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))
            {
                continue;
            }
            if (DATA_SCOPE_ALL.equals(dataScope))
            {
                sqlString = new StringBuilder();
                break;
            }
            else if (DATA_SCOPE_CUSTOM.equals(dataScope))
            {
                sqlString.append(StringUtils.format(
                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
                        role.getRoleId()));
            }
            else if (DATA_SCOPE_DEPT.equals(dataScope))
            {
                sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
            }
            else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
            {
                sqlString.append(StringUtils.format(
                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
                        deptAlias, user.getDeptId(), user.getDeptId()));
            }
            else if (DATA_SCOPE_SELF.equals(dataScope))
            {
                if (StringUtils.isNotBlank(userAlias))
                {
                    sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
                }
                else
                {
                    // 数据权限为仅本人且没有userAlias别名不查询任何数据
                    sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
                }
            }
            conditions.add(dataScope);
        }

        if (StringUtils.isNotBlank(sqlString.toString()))
        {
            Object params = joinPoint.getArgs()[0];
            if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
            {
                BaseEntity baseEntity = (BaseEntity) params;
                baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
            }
        }
    }

我们可以在对应xml语句上看到对应的sql注入

<select id="selectDeptList" parameterType="SysDept" resultMap="SysDeptResult">
        <include refid="selectDeptVo"/>
        where d.del_flag = '0'
		<if test="deptId != null and deptId != 0">
			AND dept_id = #{deptId}
		</if>
        <if test="parentId != null and parentId != 0">
			AND parent_id = #{parentId}
		</if>
		<if test="deptName != null and deptName != ''">
			AND dept_name like concat('%', #{deptName}, '%')
		</if>
		<if test="status != null and status != ''">
			AND status = #{status}
		</if>
		<!-- 数据范围过滤 -->
		${params.dataScope}
		order by d.parent_id, d.order_num
    </select>

仅实体继承BaseEntity才会进行处理,SQL语句会存放到BaseEntity对象中的params属性中供xml参数params.dataScope获取