Vue权限控制学习笔记

1.2.前端权限的意义

如果仅从能够修改服务器中数据库中的数据层面上讲,确实只在后端做控制就足够了, 那为什么越来越多的项目也进行了前端权限的控制, 主要有这几方面的好处

  • 降低非法操作的可能性

    不怕贼偷就怕贼惦记, 在页面中展示出一个 就算点击了也最终会失败 的按钮, 势必会增加有心者非法操作的可能性

  • 尽可能排除不必要请求,减轻服务器压力

    没必要的请求, 操作失败的请求, 不具备权限的请求, 应该压根就不需要发送, 请求少了, 自然也会减轻服务器的压力

  • 提高用户体验

    根据用户具备的权限为该用户展现自己权限范围内的内容,避免在界面上给用户带来困扰, 让用户专注于分内之事

2.前端权限控制思路

在这里插入图片描述

2.1.菜单的控制

在登录请求中, 会得到权限数据, 当然, 这个需要后端返回数据的支持. 前端根据权限数据, 展示对应的菜单.点击菜单,才能查看相关的界面.

2.2.界面的控制

如果用户没有登录,手动在地址栏敲入管理界面的地址, 则需要跳转到登录界面

如果用户已经登录, 可是手动敲入非权限内的地址, 则需要跳转404界面

2.3.按钮的控制

在某个菜单的界面中, 还得根据权限数据, 展示出可进行操作的按钮, 比如删除,修改,增加

2.4.请求和响应的控制

如果用户通过非常规操作, 比如通过浏览器调试工具将某些禁用的按钮变成启用状态, 此时发的请求, 也应该被前端所拦截

3. Vue的权限控制实现

3.1.菜单的控制

mock数据
admin用户数据

{
    "data": {
        "id": 500,
        "rid": 0,
        "username": "admin",
        "mobile": "13999999999",
        "email": "123999@qq.com",
        "token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE1MTI1NDQyOTksImV4cCI6MTUxMjYzMDY5OX0.eGrsrvwHm-tPsO9r_pxHIQ5i5L1kX9RX444uwnRGaIM"
    },
    "rights": [
        {
            "id": 125,
            "authName": "用户管理",
            "icon": "icon-user",
            "children": [
                {
                    "id": 110,
                    "authName": "用户列表",
                    "path": "users",
                    "rights": [
                        "view",
                        "edit",
                        "add",
                        "delete"
                    ]
                }
            ]
        },
        {
            "id": 103,
            "authName": "角色管理",
            "icon": "icon-tijikongjian",
            "children": [
                {
                    "id": 111,
                    "authName": "角色列表",
                    "path": "roles",
                    "rights": [
                        "view",
                        "edit",
                        "add",
                        "delete"
                    ]
                }
            ]
        },
        {
            "id": 101,
            "authName": "商品管理",
            "icon": "icon-shangpin",
            "children": [
                {
                    "id": 104,
                    "authName": "商品列表",
                    "path": "goods",
                    "rights": [
                        "view",
                        "edit",
                        "add",
                        "delete"
                    ]
                },
                {
                    "id": 121,
                    "authName": "商品分类",
                    "path": "categories",
                    "rights": [
                        "view",
                        "edit",
                        "add",
                        "delete"
                    ]
                }
            ]
        }
    ],
    "meta": {
        "msg": "登录成功",
        "status": 200
    }
}

zhangsan用户数据

{
    "data": {
        "id": 666,
        "rid": 0,
        "username": "zhangsan",
        "mobile": "13812811228",
        "email": "123@qq.com",
        "token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE1MTI1NDQyOTksImV4cCI6MTUxMjYzMDY5OX0.eGrsrvwHm-tPsO9r_pxHIQ5i5L1kX9RX444uwnRGaIM"
    },
    "rights": [
        {
            "id": 125,
            "authName": "用户管理",
            "icon": "icon-user",
            "children": [
                {
                    "id": 110,
                    "authName": "用户列表",
                    "path": "users",
                    "rights": [
                        "view"
                    ]
                }
            ]
        },
        {
            "id": 101,
            "authName": "商品管理",
            "icon": "icon-shangpin",
            "children": [
                {
                    "id": 104,
                    "authName": "商品列表",
                    "path": "goods",
                    "rights": [
                        "view",
                        "add"
                    ]
                },
                {
                    "id": 121,
                    "authName": "商品分类",
                    "path": "categories",
                    "rights": [
                        "view"
                    ]
                }
            ]
        }
    ],
    "meta": {
        "msg": "登录成功",
        "status": 200
    }
}

登录界面js

