前端做权限的几种方式(一) --- 手把手教你在vue中做各种权限管控(JWT接口权限、菜单权限管控)
一、权限类别1、从产品的角度来说权限一般分为 功能权限 与 数据权限。功能权限是什么?即用户登录到系统后,他能看到什么模块?能看到哪些页面?能操作哪些模块,这些是属于功能权限的范畴。数据权限是什么?即用户登录到系统后,在某个模块之中能看到哪些数据?不同用户在看同一模块时可能因为数据权限不同而看到不同的数据,这些是属于数据权限的范畴。2、功能权限的类别菜单权限:后台管理系统中的菜单一般由路由定义而成
一、权限类别
1、从产品的角度来说权限一般分为 功能权限 与 数据权限。
功能权限是什么?
即用户登录到系统后,他能看到什么模块?能看到哪些页面?能操作哪些模块,这些是属于功能权限的范畴。
数据权限是什么?
即用户登录到系统后,在某个模块之中能看到哪些数据?不同用户在看同一模块时可能因为数据权限不同而看到不同的数据,这些是属于数据权限的范畴。
2、功能权限的类别
- 菜单权限:后台管理系统中的菜单一般由路由定义而成,因此对菜单进行权限管控实际上就是对路由进行操作。
- 按钮权限
3、数据权限的说明
数据权限一般不在前端处理,不是说前端不能处理,而是没有必要去在前端处理。在通过接口请求数据时,后台已经可以通过判断当前请求用户的权限对数据进行一次清洗过滤,只需要后台返回清洗后的数据即可。
二、前端怎么做功能权限
写在前面: 前端的功能权限做的再完善、构思再精妙,也是不安全、不可靠的,因此后台的鉴权是必不可少的。
从上面我们可以得知,前端主要进行的是功能权限,通常我们只需要根据权限及业务逻辑通过 v-if, disabled 等属性即可限制。 下面展开说一下不同场景下的功能权限。
1、JWT: Token 判断用户接口权限
步骤:
1)调用登录方法,成功后获取从后台传递过来的token;
2)将token存储到 localstorage 或者 cookie 中;
3)请求拦截器(request interceptors)中设置每次请求前将token带到请求头中; 此处要与后台进行沟通请求头中 token 所对应的字段是什么,并与运维人员进行沟通,因为有些公司出于安全的考虑会对请求头中的字段名进行限制。
4)后续进行其他接口的调试,看token是否正确
解释:
token 可以用作是一个当前用户的标识,同样也是一个接口权限的说明,没有token的请求会被判断为错误请求。
延申:
如果需要判断当前请求是从当前系统发出的,那么需要前后台约定一定规则对接口进行统一加密处理。
如: 对接口地址与参数进行MD5转码并倒序处理,然后拼到token后或使用新的请求头字段。
经过这样的处理可以保证当前请求是从当前系统发出的,而不是从其他抓包工具或是其他请求处理工具发出的伪造请求。
2、菜单权限
Vue 中,菜单权限的实现根据不同需求,有2种不同的实现方式。
第一种:前端根据接口返回的用户权限,对定义的路由进行筛选,将筛选出的结果使用addRoutes添加到路由然后用于渲染菜单栏。
第二种:前端接受后台返回的已经筛选好的用户权限列表,使用addRoutes直接动态添加到路由中,然后根据渲染到菜单栏。
两者的区别在于是否让后台控制菜单上的所有资源,对于第一种方法来说,后台仅返回用户权限,菜单上的菜单名、菜单图标等均需要前端进行控制展示,若需要更改菜单图标、文字等信息则前端需要本地更改后重新打包发版。
第二种方法则是将菜单名称、菜单图标等要展示的信息全部存放到后台,由后台统一返回,前端仅需要提供相应资源即可,这种方式较为便捷,线上版本需要更改图标、文字时,只需要后台对返回信息进行边更即可(一般会存在数据库中,因此只需要变更库信息即可)。
第一种方法实现步骤:
1)定义路由信息、引入路由组件,定义的路由将用于菜单展示,因此需要在路由中加入mate字段,并设置title属性,代表菜单的名字。设置icon属性,代表菜单的图标。示例如下:
定义一个文件 routesData.js,该文件返回一个数组,内容为菜单栏展示的数据。
Tips: 通过设置 menuShow属性,来控制该项菜单是否展示。
// routersData.js 文件
const routes = [
{
path: '/',
redirect: '/login',
component: Home, // Home 组件为根路由组件为必须组件,这里直接引入即可,不需要按需加载 Children里面的内容才是菜单内容
meta: {
title: 'Home'
},
children: [
{
name: 'explorData',
path: '/explorData',
menuShow:true,
component: () => import('@/components/pages/explorData/index.vue'),
meta: {
title: '菜单一',
icon: 'el-icon-data-line'
}
},
{
name: 'taskManage',
path: '/taskManage',
menuShow:true,
component: () => import('@/components/pages/taskManage/index.vue'),
meta: {
title: '菜单二',
icon: 'el-icon-data-line2'
}
},
{
name: 'workFlow',
path: '/workFlow',
menuShow:true,
component: () => import('@/components/pages/workFlow/index.vue'),
meta: {
title: '菜单三',
icon: 'el-icon-data-line3'
}
},
{
name: 'error',
path: '/error',
menuShow:false,
component: () => import('@/components/pages/common/error-401.vue'),
meta: {
title: '错误页面'
}
}
]
},
{
path: '/login',
component: () => import('@/components/pages/login/index.vue'),
meta: {
title: '登录'
}
}
]
export default routes
2)前端需要在login接口调用成功后,调用获取权限的接口 getPermission
3)在getPermission的成功回调中,将返回的权限列表存储到 Vuex 或者 SessionStorage / LocalStorage 中 (推荐放到SessionStorage中,若放到Vuex会出现刷新页面丢失数据问题),返回的数据格式可以为:
(返回格式不固定,主要意图为通过字段来改变对应菜单的状态)
{
code: 200,
success: true,
result: [
{
name: 'explorData',
permission: true
},
{
name: 'taskManage',
permission: true
},
{
name: 'workFlow',
permission: false
}
]
}
4)将routesData.js 的数据引入到Login.vue中,在getPermission接口的成功回调里面对数据进行处理,根据 name 和 permission 属性来决定哪些菜单需要展示,最终获得一份筛选好的路由表。
注意: routes是整个路由数组对象,但是我们的目的是筛选 要展示的菜单项,因此在对 routes进行处理的时候,只需要对 routes[0].children 里面的数据进行更改。但是在调用addRoutes时要把整个routes都添加进去。
const routes = [
{
path: '/',
redirect: '/login',
component: Home, // Home 组件为根路由组件为必须组件,这里直接引入即可,不需要按需加载 。Children里面的内容才是菜单内容
meta: {
title: 'Home'
},
children: [
{
name: 'explorData',
path: '/explorData',
menuShow:true,
component: () => import('@/components/pages/explorData/index.vue'),
meta: {
title: '菜单一',
icon: 'el-icon-data-line'
}
},
{
name: 'taskManage',
path: '/taskManage',
menuShow:true,
component: () => import('@/components/pages/taskManage/index.vue'),
meta: {
title: '菜单二',
icon: 'el-icon-data-line2'
}
},
{
name: 'workFlow',
path: '/workFlow',
menuShow:false,
component: () => import('@/components/pages/workFlow/index.vue'),
meta: {
title: '菜单三',
icon: 'el-icon-data-line3'
}
},
{
name: 'error',
path: '/error',
menuShow:false,
component: () => import('@/components/pages/common/error-401.vue'),
meta: {
title: '错误页面'
}
}
]
},
{
path: '/login',
component: () => import('@/components/pages/login/index.vue'),
meta: {
title: '登录'
}
}
]
export default routes
该路由表是根据后台返回的权限表筛选后得到的结果,可以发现同之前的数据相比,仅有 workFlow 菜单的 menuShow 属性被改为了 false,这是与后台数据比对后的结果,同时也代表着在渲染时该项不会被渲染出来。
5)将筛选出的路由表动态添加到router中
this.$router.addRoutes(routes)
6)在菜单栏模块,通过 this.$router.options.routes 获取动态添加后的路由对象,是一个数组,使用v-for将其渲染在菜单栏部分,并在里面使用v-if 判断当某项路由菜单的menuShow属性为false时,不展示该项。
7)以上就完成的菜单的权限管控,但如果只做到这样,还是会有风险的,因为用户可以直接在地址栏输入隐藏的路由进行跳转。如:用户无workFlow页面权限,前端也隐藏了对应的菜单按钮,但是用户可以通过地址栏直接输入 /workFlow进行页面跳转。因此还需要对这种情况进行处理。
8)配置好项目的404页面,在路由的 router.beforeEach 钩子中进行判断,每进行路由切换时,从 Vuex 或者 SessionStorage / LocalStorage 中获取到后台返回的权限列表,同即将要跳转的路由进行比对,如果有权限则放行,如果无权限则导到404页面。
以上就完成了第一种前端实现菜单权限,这种实现较为简单,但拓展性不是很强, 当有菜单的文字、图标等信息进行更改时,需要前端重新发版打包,较为麻烦。
第二种方法实现步骤:
1)路由表不在前端进行比对,后台对用户的权限进行比对,返回给前端一个比对好的路由表,该路由表应包含 routes 中用到的所有属性,包括: name、path、component、children、meta、title等前端需要的属性,就像一个真正的路由表那样。
2)需要注意的是,并不能拿后台返回的路由表直接使用router.addRoutes()方法,因为后台返回的component字段值并不是真正的组件对象,而是 字符串 。router.addRoutes方法需要添加真正的路由对象,因此我们需要对后台返回的数据进行处理。
3)后台返回的其他字段都不需要更改,只需要对component进行处理。
处理一: 后台返回的component字段需要是一段路径,该路径代表着前端里 对应组件的 文件路径。
如后台返回格式为:
{
"data": {
"router": [
{
"path": "",
"redirect": "/home",
},
{
"path": "/home",
"component": "Home",
"name": "Home",
"meta": {
"title": "首页",
"icon": "example"
},
"children": [
{
"path": "/demo",
"name": "demo",
"component": "demo/demo1",
"meta": {
"title": "DEMO",
"icon": "table"
}
}
]
},
{
"path": "*",
"redirect": "/404",
"hidden": true
}
]
}
}
观察可以发现,菜单DEMO的component为一段路径,这个路径指向的是 demo.vue 这个组件,因此我们需要封装一个方法来处理这个路径,使其从字符串变为一个真正的路由对象。
处理二:
封装函数,该函数可以将后台传递过来的路由字符串转化为一个真正的前端路由对象。
function filterRouter(routers) { // 遍历后台传来的路由字符串,转换为组件对象
const accessedRouters = routers.filter(route => {
if (route.component) {
if (route.component === 'Home') { // Home组件特殊处理
route.component = Home
} else {
route.component = _import(route.component)
}
}
if (route.children && route.children.length) {
route.children = filterRouter(route.children)
}
return true
})
return accessedRouters
}
function _import (file) {
return () => import('@/components/views/' + file + '.vue')
}
可以看到,我们实际上是把后台传过来的 component 字段值进行的按需引入操作,后台给我们的component值实际上是一个前端存放 相应组件 的目录路径。 这样我们就可以通过按需加载获取到相应的路由对象了。
Tips:component 的值与 _import函数 不是固定的,可以根据自己项目进行动态调整。 当然你也可以不用_import方法,而是把所有的路由对象都像Home那样处理,但当路由对象有很多时,就显得十分繁琐。
4)处理完component,我们只需要调用addRoutes方法,将其动态添加到路由中即可。
5)后面的处理与第一种方式相同,不再赘述。
以上就完成了第二种前端实现菜单权限,这种实现较第一种来说,所有数据是从后台拿到的,后台返回的时候就已经做了判断,因此也不用担心用户直接在地址栏输入路由跳转等操作。仅仅需要注意的是component字段,保证这个字段的正确性即可。
以上就是vue中做菜单权限的2种方式的思路,较为简陋,另防止篇幅过长,一些容错机制未提到:
1)从后台获取的 权限列表 可以通过加密的方式存储到localstorage / sessionStorage, 防止用户手动修改权限
2)在beforeEach中进行权限比对时,如果getItem的值为null,则需要重新调用getPermission进行获取
...
可以根据自己项目进行添加。
后面有时间会写 vue中按钮的权限管控
如有错误,敬请指出。
更多推荐
所有评论(0)