注意事项写在前面:
- 插件依赖VUE Element,请确保组件和CSS都正确引入
- 插件依赖的Element图标需要单独安装
- 凡是layout加载的管理页面都需要一个根div,它不能为空template也不能多个div并列
Option接口
admin/option.ts
export interface Option {
webConfig?: webConfig // 基本配置 网站名称,logo
getUser(): Promise<User | null> // 用于获取用户信息
logout(): void // 退出登录时回调函数
adminRoutes: RouteRecordRaw [] // 管理员路由数组
roleModule?: RoleModule // 角色权限管理模块配置
}
webConfig
后台管理基础配置
const webConfig = {
name: "后台管理系统", //网站名称
logo: import("@/assets/logo.svg"), // logo
loginPath:"/login", // 登录页面(用于游客越权访问时的跳转)
errorPath:"/error403" // 用户越权访问时的"无权访问页面"
}
getUser
插件根据用户角色信息user.role来控制路由访问以及侧边栏的选择性展示,因此option需要传入一个用于获取用户信息的异步函数。getUser的函数签名()=> Promise<User | null>。下面介绍getUser函数的编写。
假如你有这样一个获取 userInfo 的api函数:
import axios from "axios";
const getUserInfo = () => {
return axios.get("http://localhost/get-info")
}
那么getUser函数可以写成这样
async function getUser(): Promise<User | null> {
// 重要!从cookies中获取token, 如果token不存在(未登录状态)需要返回null
if (!Cookies.get("token")) {
return null
}
return getUserInfo().then(resp => <User>resp.data).catch(e => null)
}
但是本例中有一个前提,就是api函数getUserInfo返回的response.data恰好是接口User的一个实现
interface User
// User接口
export interface User {
id: number,
role: Role | null // 关于Role后面再讲
}
response.data
{
"id": 10086,
"role": {
"id": 73,
"name": "超级管理员",
"desc": "角色描述",
"pms": [ // 角色权限列表
...
]
}
//....省略其它用户信息
}
如果api函数返回的response.data与接口User不符只需要做一下转换即可。关于api接口中的user.role从何而来,后面角色权限管理部分会说明
const response = await getUserInfo()
// return response.data
return {
id: response.data.xxid,
role: response.data.xxxRole
}
logout
用于点击管理后台"退出"按钮的回调
// logout 可以是这样
funtion logout(){
Cookies.remove("token") // 清除cookies
router.push("/") // 跳转到某个路由
}
adminRoutes
与后台管理相关的所有路由记录 (RouteRecord)都需要从原来的routes中剥离,并重新组织到adminRoutes中。
import {AuthLevel} from '@/admin/permission'
const adminRoutes = [
{
path: 'role',
// redirect: 'https://element-plus.org/', // redirect以http开头会被当作一个外链
name: 'RoleManage', // 注意adminRoutes中的路由名称必须填写
meta: {
title: "角色权限管理",// 侧边栏,任务栏,面包屑中展示的标题
inSidebar: true, // 是否在侧边栏中展示 默认false
icon: "lock", // 侧边栏图标,传入string类型时使用element默认图标,import("@/assets/edit.svg")使用自定义图标
cacheable: false, // 是否启用<keep-alive> 缓存 默认false
authLevel: AuthLevel.NO_AUTH // 验证级别 import {AuthLevel} from '@/admin/permission'
},
component: () => import('@/admin/views/role/Index.vue')
}
]
插件会自动将adminRoutes中的路由添加到路由Admin的children中作为Admin的子路由. 因此无需再在外面嵌套一层
// 插件在install中会自动添加管理路由到Admin的children中
router.addRoute({
path: '/admin',
name: 'Admin',
meta: {title: '管理首页'},
component: () => import('@/admin/layout/Layout.vue'),
children: [...adminRoutes]
})
path: /admin/role
roleModule
roleModule(角色权限管理模块)是整个插件配置中最繁琐的部分。虽然它被定义为Option接口的可选项,但它却是整个插件最为重要的核心。使用roleModule可以真正做到前后端角色权限的统一。
interface RoleModule
// 角色权限模块配置接口
export interface RoleModule {
pmsTreeData: Permission[] | Ref<Permission[]> // 供选择的权限树数据
roleList(): Promise<Role[]> // 获取角色列表
addRole(role: Role): Promise<number> // 添加角色,成功返回的角色ID
delRole(id: number): Promise<number>// 删除角色,返回影响的行数(成功总是1)
editRole(role: Role): Promise<number> // 编辑角色,返回影响的行数(成功总是1)
exceptionHandler?(err: any): void // 用于以上方法的异常处理函数
}
pmsTreeData
pmsTreeData用于定义添加角色时的权限树,也可以将其理解为权限表。它通常是一个Permission[] 类型的数组。
但有时候我们需要根据不同的用户或者角色显示不同的权限树,
那么可以传入一个Ref<Permission[]>或者computed来实现该需求。
合理设计pmsTreeData至关重要,因为它不仅控制前端路由的访问权限和某些按钮的显示,还负责限制后端API接口的访问权限,它直接关乎到整个系统的安全。在介绍如何编写pmsTreeData之前我们先了解下Permission。
interface Permission
type Permission = PermNode & PermLeaf
interface PermNode {
name: string // 权限名称,全局唯一不可重复
requirePms?: Permission[] // 依赖的权限列表,有时候一个权限需要依赖另一个权限
pms?: Permission[] // 权限子节点,用于组织权限树的层次结构
}
interface PermLeaf {
name: string // 权限名称,全局唯一不可重复
requirePms?: Permission[] // 依赖的权限列表,有时候一个权限需要依赖另一个权限
api?: string // 需要调用的后端api (相对地址:/role/add )主要用于后端权限控制
routeName?: string // 路由名称,并不是所有权限都有自己的路由,比如"删除角色"它仅仅属于一个动作,因此这里无需设置
}
// requirePms 不会出现在权限树的节点中,因为它总是作为依赖被选择
Permission其实是PermNode和PermLeaf的交叉类型。
PermNode不负责编写特定权限,它作为根茎节点只用作挂载某个模块下的权限叶子节点(PermLeaf),但是PermNode的requirePms可以挂载该模块下的依赖权限。
PermLeaf叶子节点才是真正用于编写权限的节点。也就是说权限只能编写在叶子节点上。我们说挂载某个权限,依赖某个权限指的就是PermLeaf叶子节点
代码演示如何编写如下图所示的pmsTreeData
import type {Permission, PermLeaf, PermNode} from "@/admin/permission";
// -------用户管理模块------
const addUser: PermLeaf = {
name: "添加用户",
api: "/user/add",
routeName: "AddUser"
}
const delUser: PermLeaf = {
name: "删除用户",
api: "/user/del",
// routeName: 删除权限仅仅是个操作按钮,而没有专门的路由,因此无需配置routeName
}
const userList: PermLeaf = {
name: "用户列表",
api: "/user/list",
routeName: "UserList"
}
// 用户管理(节点)
const userModule: PermNode = {
// 用户管理节点下的所有叶子都依赖userList权限,因此将依赖配置在userModule节点下
requirePms: [userList],
name: "用户管理",
// 被依赖权限可以不出现在pms中,这里配置newsList是因为用户列表可以单独授权,也就是"只读模式"
pms: [userList, addUser, delUser]
}
// ------角色模块------
const roleList: PermLeaf = {
name: "角色列表",
api: "/role/del",
routeName: "RoleList"
}
const addRole: PermLeaf = {
name: "添加角色",
api: "/role/add",
routeName: "AddRole"
}
const delRole: PermLeaf = {
// 删除角色通常在角色列表中出现,因此它依赖于roleList权限
requirePms: [roleList],
name: "删除角色",
api: "/role/del"
}
const roleModule: PermNode = {
name: "角色模块",
pms: [roleList, addRole, delRole]
}
// 系统管理(节点)
const sysManage: PermNode = {
name: "系统管理",
pms: [userModule, roleModule]
}
// ------新闻模块------
const newsList: PermLeaf = {
name: "新闻列表",
api: "/news/list",
routeName: "NewsList"
}
const addNews: PermLeaf = {
name: "添加新闻",
api: "/news/add",
routeName: "AddNews"
}
const delNews: PermLeaf = {
// 删除新闻依赖于newsList权限
requirePms: [newsList],
name: "删除新闻",
api: "/role/del"
}
// 新闻管理(节点)
const newsModule: PermNode = {
name: "新闻管理",
pms: [newsList, addNews, delNews]
}
// 一个叶子权限它只有一个routeName 那么它可能是一个外链
const link: PermLeaf = {
name: "友情链接",
routeName: "Link",
}
// 最终将所有权限节点组织成pmsTreeData
export const pmsTreeData: Permission[] = [sysManage, newsModule, link]
Ref<Permission[]> 类型的pmsTreeData
// 可以是一个Permission[]
const pmsTreeData = [permission1, permission2]
// 也可以是一个 Ref<Permission[]>
const pmsTreeData = computed(() => {
if (role === "xxxx") {
return [permission1, ...]
} else {
return [permission2, ...]
}
})
在设计pmsTreeData时需要注意的是何时使用PremNode,何时使用PremLeaf。
roleList 等模块方法
roleList方法用于返回所有的roleList,它被展示在角色管理模块中。roleList 函数签名 :()=> Promise<Role[]>
编写roleList: 假如你有这样一个获取角色列表的api函数
import axios from "axios";
export const roleListApi = () => {
// response.data = [{role1}, {role2}...]
return axios.get("http://localhost/role-list")
}
那么roleList可以写成
function roleList() {
return roleListApi().then(resp => <Role[]>resp.data)
}
addRole, delRole以及editRole可以以此类推
/api/role.ts
import axios from "axios";
export const addRoleApi = (data: Role) => {
// 成功返回角色ID , response.data = 25
return axios.post("http://localhost/role-add", data)
}
export const editRoleApi = (data: Role) => {
// 成功返回影响的行数 , response.data = 1
return axios.post("http://localhost/role-update", data)
}
export const delRoleApi = (id: number) => {
// 成功返回影响的行数 , response.data = 1
return axios.get("http://localhost/role-del", {params: {id: id}})
}
最终的roleModule配置
const roleModule: RoleModule = {
pmsTreeData: pmsTreeData,
roleList() {
return roleListApi().then(resp => <Role[]>resp.data)
},
// 添加一个角色
addRole(role: Role) {
return addRoleApi(role).then(resp => <number>resp.data)
},
// 编辑角色
editRole(role: Role) {
return editRoleApi(role).then(resp => <number>resp.data)
},
// 删除角色
delRole(id: number) {
return delRoleApi(id).then(resp => <number>resp.data)
}
}
前后端权限控制的统一
在目前流行的前后端分离的开发模式下,后端需要定义角色来限制API接口的调用。 而前端又需要定义角色来控制路由的访问以及侧边栏的展示,甚至细化到某个按钮的显示。在角色定义时前后端往往会产生分歧和冲突。那么有没有一种方案,在前端尽量不涉足后端的情况下,把前后端角色权限结合并统一起来?roleModule正是为了解决这一问题。
回顾下Role及Permission接口:
// 角色接口
export interface Role {
id?: number // 角色ID
name: string // 角色名称
desc?: string // 角色介绍
pms: Permission[] // 权限列表
}
// 由于`Permission`是`PermNode`和`PermLeaf`的交叉类型,等同如下接口
export interface Permission {
name: string // 权限名称,全局唯一不可重复
requirePms?: Permission[] // 依赖的权限列表,有时候一个权限需要依赖另一个权限
api?: string // 需要调用的后端api (相对地址:/role/add )主要用于后端权限控制
routeName?: string // 路由名称,并不是所有权限都有自己的路由,比如"删除角色"它仅仅属于一个动作,因此这里无需设置
pms?: Permission[] // 权限子节点,用于组织权限树的层次结构
}
先看Role,本篇上述getUserInfoAPI接口获取的user.role其实就是该接口的实现。在Role中我们定义了一个pms,可以把它当作"角色的权限策略",它由角色权限模块的权限树选择生成(pmsTreeData的子集合),结果是一个数组。其中保存的是角色所拥有的全部Permission。role由roleModule生成并通过addRole方法提交给后端,后端在业务中比如"分配用户角色"时绑定给了某个user。
再看Permission,后端先是拿到user.role.pms,再根据Permission中的api属性来控制API接口的访问权限, 而插件则是利用routeName属性在路由守卫中控制每个路由的访问权限。
按钮级别的权限控制,自定义指令v-action
<el-button v-action="['/role/add']">添加角色</el-button>
v-action 的原理就是判断当前用户的角色权限,如果当前用户的user.role.pms中不存在api /role/add,那么直接将该元素的display设置为none
element.style.display = "none"。因此v-action存在兼容性问题,可能对有些组件并不适用。