前言
一个完整的后台管理系统,往往有很多种角色,如超级管理员、业务模块的管理员、财务模块的管理员等等……
那么不同的管理员(角色)所能做的操作以及能看到的菜单应该是不一样的,比如一个商品的后台管理系统,业务模块的管理员需要维护一些商品的上下架,商品的促销等,财务模块的管理员并不需要对商品数据模块进行这些维护工作,这个时候就需要在财务角色的后台管理系统中隐藏那些功能……
实现
- 账号(用户)?
- 角色?
- 菜单?
实现之前我们先去理解上述的3个概念并找到他们之间的关联(这里我们用ruoyi项目为示例):
理解三者的概念以及之间的关联
账号(用户)
这个好理解,后台管理系统等登录页面需要填写的那几个信息就是账号的相关信息:
账号:---------
密码:********
方便理解:
账号(用户)这个在现实生活中,可以理解为类似人的姓名,如张三、李四等。
角色
方便理解:
现实生活中,人有姓名,他也有身份,如母亲、父亲、老师等,不同的身份所要处理的事情、接触到的事物也不同,这个身份就可以看作是这个人的角色。
例如老师这个角色,任何一个人都可能是老师,你可以是,我也可以是,他也可以是,这个就需要这个人(姓名=账号(用户))选择(分配)的身份(角色)是什么……
菜单
方便理解:
前面讲角色的时候说到,不同的身份(角色)所要面对、处理的事物都有所不同,这个事物就可以简单理解为菜单
例如老师这个角色,老师面对的大部分都是学生、学校、书本,处理的事物大部分都是教书育人;
传授知识,这些(学生、学校、书本、教书育人、传授知识) ≈ 事物(菜单)
看到这里,对于这三个概念以及他们之间的关联应该有个大致的模糊的概念了:
这三个之间可以说没有关联,也可以说是环环相扣;
- 没有关联?:给不同的账号(用户)分配不同的角色,在不同角色里面分配不同的菜单,这就是我们暂时看到的关联;
- 环环相扣?:他们都在不同的页面(表)里面,可以有不同的维护逻辑。
用户权限授权相关概念
用户权限授权是对用户身份认证的细化。可简单理解为访问控制,在用户身份认证通过后,系统对用户访问菜单或按钮进行控制。也就是说,该用户有身份进入系统了,但他不一定能访问系统里的所有菜单或按钮,而他只能访问管理员给他分配的权限菜单或按钮。主要包括:
- Permission(权限标识、权限字符串):针对系统访问资源的权限标识,如:用户添加、用户修改、用户删除。
- Role (角色):可以理解为权限组,也就是说角色下可以访问和点击哪些菜单、访问哪些权限标识。
权限标识或权限字符串校验规则:
- 权限字符串:指定权限串必须和菜单中的权限标识匹配才可访问;
- 权限字符串命名规范为:模块:功能:操作,例如:system:user:edit;
- 使用冒号分隔,对授权资源进行分类,如 system:user:edit 代表 系统模块:用户功能:编辑操作;
- 设定的功能指定的权限字符串与当前用户的权限字符串进行匹配,若匹配成功说明当前用户有该功能权限;
- 还可以使用简单的通配符,如 system:user:*,建议省略为 system:user(分离前端不能使用星号写法);
- 举例1 system:user 将于 system:user 或 system:user: 开头的所有权限字符串匹配成功;
- 举例2 system 将于 system 或 system: 开头的所有权限字符串匹配成功 这种命名格式的好处有:
- 可读性和可理解性:使用模块、功能和操作的格式可以直观地表达权限的含义。每个部分都有明确的作用,模块表示特定的模块或子系统,功能表示模块内的某个功能或页面,操作表示对功能进行的具体操作。通过这种格式,权限名称可以更容易地被开发人员、管理员和其他人员理解和解释。
- 可扩展性和灵活性: 通过使用模块、功能和操作的格式,可以轻松地扩展和管理权限。每个模块、功能和操作都可以被单独定义和控制。当系统需要增加新的功能或操作时,可以根据需要添加新的权限字符串,而不需要修改现有的权限规则和代码。
- 细粒度的权限控制: 这种格式支持细粒度的权限控制,可以针对特定的功能和操作进行权限管理。通过将权限名称拆分为模块、功能和操作,可以精确地定义哪些用户或角色具有访问或操作特定功能的权限。
- 避免权限冲突: 使用模块、功能和操作的格式可以避免权限之间的冲突。不同模块、功能和操作的权限名称是唯一的,这样可以避免同名权限之间的混淆和冲突。
实现
代码实现
接下来,以代码的形式来先走一下流程:
这里进行模块分类(使用vuex进行数据状态管理):用户信息的放在
user.js
里面,权限验证,构建路由放在premission.js
;
登录
modules/store/user.js
登录成功后获取token
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
跳转路由,路由守卫中进行权限验证
获取用户信息
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.user
const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', res.roles)
commit('SET_PERMISSIONS', res.permissions)
} else {
commit('SET_ROLES', ['ROLE_DEFAULT'])
}
commit('SET_ID', user.userId)
commit('SET_NAME', user.userName)
commit('SET_AVATAR', avatar)
resolve(res)
}).catch(error => {
reject(error)
})
})
},
生成路由配置表
store/modules/premission.js
import auth from '@/plugins/auth'
import router, { constantRoutes, dynamicRoutes } from '@/router'
import { getRouters } from '@/api/menu'
import Layout from '@/layout/index'
import ParentView from '@/components/ParentView'
import InnerLink from '@/layout/components/InnerLink'
const permission = {
state: {
routes: [],
addRoutes: [],
defaultRoutes: [],
topbarRouters: [],
sidebarRouters: []
},
mutations: {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
},
SET_DEFAULT_ROUTES: (state, routes) => {
state.defaultRoutes = constantRoutes.concat(routes)
},
SET_TOPBAR_ROUTES: (state, routes) => {
state.topbarRouters = routes
},
SET_SIDEBAR_ROUTERS: (state, routes) => {
state.sidebarRouters = routes
},
},
actions: {
// 生成路由
GenerateRoutes({ commit }) {
return new Promise(resolve => {
// 向后端请求路由数据
getRouters().then(res => {
const sdata = JSON.parse(JSON.stringify(res.data))
const rdata = JSON.parse(JSON.stringify(res.data))
const sidebarRoutes = filterAsyncRouter(sdata)
const rewriteRoutes = filterAsyncRouter(rdata, false, true)
const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
router.addRoutes(asyncRoutes);
commit('SET_ROUTES', rewriteRoutes)
commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
commit('SET_DEFAULT_ROUTES', sidebarRoutes)
commit('SET_TOPBAR_ROUTES', sidebarRoutes)
resolve(rewriteRoutes)
})
})
}
}
}
// 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
return asyncRouterMap.filter(route => {
if (type && route.children) {
route.children = filterChildren(route.children)
}
if (route.component) {
// Layout ParentView 组件特殊处理
if (route.component === 'Layout') {
route.component = Layout
} else if (route.component === 'ParentView') {
route.component = ParentView
} else if (route.component === 'InnerLink') {
route.component = InnerLink
} else {
route.component = loadView(route.component)
}
}
if (route.children != null && route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, route, type)
} else {
delete route['children']
delete route['redirect']
}
return true
})
}
function filterChildren(childrenMap, lastRouter = false) {
var children = []
childrenMap.forEach((el, index) => {
if (el.children && el.children.length) {
if (el.component === 'ParentView' && !lastRouter) {
el.children.forEach(c => {
c.path = el.path + '/' + c.path
if (c.children && c.children.length) {
children = children.concat(filterChildren(c.children, c))
return
}
children.push(c)
})
return
}
}
if (lastRouter) {
el.path = lastRouter.path + '/' + el.path
if (el.children && el.children.length) {
children = children.concat(filterChildren(el.children, el))
return
}
}
children = children.concat(el)
})
return children
}
// 动态路由遍历,验证是否具备权限
export function filterDynamicRoutes(routes) {
const res = []
routes.forEach(route => {
if (route.permissions) {
if (auth.hasPermiOr(route.permissions)) {
res.push(route)
}
} else if (route.roles) {
if (auth.hasRoleOr(route.roles)) {
res.push(route)
}
}
})
return res
}
export const loadView = (view) => {
if (process.env.NODE_ENV === 'development') {
return (resolve) => require([`@/views/${view}`], resolve)
} else {
// 使用 import 实现生产环境的路由懒加载
return () => import(`@/views/${view}`)
}
}
export default permission
路由守卫premission.js
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { isRelogin } from '@/utils/request'
NProgress.configure({ showSpinner: false })
// 免登录白名单
const whiteList = ['/login', '/register']
// 路由守卫
router.beforeEach((to, from, next) => {
NProgress.start()
// 判断是否有token
if (getToken()) {
// 设置页面标题
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
// 判断是否拉取了用户信息,角色信息等信息
if (store.getters.roles.length === 0) {
// 未拉取user_info信息
isRelogin.show = true
// 调接口获取相关信息
store.dispatch('GetInfo').then(() => {
isRelogin.show = false
// 获取路由表信息
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
// 退出登录
store.dispatch('LogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else {
// 已拉取完user_info信息,直接进入下一步
next()
}
}
} else {
// 没有token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done()
})
按钮权限
前端按钮级权限,是指在前端界面中,根据用户的权限不同,对不同的按钮进行权限控制。这样做的目的是为了确保系统的安全性和数据的保密性,使得不同用户只能执行其有权执行的操作,从而避免潜在的安全风险。
大致主流方式有两种:
- 条件渲染(Conditional Rendering): 这是一种简单有效的方法,通过在前端代码中根据用户的权限信息来决定是否渲染特定的按钮或组件。比如,你可以使用条件语句(如v-if等)来判断用户是否有权限,从而决定是否渲染按钮。
- 指令/组件封装: 使用一些自定义指令或组件,可以封装权限控制逻辑。你可以创建一个自定义指令或组件,接受用户权限作为输入,然后根据权限来决定是否显示按钮。
封装指令:
/directive/premission/hasPermi.js
:操作权限处理 - 包含指定权限方可显示调用
/**
* v-hasPermi 操作权限处理
* Copyright (c) 2019 ruoyi
*/
import store from '@/store'
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(`请设置操作权限标签值`)
}
}
}
/directive/premission/hasRole.js
:角色权限处理 - 包含指定角色方可显示调用
/**
* v-hasRole 角色权限处理
* Copyright (c) 2019 ruoyi
*/
import store from '@/store'
export default {
inserted(el, binding, vnode) {
const { value } = binding
const super_admin = "admin";
const roles = store.getters && store.getters.roles
if (value && value instanceof Array && value.length > 0) {
const roleFlag = value
const hasRole = roles.some(role => {
return super_admin === role || roleFlag.includes(role)
})
if (!hasRole) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`请设置角色权限标签值"`)
}
}
}
调用(其中一个页面参考)
思路,操作流程
-
一个后台管理系统默认是有一个超级管理员的,这个超级管理员是有这个系统的所有权限,如果没有,那就直接在数据库
insert
一个超级管理员; -
然后这个超级管理员在账号(用户)列表里面创建的一个账号(用户);
-
并给这个新创建的账号(用户)分配了一个角色(这个角色可以是之前就存在的角色,也可以是这个超级管理员刚刚创建的角色);
-
然后在角色列表里面,给这个角色分配不同的菜单;
-
用户登录接口,获取token;
-
获取到用户、角色、对应角色权限的数据后,保存相关信息并进行前端路由动态渲染;
这里有个需要注意的是:获取到对应的角色权限(也就是现在说的:权限验证、构建路由表)是在路由守卫中发生的。