前言

本文主要讲解 RBAC 后台系统中的按钮级权限控制

RBAC 权限细粒度

在前两篇文章:RBAC 权限系统实战(一):页面级访问控制全解析、RBAC 权限系统实战(二):权限信息管理的设计 中,我们在后台系统里已经实现了权限控制,但从权限细粒度的角度看,我们只做了“页面级”权限

在权限细粒度中,一般有这三种权限粒度:

  1. 页面/菜单级:用户是否能看到并访问该页面
  2. 功能/操作级:进入页面后,是否能执行某个动作(比如按钮权限)
  3. 数据级:可以操作、获取哪些数据、接口

在某些业务场景下,我们希望用户能看到/进入页面,但不一定能操作所有功能。比如同一个列表页:A 只能“查看”,B 可以“新增/编辑”,C 还能“删除/导出”

因此本文主要实现操作级权限控制,也常称为“按钮级权限控制”

权限码设计

在第一篇权限文章中,我们在登录后,会请求用户信息接口,拿到用户的菜单路由数据再渲染访问

现在,还是基于这个接口返回的用户信息,我们要增加一个字段:permissionCodes,它是一个字符串数组,代表该用户拥有的操作权限

你可以看到,返回的权限码列表遵循一定的格式来确保语义清晰,我们约定,权限码的格式如下:

比如下面的菜单模块,关于新增、详情的权限码:

当然,不是说不遵守这个格式就不行,但我推荐这种格式。可以让我们在代码里更清楚地理解权限码含义,也方便后续维护

还要考虑一种情况:某个角色不管系统有多少权限都可以访问,比如超级管理员 superAdmin,对于这样的角色,我们做点特殊处理,比如用通配符 * 表示全部权限(如 *:*:*)。这时无需再做权限筛选,直接放行即可

按钮级权限实战

在操作级权限设计中,在 Vue 框架下,有三种实现按钮级权限的方式:组件式、自定义指令、函数式

  1. 组件式:编写 Vue 组件,插槽内容由权限码属性决定是否渲染
  2. 自定义指令:通过指令控制 DOM 来实现元素显隐
  3. 函数式:在函数中写权限筛选逻辑,上面两种方式都会依赖它

函数式

函数式写法最常见,把权限判断封装成工具函数或 hook 都可以。先看一个实现:

import { useUserStore } from '@/store/modules/user';
import { PermissionCode } from '#/type';
import { storeToRefs } from 'pinia';
import { isEmpty } from '@/utils';

export const useAuth = () => {
  const userStore = useUserStore();
  const { getPermissionCodes } = storeToRefs(userStore);

  //...

  /**
   * 判断是否有权限
   * @param code 权限码,可以是单个权限码字符串,也可以是权限码数组
   * @returns 是否有权限
   */
  const hasPermission = (code: PermissionCode): boolean => {
    // 如果是特殊通配符,直接放行
    if (getPermissionCodes.value.includes('*:*:*')) return true;

    // 空字符串、空数组情况,默认为无权限
    if (isEmpty(code)) return false;

    const codes = Array.isArray(code) ? code : [code];

    // 只要满足其中一个权限码即可
    return codes.some((c) => getPermissionCodes.value.includes(c));
  };

  return {
    hasPermission,
  };
};

这里我把逻辑写成一个 hook,重点关注 hasPermission 方法。它接收权限码参数 code,返回一个布尔值,表示是否有权限

getPermissionCodes 表示当前用户拥有的权限码

code 参数既可以是单个字符串,也可以是数组。因为用户可以同时拥有多个权限码(如 user:adduser:edit),所以类型定义如下:

/**
 * 权限码类型
 */
export type PermissionCode<T = string | string[]> = T;

实际场景中,可配合 v-if 来控制元素显隐:

组件式

组件式很好理解:把“权限判断”封装成 Vue 组件,内部内容由权限码决定是否渲染。

这里用 AppAuth 组件示例:

<script setup lang="ts">
import { PermissionCode } from '@/types/common';
import { computed } from 'vue';
import { useAuth } from '@/hooks/useAuth';

defineOptions({
  name: 'AppAuth',
});

export interface AppAuthProps {
  /**
   * 权限码
   */
  codes: PermissionCode;
}

const props = withDefaults(defineProps<AppAuthProps>(), {
  codes: '',
});

const { hasPermission } = useAuth();

/**
 * 是否有权限
 */
const hasAuth = computed(() => {
  return hasPermission(props.codes);
});
</script>

<template>
  <slot v-if="hasAuth" />
  <slot v-else name="no-auth" />
</template>

在频繁使用的场景下,最好全局注册该组件:

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import AppAuth from './components/AppAuth.vue'

const app = createApp(App)

// 全局注册
app.component('AppAuth', AppAuth)

app.mount('#app')

全局注册组件时要补上 TypeScript 的类型提示,我在 src/typings/app-components.d.ts 中添加了类型声明:

export {};

declare module 'vue' {
  export interface GlobalComponents {
    //...
    AppAuth: (typeof import('../components/common/app-auth/index'))['AppAuth'];
  }
}

然后就可以直接使用 AppAuth 组件了:

自定义指令

自定义指令也是一种很方便的实现方式,通过操作 DOM 来实现元素显隐

通过 app.directive 方法来注册 v-auth

// directives/auth.ts
import type { Directive } from 'vue';
import type { PermissionCode } from '@/types';
import { useAuth } from '@/hooks/useAuth';

export type AuthDirective = Directive<HTMLElement, PermissionCode>;

export const authDirective: AuthDirective = {
  mounted(el, binding) {
    const { hasPermission } = useAuth();
    if (!hasPermission(binding.value)) {
      el.remove();
    }
  },
  updated(el, binding) {
    const { hasPermission } = useAuth();
    if (!hasPermission(binding.value)) {
      el.remove();
    }
  },
};

然后在 main.ts 中注册 v-auth 指令:

// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { authDirective } from './directives/auth';

const app = createApp(App);
app.directive('auth', authDirective);
app.mount('#app');

同样要补上全局指令的类型定义,在 src/typings/directive.d.ts 中添加类型声明:

import type { AuthDirective } from '@/directives/typing';

declare module 'vue' {
  export interface GlobalDirectives {
    vAuth: AuthDirective;
  }
}

然后就可以使用 v-auth 指令来实现权限控制:

菜单管理、角色管理

在权限实战第二篇:RBAC 权限系统实战(二):权限信息管理的设计 中,实现了菜单、角色管理的基本管理操作,比如菜单 CRUD、角色绑定权限等操作

从细粒度来看,我们现在多做了一层操作级权限,关于这两个模块,要进行一点小改动

在菜单管理中,新增、编辑菜单等操作中,新加一个”操作“的类型,以支持添加操作级权限信息

注意这里要填写的表单信息,是根据菜单类型来展示不同的字段,比如“操作”类型,需要填写权限码

然后,菜单列表的数据是这样的:

在角色管理模块中,主要关注”分配权限“的操作,允许给角色分配操作级权限

了解更多

系列专栏地址:GitHub 博客 | 掘金专栏 | 思否专栏

实战项目:vue-clean-admin

交流讨论

文章如有错误或需要改进之处,欢迎指正。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com