Vue+Element-ui技术栈
demo源码:https://github.com/Lindsayyyy/vue-permission
演示地址:http://auth.qywyyztp.top
目录
1、概述
1.1 粗粒度与细粒度
1.2 控制哪些
2、路由控制
2.1 初始化挂载登录以及无需权限的公共页面。
2.2 用户登录成功获取role,获取permission
2.3 routerConfig.js
2.4 404
2.5 渲染导航菜单
3、其他辅助方式
3.1 axios拦截器
3.2 路由拦截
参考资料
后台管理系统,有不同的用户角色,这些角色对系统拥有的权限不同。本文讨论的是基于角色的权限管理,即用户角色对应权限,而非用户对应权限。
粗粒度权限管理,对资源类型的权限管理。资源类型比如:菜单、url连接、页面中按钮。粗粒度权限管理比如:超级管理员可以访问用户添加页面、用户信息等全部页面。部门管理员可以访问用户信息页面包括页面中所有按钮。
细粒度权限管理,对资源实例的权限管理。资源实例就是资源类型的具体化,比如:用户id为001的修改连接,1110班的用户信息、行政部的员工。细粒度权限管理就是数据级别的权限管理。细粒度权限管理比如:部门经理只可以访问本部门的员工信息,用户只可以看到自己的菜单,大区经理只能查看本辖区的销售订单。
粗粒度和细粒度例子:系统有一个用户列表查询页面,对用户列表查询分权限,如果粗颗粒管理,张三和李四都有用户列表查询的权限,张三和李四都可以访问用户列表查询。进一步进行细颗粒管理,张三(行政部)和李四(开发部)只可以查询自己本部门的用户信息。张三只能查看行政部的用户信息,李四只能查看开发部门的用户信息。
粗粒度比较容易将权限管理代码抽取出来在系统架构级别统一处理。细粒度数据处理没有共性,难以统一抽取到系统架构级别去处理。所以细粒度的权限管理建议在业务层处理。
权限控制主要是对路由、视图、请求的控制。
路由资源:菜单资源,用户可以访问到的页面
视图资源:视图包括列表显示,操作按钮等
请求资源:当前用户可以进行的请求合集
路由控制和视图控制通过前端渲染实现,如果已经对页面中的功能按钮进行了权限管理,就可以避免用户执行自己没有权限的操作,所以我们目前只考虑路由控制和视图控制。
前端的权限控制实质上就是用于展示,过滤越权请求,减轻服务器压力,提高用户体验,真正的安全是由后端控制的。所以权限控制的相关数据需要后端返回给前端。具体的数据格式、包含的参数,根据具体需求设计。
本文以路由控制为例,视图中操作权限控制和接下来要说的路由控制菜单渲染部分逻辑一样,在后端返回数据中加入所需参数前端处理即可。
如果页面初始化的时候挂载全部路由,就算我们在界面对用户进行了控制,用户仍然可以通过手动修改url的方式访问自己没有权限的页面,危害系统安全。所以我们要动态渲染路由,只挂载当前用户有权限的路由。通过Vue-router的addRoutes()函数实现。
整体方案流程如下:
初始化只配置login、404页面
登录页面,用户登录成功后,后端返回role,前端再用role请求该角色对应的permission信息,permission中包含用户有权限的路由表。
用sessionStorage存储后端返回的路由信息,是因为浏览器刷新时,页面初始化了,回到没有挂载路由的状态,所以浏览器刷新后,需要重新挂载路由,这一步做在App.vue中。
adminRouteInfo.json结构如下
{
"path": "/home",
"component": "home",
"children": [{
"path": "devManage",
"title": "设备管理",
"key": "1",
"children": [{
"path": "monitor",
"title": "数据监控",
"key": "1-1",
"children": [{
"path": "alarm",
"key": "1-1-1",
"title": "报警记录",
"component": ""
}, {
"path": "historydata",
"key": "1-1-2",
"title": "历史数据",
"component": ""
}]
}, {
"path": "devlist",
"title": "设备列表",
"component": "",
"key": "1-2"
}, {
"path": "onlinerecord",
"title": "上线记录",
"component": "",
"key": "1-3"
}]
}, {
"path": "clientManage",
"title": "客户管理",
"key": "2",
"children": [{
"path": "clientlist",
"title": "客户列表",
"component": "",
"key": "2-1"
}, {
"path": "analysis",
"title": "客户分析",
"component": "",
"key": "2-2"
}]
}, {
"path": "statistics",
"title": "数据统计",
"key": "3",
"children": [{
"path": "devdata",
"title": "设备数据",
"component": "",
"key": "3-1"
}, {
"path": "clientdata",
"title": "客户数据",
"component": "",
"key": "3-2"
}]
}, {
"path": "funds",
"title": "资金结算",
"key": "4",
"children": [{
"path": "account",
"title": "结算列表",
"component": "",
"key": "4-1"
}]
}]
}
addRouterMenu()函数写在routerConfig.js中,该函数主要是将json数据处理成挂载路由所需的数据格式。addRouterMenu()需要在main.js中全局引入或者在页面中引入。
import { sessionSetStore } from '@/components/config/Utils'
// 路由懒加载
// 主菜单
const Home = r => require.ensure([], () => r(require('@/components/home')), 'Home')
// 设备管理
const Devlist = r => require.ensure([], () => r(require('@/components/devManage/devlist')), 'Devlist')
const Alarm = r => require.ensure([], () => r(require('@/components/devManage/monitor/alarm')), 'Alarm')
const Historydata = r => require.ensure([], () => r(require('@/components/devManage/monitor/historydata')), 'Historydata')
const Onlinerecord = r => require.ensure([], () => r(require('@/components/devManage/onlinerecord')), 'Onlinerecord')
// 客户管理
const Clientlist = r => require.ensure([], () => r(require('@/components/clientManage/clientlist')), 'Clientlist')
const Analysis = r => require.ensure([], () => r(require('@/components/clientManage/analysis')), 'Analysis')
// 数据统计
const Devdata = r => require.ensure([], () => r(require('@/components/statistics/devdata')), 'Devdata')
const Clientdata = r => require.ensure([], () => r(require('@/components/statistics/clientdata')), 'Clientdata')
// 资金结算
const Account = r => require.ensure([], () => r(require('@/components/account')), 'Account')
// 本地路由映射表
let localRoutes = [{
name: 'home',
component: Home,
flag: 0 // 接下来用作标识该路径是否被当前用户使用
}, {
name: 'devlist',
component: Devlist,
flag: 0
}, {
name: 'alarm',
component: Alarm,
flag: 0
}, {
name: 'historydata',
component: Historydata,
flag: 0
}, {
name: 'onlinerecord',
component: Onlinerecord,
flag: 0
}, {
name: 'clientlist',
component: Clientlist,
flag: 0
}, {
name: 'analysis',
component: Analysis,
flag: 0
}, {
name: 'devdata',
component: Devdata,
flag: 0
}, {
name: 'clientdata',
component: Clientdata,
flag: 0
}, {
name: 'account',
component: Account,
flag: 0
}]
// 需要挂载的路由
let addRouteData = [
{ path: '/home',
component: Home,
children: []
}
]
const addRouterMenu = function (data, refreshdata) {
// 挂载页面
if (data !== '') {
var arrs = JSON.parse(JSON.stringify(data))
processing(arrs.children)
console.log('addRouteData')
console.log(addRouteData)
var routerUsed = []
for (let i = 0; i < localRoutes.length; i++) {
if (localRoutes[i].flag === 1) {
routerUsed.push(localRoutes[i].name)
}
}
sessionSetStore('routerUsed', routerUsed)
// 最后做404页面的模糊匹配
addRouteData.push({
path: '*',
redirect: '404'
})
this.$router.addRoutes(addRouteData)
}
if (refreshdata !== '') {
// sessionStorage是以字符串形式存储数据的,需要转换
refreshdata = JSON.parse(refreshdata)
processing(refreshdata.children)
addRouteData.push({
path: '*',
redirect: '404'
})
console.log(addRouteData)
this.$router.addRoutes(addRouteData)
}
}
const processing = function (data) {
for (let i = 0; i < data.length; i++) {
if (data[i].component !== undefined) {
for (let j = 0; j < localRoutes.length; j++) {
if (localRoutes[j].name === data[i].path) {
let obj = {
path: '',
name: '',
component: ''
}
obj.path = localRoutes[j].name
obj.name = localRoutes[j].name
obj.component = localRoutes[j].component
addRouteData[0].children.push(obj)
localRoutes[j].flag = 1
}
}
} else {
processing(data[i].children)
}
}
}
export {addRouterMenu}
对于用户无权的资源,进行模糊匹配,跳转到404页面。注意,404的模糊匹配必须放在最后。
我在404中设置了两个按钮“返回首页”、“返回上一页”。
“返回上一页”通过在路由导航中存储路径信息实现。
“返回首页”功能需要注意,(1)因为这个窗口已经执行过addRoutes(),用户再次登录会警告“Duplicate named routes definition”,路由挂载重复了。(2)如果先用管理员账号登录,退出后,在当前窗口用客户账号登录,当前窗口挂载过管理员的权限路由,就会导致用户可以通过url访问到管理员权限页面。
所以我们在执行路由跳转后要再执行window.location.reload(),初始化页面。
路由挂载后,跳转页面,渲染菜单。仍然是处理数据,处理成菜单所需的信息。用vuex进行实时状态管理。代码太多就不贴了,有兴趣去看我的源码,主要就是从sessionStorage中取出登录时存储的permission信息处理渲染。
axios的拦截器可以统一处理所有的http请求。
请求拦截,实现统一为请求加authorization配置信息。
// axios请求拦截器,为请求头统一配置鉴权信息authorization
axios.interceptors.request.use(
config => {
let token = sessionGetStore('userToken')
// 对应几条不需要authorization的请求
let arr = ['/login/log', '/login/token']
var index = 0
for (var i in arr) {
if (config.url.indexOf(arr[i]) > -1) {
// 不需要加authorization
index = 1
}
}
if (index !== 1) {
if (!config.headers.authorization) {
config.headers = {
'authorization': token
}
}
}
return config
},
error => {
return Promise.reject(error)
}
)
响应拦截,根据后台返回的状态码在token失效的时候续签token。
// axios拦截器,token失效时,及时续签
axios.interceptors.response.use(
response => {
// 根据自己后台定义的错误码判断
if (response.data.code === '4') {
var param = { userId: sessionGetStore('userId') }
back.refreshToken(param).then(function (response) {
if (response.code === 0) {
console.log('续签成功', response.data.token)
sessionSetStore('userToken', response.data.token)
}
})
return
}
if (response.config.url.indexOf('login/token') > -1) {
// 刷新token的接口
if (response.data.code === 3) {
// 刷新token出错,返回首页
this.$router.push({ path: '/login' })
return
}
}
return response
},
error => {
console.log(error)
return Promise.reject(error)
}
)
定义路由的时候多添加一个自定义字段requireAuth,
{
path: '/repository',
name: 'repository',
meta: {
requireAuth: true,
},
component: Repository
}
在导航守卫中进行处理
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth) { // 判断该路由是否需要登录权限
// 处理程序
}
else {
next()
}
})
路由导航只在路由切换的时候发挥拦截作用,效果不好,并且如果我们已经采用了动态渲染路由的方案,就没有必要做路由拦截了。
https://blog.csdn.net/WangK_1991/article/details/53914066