<script>
import { initDynamicRoutes } from '@/router.js'
export default {
  data() {
    return {
      // 这是登录表单的数据绑定对象
      loginForm: {
        username: 'admin',
        password: '123456'
      },
      // 这是表单的验证规则对象
      loginFormRules: {
        // 验证用户名是否合法
        username: [
          { required: true, message: '请输入登录名称', trigger: 'blur' },
          { min: 3, max: 10, message: '长度在 3 到 10 个字符', trigger: 'blur' }
        ],
        // 验证密码是否合法
        password: [
          { required: true, message: '请输入登录密码', trigger: 'blur' },
          { min: 6, max: 15, message: '长度在 6 到 15 个字符', trigger: 'blur' }
        ]
      }
    }
  },
  methods: {
    // 点击重置按钮,重置登录表单
    resetLoginForm() {
      // console.log(this);
      this.$refs.loginFormRef.resetFields()
    },
    login() {
      this.$refs.loginFormRef.validate(async valid => {
        if (!valid) return
        const { data: res } = await this.$http.post('login', this.loginForm)
        if (res.meta.status !== 200) return this.$message.error('登录失败!')
        this.$store.commit('setRightList', res.rights)
        this.$store.commit('setUsername', res.data.username)
        sessionStorage.setItem('token', res.data.token)
        initDynamicRoutes()
        this.$message.success('登录成功')
        this.$router.push('/home')
      })
    }
  }
}
</script>

router.js

import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login.vue'
import Home from '@/components/Home.vue'
import Welcome from '@/components/Welcome.vue'
import Users from '@/components/user/Users.vue'
import Roles from '@/components/role/Roles.vue'
import GoodsCate from '@/components/goods/GoodsCate.vue'
import GoodsList from '@/components/goods/GoodsList.vue'
import NotFound from '@/components/NotFound.vue'
import store from '@/store'

Vue.use(Router)

const userRule = { path: '/users', component: Users }
const roleRule = { path: '/roles', component: Roles }
const goodsRule = { path: '/goods', component: GoodsList }
const categoryRule = { path: '/categories', component: GoodsCate }

const ruleMapping = {
  'users': userRule,
  'roles': roleRule,
  'goods': goodsRule,
  'categories': categoryRule
}

const router = new Router({
  routes: [
    { 
      path: '/', 
      redirect: '/home' 
    },
    { 
      path: '/login', 
      component: Login 
    },
    {
      path: '/home',
      component: Home,
      redirect: '/welcome',
      children: [
        { path: '/welcome', component: Welcome },
        // { path: '/users', component: Users },
        // { path: '/roles', component: Roles },
        // { path: '/goods', component: GoodsList },
        // { path: '/categories', component: GoodsCate }
      ]
    },
    {
      path: '*',
      component: NotFound
    }
  ]
})

router.beforeEach((to, from, next) => {
  if (to.path === '/login') {
    next()
  } else {
    const token = sessionStorage.getItem('token')
    if(!token) {
      next('/login')
    } else {
      next()
    }
  }
})

export function initDynamicRoutes() {
  const currentRoutes = router.options.routes
  const rightList = store.state.rightList
  rightList.forEach(item => {
    item.children.forEach(item => {
      const itemRule = ruleMapping[item.path]
      itemRule.meta = item.rights
      currentRoutes[2].children.push(itemRule)
    })
  })
  router.addRoutes(currentRoutes)
}

export default router

渲染左侧菜单

<!-- 侧边栏 -->
      <el-aside :width="isCollapse ? '64px' : '200px'">
        <div class="toggle-button" @click="toggleCollapse">|||</div>
        <!-- 侧边栏菜单区域 -->
        <el-menu
         background-color="#333744"
         text-color="#fff"
         active-text-color="#409EFF"
         unique-opened
         :collapse="isCollapse"
         :collapse-transition="false"
         router
         :default-active="activePath"
         >
          <!-- 一级菜单 -->
          <el-submenu 
            :index="item.id + ''" 
            v-for="item in menulist" 
            :key="item.id">
            <!-- 一级菜单的模板区域 -->
            <template slot="title">
              <!-- 图标 -->
              <i :class="['iconfont',item.icon]"></i>
              <!-- 文本 -->
              <span>{{item.authName}}</span>
            </template>
            <!-- 二级菜单 -->
            <el-menu-item
              :index="'/' + subItem.path"
              v-for="subItem in item.children"
              :key="subItem.id"
              @click="saveNavState('/' + subItem.path)">
              <template slot="title">
                <!-- 图标 -->
                <i class="el-icon-menu"></i>
                <!-- 文本 -->
                <span>{{subItem.authName}}</span>
              </template>
            </el-menu-item>
          </el-submenu>
        </el-menu>
      </el-aside>
