对于前端项目特别是中后台管理系统项目,权限设计是最复杂的点之一。
一般来说权限设计需要后端来把关,毕竟相对来说前端是无法保证安全的,前端的代码和数据请求都可以伪造。而前端的权限设计更多是为了用户体验的考虑。前端保证体验,后端保证安全。
由于前后端的开发差异和侧重点不同,在权限设计上也不一样。后端更多的是根据功能对象划分不同的权限模块,针对接口相应进行权限判断;而前端更多是针对页面路由进行模块划分,针对页面可访问进行判断。
接下来将以后台管理系统为例,分享个人对前端权限设计的见解。
(具体内容尽量做到和技术框架无关,无论是vue还是react都只是代码实现上的差异,主思路一致。不过话说vue的实现确实要比react简便很多,所以下述代码都以react为例)
先上几个常见的权限设计方式。
方式一:由后端返回筛选后的路由配置,前端渲染
方式二:后端返回用户角色,前端根据角色做路由筛选
方式三:后端返回所有权限id,前端根据权限id做路由筛选
具体来说,就是对每一个页面路由都设置一个匹配的权限id(accessId),后端只需要把用户的所有权限id给到前端即可,不需要角色信息。
有些人可能对角色这点绕不过去,其实不管你的系统有没有角色这个概念,角色只是一个称谓而已,在需要的地方展示这个称谓即可。
一个角色可以有多个权限,具体有哪些权限一般不需要关心,只有在动态配置角色权限的页面才需要这个数据,需要的时候遍历路由配置以一个tree树形组件展示编辑即可。
其实你也可以把每一个权限id都当成不同的角色理解,只不过这个角色就只有一个权限。
至于路由的权限id在哪里配置,这就看你项目的路由管理方案了,最好是对路由有一个统一管理,然后根据用户权限对路由做动态筛选,或者在路由访问时拦截判断。
一般来说后台管理系统都会有个导航菜单,以侧边栏导航居多,对于用户来说这个也是所有页面的访问入口,所以导航菜单需要根据用户权限动态展示。
建议将所有路由配置信息存储在一个配置数组中,导航菜单就根据路由配置数组来动态生成,同时判断权限做筛选。
react-router-config
来统一管理路由,react-router-waiter
(传送门)。路由配置示例:
const routes = [
{
path: '/index',
component: Index,
meta: { // meta,自定义的数据都放这里面
title: '首页', // 菜单标题
accessId: 10000, // 权限id
hideMenu: false, // 是否在侧边栏隐藏当前路由菜单
noLogin: false, // 当前路由访问是否需要登录
},
},
{
path: '/nest',
meta: {
title: '多级菜单',
},
children: [
{
path: 'nest1',
component: Nest1,
meta: {
title: '二级菜单',
accessId: 10001,
}
},
]
},
]
导航菜单动态生成示例:
function getMenuList () {
const getList = (routeList = [], prePath = '') => {
let menuList = []
// 遍历路由
routeList.forEach(v => {
v.meta = v.meta || {}
// 排除不需要显示菜单的路由
if (v.redirect || v.path === '*' || v.meta.hideMenu) {
return
}
// 排除没有访问权限的路由
if (!getIsCanAccess(v.meta.accessId)) {
return
}
const currentPath = prePath + v.path
if (v.children) {
// 有嵌套路由,递归添加菜单
menuList.push((
<SubMenu key={currentPath} title={v.meta.title}>
{getList(v.children, currentPath + '/')}
</SubMenu>
))
} else {
// 无嵌套路由,菜单添加结束
menuList.push((
<ItemMenu key={currentPath}>
<Link to={currentPath}>{v.meta.title}</Link>
</ItemMenu>
))
}
})
return menuList
}
return getList(routes)
}
导航菜单动态生成一定程度上限制了用户访问无权限的路由,但还不够,用户如果跳转一个没有权限的路由,或者在地址栏手动输入没有权限的路由网址,也是能访问页面,这就需要处理。
一般用户的权限信息都是从接口异步获取,所以我们需要在用户打开项目进入页面之前先请求接口拿到权限信息,然后再做后续页面的展示,这样才能保证在用户手动输入url场景下能有效地进行权限判断和路由拦截。
两种方式:
渲染路由前的控制,在入口组件App.vue或App.js里来写,代码示例:
import { HashRouter } from 'react-router-dom'
import RouterWaiter from 'react-router-waiter'
export default function App () {
const [isRender, setIsRender] = useState(false)
useEffect(() => {
// 解析url,获取path路由
const path = getRoutePath()
// 排除登录页等不需要权限的路由
if (['/login'].includes(path)) {
setIsRender(true)
} else {
// 判断是否已获取到权限信息
if (!store.isGotUserInfo) {
api.getUserInfo().then(res => {
const data = res.data || {}
// 权限信息存储到store状态管理数据中
store.setUserInfo(data)
// 获取完权限信息,放开路由渲染
setIsRender(true)
})
}
}
}, [])
return (
<HashRouter>
{isRender ? <RouterWaiter /> : null}
</HashRouter>
)
}
v-if
绑定控制
即可。这是对上述“路由访问控制”的方式2的补充说明。
要实现路由拦截,需要对每一个路由的访问都做前置判断。
拦截判断的代码示例:
meta = meta || {} // 路由配置数据的meta字段
if (!meta.noLogin && store.isLogin) { // 登录判断
const { accessId } = meta
if (!store.isGotUserInfo) { // 是否已获取到用户(权限)信息
api.getUserInfo().then(res => {
const data = res.data || {}
store.setUserInfo(data)
// 无权限时拦截跳转403页面
if (!getIsCanAccess(accessId)) {
toPage403()
}
})
} else {
if (!getIsCanAccess(accessId)) {
toPage403()
}
}
} else {
// 未登录时拦截跳转登录页
toPageLoin()
}
按钮级别,即页面中更细粒度的权限控制。
这个其实就很简单了,只需要控制相关的dom是否展示即可。
每一个需要控制的操作区域dom都给分配一个权限id,然后判断该用户是否具有该权限,控制该区域dom的显示隐藏。
后端也只需要把所有页面权限id和按钮级别的权限id都一箩筐给到前端就行。
代码示例:
return (
<div>
{getIsCanAccess('10008')
? (
<div>我是权限dom1</div>
)
: null}
<div>hello</div>
{getIsCanAccess('10009')
? (
<div>我是权限dom2</div>
)
: null}
</div>
)
基于此权限设计方案,个人搭建了一个react后台管理系统react-antd-mobx-admin,里面有完整的权限设计代码,供参考。