<script>
import { mapState } from 'vuex'
export default {
  data() {
    return {
      // 左侧菜单数据
      menulist: [],
      // 是否折叠
      isCollapse: false,
      // 被激活的链接地址
      activePath: ''
    }
  },
  created() {
    this.activePath = window.sessionStorage.getItem('activePath')
    this.menulist = this.rightList
  },
  methods: {
    logout() {
      sessionStorage.clear()
      window.location.reload()
      this.$router.push('/login')
    },
    // 点击按钮,切换菜单的折叠与展开
    toggleCollapse() {
      this.isCollapse = !this.isCollapse
    },
    // 保存链接的激活状态
    saveNavState(activePath) {
      window.sessionStorage.setItem('activePath', activePath)
      this.activePath = activePath
    }
  },
  computed: {
    ...mapState(['rightList','username'])
  }
}
</script>

store下index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    rightList:JSON.parse(sessionStorage.getItem('rightList')||'[]'),
    username: sessionStorage.getItem('username')|| ''
  },
  mutations: {
    setRightList(state, data) {
      state.rightList = data
      sessionStorage.setItem('rightList',JSON.stringify(data))
    },
    setUsername(state, data) {
      state.username = data
      sessionStorage.setItem('username',data)
    }
  },
  actions: {
  },
  getters: {
  }
})

3.2.界面的控制

1.正常的逻辑是通过登录界面, 登录成功之后跳转到管理平台界面, 但是如果用户直接敲入管理平台的地址, 也是可以跳过登录的步骤.所以应该在某个时机判断用户是否登录

  • 如何判断是否登录
sessionStorage.setItem('token', res.data.token)

什么时机

  • 路由导航守卫
router.beforeEach((to, from, next) => {
  if (to.path === '/login') {
    next()
  } else {
    const token = sessionStorage.getItem('token')
    if(!token) {
      next('/login')
    } else {
      next()
    }
  }
})

2.虽然菜单项已经被控制住了, 但是路由信息还是完整的存在于浏览器,正比如zhangsan这个用户并不具备角色这个菜单, 但是他如果自己在地址栏中敲入/roles的地址, 依然也可以访问角色界面

  • 路由导航守卫

    路由导航守卫固然可以在每次路由地址发生变化的时候, 从vuex中取出rightList判断用户将要访问的界面, 这个用户到底有没有权限.不过从另外一个角度来说,这个用户不具备权限的路由, 是否也应该压根就不存在呢?

  • 动态路由

    • 登录成功之后动态添加

    • App.vue中添加

    • 代码如下:

      • router.js
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login.vue'
import Home from '@/components/Home.vue'
import Welcome from '@/components/Welcome.vue'
import Users from '@/components/user/Users.vue'
import Roles from '@/components/role/Roles.vue'
import GoodsCate from '@/components/goods/GoodsCate.vue'
import GoodsList from '@/components/goods/GoodsList.vue'
import NotFound from '@/components/NotFound.vue'
import store from '@/store'

Vue.use(Router)

const userRule = { path: '/users', component: Users }
const roleRule = { path: '/roles', component: Roles }
const goodsRule = { path: '/goods', component: GoodsList }
const categoryRule = { path: '/categories', component: GoodsCate }

const ruleMapping = {
  'users': userRule,
  'roles': roleRule,
  'goods': goodsRule,
  'categories': categoryRule
}

const router = new Router({
  routes: [
    { 
      path: '/', 
      redirect: '/home' 
    },
    { 
      path: '/login', 
      component: Login 
    },
    {
      path: '/home',
      component: Home,
      redirect: '/welcome',
      children: [
        { path: '/welcome', component: Welcome },
        // { path: '/users', component: Users },
        // { path: '/roles', component: Roles },
        // { path: '/goods', component: GoodsList },
        // { path: '/categories', component: GoodsCate }
      ]
    },
    {
      path: '*',
      component: NotFound
    }
  ]
})

router.beforeEach((to, from, next) => {
  if (to.path === '/login') {
    next()
  } else {
    const token = sessionStorage.getItem('token')
    if(!token) {
      next('/login')
    } else {
      next()
    }
  }
})

export function initDynamicRoutes() {
  const currentRoutes = router.options.routes
  const rightList = store.state.rightList
  rightList.forEach(item => {
    item.children.forEach(item => {
      currentRoutes[2].children.push(ruleMapping[item.path])
    })
  })
  router.addRoutes(currentRoutes)
}

export default router

Login.vue

import { initDynamicRoutes } from '@/router.js'
login() {
      this.$refs.loginFormRef.validate(async valid => {
        if (!valid) return
        const { data: res } = await this.$http.post('login', this.loginForm)
        if (res.meta.status !== 200) return this.$message.error('登录失败!')
        this.$store.commit('setRightList', res.rights)
        this.$store.commit('setUsername', res.data.username)
        sessionStorage.setItem('token', res.data.token)
        initDynamicRoutes()
        this.$message.success('登录成功')
        this.$router.push('/home')
      })
    }

App.vue

import { initDynamicRoutes } from '@/router.js'
export default {
  name: 'app',
  created() {
    initDynamicRoutes()
  }
}
3.3.按钮的控制

按钮控制

虽然用户可以看到某些界面了, 但是这个界面的一些按钮,该用户可能是没有权限的.因此, 我们需要对组件中的一些按钮进行控制. 用户不具备权限的按钮就隐藏或者禁用, 而在这块中, 可以把该逻辑放到自定义指令中

  • permission.js
import Vue from 'vue'
import router from '@/router.js'
Vue.directive('permission', {
  inserted: function(el, binding){
    const action = binding.value.action
    const currentRight = router.currentRoute.meta
    if(currentRight) {
      if(currentRight.indexOf(action) == -1) {
        // 不具备权限
        const type = binding.value.effect
        if(type === 'disabled') {
          el.disabled = true
          el.classList.add('is-disabled')
        } else {
          el.parentNode.removeChild(el)
        }
      } 
    }
  }
})

main.js

import './utils/permission.js'

router.js

export function initDynamicRoutes() {
  const currentRoutes = router.options.routes
  const rightList = store.state.rightList
  rightList.forEach(item => {
    item.children.forEach(item => {
      const itemRule = ruleMapping[item.path]
      itemRule.meta = item.rights
      currentRoutes[2].children.push(itemRule)
    })
  })
  router.addRoutes(currentRoutes)
}

使用指令

 <el-button
            type="primary"
            @click="addDialogVisible = true"
            v-permission="{action:'add'}"
          >添加用户</el-button>
           <!-- 修改按钮 -->
            <el-button 
              type="primary" 
              icon="el-icon-edit" 
			  v-permission="{action:'edit', effect:'disabled'}"
              size="mini">
            </el-button>
            <!-- 删除按钮 -->
            <el-button
              type="danger"
              icon="el-icon-delete"
              size="mini"
			  v-permission="{action:'delete', effect:'disabled'}"
              @click="removeById(scope.row.id)">
            </el-button>
3.4.请求和响应的控制
请求控制
  • 除了登录请求都得要带上token, 这样服务器才可以鉴别你的身份
axios.interceptors.request.use(function(req){
  const currentUrl = req.url
  if(currentUrl !== 'login') {
    req.headers.Authorization = sessionStorage.getItem('token')
  }
  return req
})

如果发出了非权限内的请求, 应该直接在前端范围内阻止,虽然这个请求发到服务器也会被拒绝

import axios from 'axios'
import Vue from 'vue'
import router from '../router'
// 配置请求的跟路径, 目前用mock模拟数据, 所以暂时把这一项注释起来
// axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
const actionMapping = {
  get: 'view',
  post: 'add',
  put: 'edit',
  delete: 'delete'
}
axios.interceptors.request.use(function(req){
  const currentUrl = req.url
  if(currentUrl !== 'login') {
    req.headers.Authorization = sessionStorage.getItem('token')
    // 当前模块中具备的权限
    // 查看  get请求  
    // 增加  post请求  
    // 修改  put请求   
    // 删除  delete请求 
    const method = req.method
    // 根据请求, 得到是哪种操作
    const action = actionMapping[method]
    // 判断action是否存在当前路由的权限中
    const rights = router.currentRoute.meta
    if(rights && rights.indexOf(action) == -1) {
      // 没有权限
      alert('没有权限')
      return Promise.reject(new Error('没有权限'))
    }
  }
  return req
})
axios.interceptors.response.use(function(res){
  return res
})
Vue.prototype.$http = axios
响应控制
  • 得到了服务器返回的状态码401, 代表token超时或者被篡改了, 此时应该强制跳转到登录界面
axios.interceptors.response.use(function(res){
  if (res.data.meta.status === 401) {
    router.push('/login')
    sessionStorage.clear()
    window.location.reload()
  }
  return res
})

4.小结

前端权限的实现必须要后端提供数据支持, 否则无法实现.

返回的权限数据的结构,前后端需要沟通协商, 怎样的数据使用起来才最方便.

4.1.菜单控制
  • 权限的数据需要在多组件之间共享, 因此采用vuex
  • 防止刷新界面,权限数据丢失, 所以需要存储在sessionStorage, 并且要保证两者的同步
4.2.界面控制
  • 路由的导航守卫可以防止跳过登录界面
  • 动态路由可以让不具备权限的界面的路由规则压根就不存在
4.3.按钮控制
  • 路由规则中可以增加路由元数据meta
  • 通过路由对象可以得到当前的路由规则,以及存储在此规则中的meta数据
  • 自定义指令可以很方便的实现按钮控制
4.4.请求和响应控制
  • 请求拦截器和响应拦截器的使用
  • 请求方式的约定restful
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值