Vue 權限控制(路由驗證)

下面介紹兩種權限控制的方法:css

  • 路由元信息(meta)
  • 動態加載菜單和路由(addRoutes)

路由元信息(meta)

若是一個網站有不一樣的角色,好比管理員普通用戶,要求不一樣的角色能訪問的頁面是不同的vue

這個時候咱們就能夠把全部的頁面都放在路由表裏,只要在訪問的時候判斷一下角色權限。若是有權限就讓訪問,沒有權限的話就拒絕訪問,跳轉到404頁面node

vue-router在構建路由時提供了元信息meta配置接口,咱們能夠在元信息中添加路由對應的權限,而後在路由守衛中檢查相關權限,控制其路由跳轉。ios

能夠在每個路由的 meta 屬性裏,將能訪問該路由的角色添加到 roles 裏。用戶每次登錄後,將用戶的角色返回。而後在訪問頁面時,把路由的 meta 屬性和用戶的角色進行對比,若是用戶的角色在路由的 roles 裏,那就是能訪問,若是不在就拒絕訪問。git

代碼示例1:vue-router

路由信息:vuex

routes: [
    {
        path: '/login',
        name: 'login',
        meta: {
            roles: ['admin', 'user']
        },
        component: () => import('../components/Login.vue')
    },
    {
        path: 'home',
        name: 'home',
        meta: {
            roles: ['admin']
        },
        component: () => import('../views/Home.vue')
    },
]
複製代碼

頁面控制:docker

//假設有兩種角色:admin 和 user  
//從後臺獲取的用戶角色
const role = 'user'
//當進入一個頁面是會觸發導航守衛 router.beforeEach 事件
router.beforeEach((to,from,next)=>{
	if(to.meta.roles.includes(role)){
		next()	//放行
	}esle{
		next({path:"/404"})	//跳到404頁面
	}
})
複製代碼

代碼示例2express

固然也能夠用下面的一種方法:element-ui

// router.js
// 路由表元信息
[
  {
    path: '',
    redirect: '/home'
  },
  {
    path: '/home',
    meta: {
      title: 'Home',
      icon: 'home'
    }
  },
  {
    path: '/userCenter',
    meta: {
      title: '我的中心',
      requireAuth: true // 在須要登陸的路由的meta中添加響應的權限標識
    }
  }
]

// 在守衛中訪問元信息
router.beforeEach (to, from, next) {
  let flag = to.matched.some(record=>record.meta.requireAuth);
  //console.log(flag);  //可本身打印出來看一下
}

複製代碼

能夠在多個路由下面添加這個權限標識,達到控制的目的

只要一切換頁面,就須要看有沒有這個權限,因此能夠在最大的路由下 main.js 中配置

存儲信息

通常的,用戶登陸後會在本地存儲用戶的認證信息,能夠用 tokencookie 等,這裏咱們用 token

將用戶的token保存到localStorage裏,而用戶信息則存在內存store中。這樣能夠在vuex中存儲一個標記用戶登陸狀態的屬性auth,方便權限控制。

代碼示例

// store.js
{
  state: {
    token: window.localStorage.getItem('token'),
    auth: false,
    userInfo: {}
  },
  mutations: {
    setToken (state, token) {
      state.token = token
      window.localStorage.setItem('token', token)
    },
    clearToken (state) {
      state.token = ''
      window.localStorage.setItem('token', '')
    },
    setUserInfo (state, userInfo) {
      state.userInfo = userInfo
      state.auth = true // 獲取到用戶信息的同時將auth標記爲true,固然也能夠直接判斷userInfo
    }
  },
  actions: {
    async getUserInfo (ctx, token) {
      return fetchUserInfo(token).then(response => {
        if (response.code === 200) {
          ctx.commit('setUserInfo', response.data)
        }
        return response
      })
    },
    async login (ctx, account) {
      return login(account).then(response => {
        if (response.code === 200) {
          ctx.commit('setUserInfo', response.data.userInfo)
          ctx.commit('setToken', response.data.token)
        }
      })
    }
  }
}
複製代碼

寫好路由表和vuex以後,給全部路由設置一個全局守衛,在進入路由以前進行權限檢查,並導航到對應的路由。

// router.js
router.beforeEach(async (to, from, next) => {
  if (to.matched.some(record => record.meta.requireAuth)) { // 檢查是否須要登陸權限
    if (!store.state.auth) { // 檢查是否已登陸
      if (store.state.token) { // 未登陸,可是有token,獲取用戶信息
        try {
          const data = await store.dispatch('getUserInfo', store.state.token)
          if (data.code === 200) {
            next()
          } else {
            window.alert('請登陸')
            store.commit('clearToken')
            next({ name: 'Login' })
          }
        } catch (err) {
          window.alert('請登陸')
          store.commit('clearToken')
          next({ name: 'Login' })
        }
      } else {
        window.alert('請登陸')
        next({ name: 'Login' })
      }
    } else {
      next()
    }
  } else {
    next()
  }
})
複製代碼

上述的方法是基於jwt認證方式,本地不持久化用戶信息,只保存token,當用戶刷新或者從新打開網頁時,進入須要登陸的頁面都會嘗試去請求用戶信息,該操做在整個訪問過程當中只進行一次,直到刷新或者從新打開,對於應用後期的開發維護和擴展支持都很好。

動態加載菜單和路由(addRoutes)

有時候爲了安全,咱們須要根據用戶權限或者是用戶屬性去動態的添加菜單和路由表,能夠實現對用戶的功能進行定製。vue-router提供了addRoutes()方法,能夠動態註冊路由,須要注意的是,動態添加路由是在路由表中push路由,因爲路由是按順序匹配的,所以須要將諸如404頁面這樣的路由放在動態添加的最後。

代碼示例

// store.js
// 將須要動態註冊的路由提取到vuex中
const dynamicRoutes = [
  {
    path: '/manage',
    name: 'Manage',
    meta: {
      requireAuth: true
    },
    component: () => import('./views/Manage')
  },
  {
    path: '/userCenter',
    name: 'UserCenter',
    meta: {
      requireAuth: true
    },
    component: () => import('./views/UserCenter')
  }
]
複製代碼

vuex中添加userRoutes數組用於存儲用戶的定製菜單。在setUserInfo中根據後端返回的菜單生成用戶的路由表。

// store.js
setUserInfo (state, userInfo) {
  state.userInfo = userInfo
  state.auth = true // 獲取到用戶信息的同時將auth標記爲true,固然也能夠直接判斷userInfo
  // 生成用戶路由表
  state.userRoutes = dynamicRoutes.filter(route => {
    return userInfo.menus.some(menu => menu.name === route.name)
  })
  router.addRoutes(state.userRoutes) // 註冊路由
}
複製代碼

修改菜單渲染

// App.vue
<div id="nav">
  <router-link to="/">主頁</router-link>
  <router-link to="/login">登陸</router-link>
  <template v-for="(menu, index) of $store.state.userInfo.menus">
    <router-link :to="{ name: menu.name }" :key="index">{{menu.title}}</router-link>
  </template>
</div>
複製代碼

完整的 動態加載菜單和路由 案例

若是前面的看完了,以爲本身瞭解的能夠了,其實就不須要往下面看了,固然,你也能夠看一下這個案例,嘗試本身寫一下。

友情提示:這個案例可能會有點難,有些邏輯比較很差理解,最好親自上手寫一下,下去本身慢慢慢慢消化


那麼就讓咱們開始吧:

新建一個項目,刪去不須要的文件和代碼

後端準備數據

在src同級目錄下,新建一個server.js文件,以下:

圖片加載失敗!

代碼:
其中涉及到跨域問題,具體參考個人文章《常見的跨域解決方案(全)》

//Server.js

let express  = require('express');
let app = express();
//在後端配置,讓全部人均可以訪問api接口 其中設計到跨域問題,具體參考個人文章《常見的跨域解決方案(全)》
app.use('*', function (req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    //Access-Control-Allow-Headers ,可根據瀏覽器的F12查看,把對應的粘貼在這裏就行
    res.header('Access-Control-Allow-Headers', 'Content-Type');
    res.header('Access-Control-Allow-Methods', '*');
    res.header('Content-Type', 'application/json;charset=utf-8');
    next();
});
app.get('/role',(req,res)=>{
    res.json({
        //假如這是後端給咱們的JSON數據
        menuList:[
            {pid:-1,path:'/cart',name:'購物車',id:1,auth:'cart'},
            {pid:1,path:'/cart/cart-list',name:'購物車列表',id:4,auth:'cart-list'},
            {pid:4,path:'/cart/cart-list/lottery',auth:'lottery',id:5,name:'彩票'},
            {pid:4,path:'/cart/cart-list/product',auth:'product',id:6,name:'商品'},
            {pid:-1,path:'/shop',name:'商店',id:2,auth:'shop'},
            {pid:-1,path:'/profile',name:'我的中心',id:3,auth:'profile'},
        ],
        buttonAuth:{
            edit:true, // 可編輯
        }
    })
})
//監聽3000端口
app.listen(3000);
複製代碼

而後測試端口數據能不能得到(使用node指令):

圖片加載失敗!

使用瀏覽器訪問3000端口下的/role
若是能夠得到數據,則說明端口沒問題

圖片加載失敗!

而後把下面這些對應的頁面用vue的基本骨架寫出來

圖片加載失敗!

router.js中配置路由

import Vue from 'vue'
import Router from 'vue-router'
//引入組件
import Home from "./views/Home.vue"

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  //配置路由
  routes: [
    {
      //訪問'/'時,重定向到home頁面
      path:'/',
      redirect:'/home'
    },
    {
      path:'/home',
      name:'home',
      component:Home
    },
    {
      path:'/cart',
      name:'cart',
      //使用懶加載,當使用這個組件的時候再加載資源,當組件資源較大時,不建議使用,可能會出現白屏現象
      //並且最好使用絕對路徑,@是絕對路徑的意思,至關於src下
      component:()=>import('@/components/menu/cart.vue'),
      //配置子路由
      children:[
        {
          //當配置子路由時,最好不要在前面加'/',好比:'/cart-list'
          path:'cart-list',
          name:'cart-list',
          component:()=>import('@/components/menu/cart-list.vue'),
          //配置子路由
          children:[
            {
              path:'lottery',
              name:'lottery',
              component:()=>import('@/components/menu/lottery.vue')
            },
            {
              path:'product',
              name:'product',
              component:()=>import('@/components/menu/product.vue')
            }
          ]
        }
      ]
    },
    {
      path:'/profile',
      name:'profile',
      component:()=>import('@/components/menu/profile.vue')
    },
    {
      path:'/shop',
      name:'shop',
      component:()=>import('@/components/menu/shop.vue')
    },
    {
      path:'*',
      component:()=>import('@/views/404.vue')
    }
  ]
})
複製代碼

在瀏覽器地址欄中輸入路由,測試,確定是ok的,這裏就不貼圖了

路由配置完畢後,後端給咱們返回的數據如圖所示:

圖片加載失敗!

咱們須要把上面的數據轉換成咱們須要的數據格式,並保存在vuex中,(其中涉及到異步問題,具體參考個人文章《JS中的異步解決方案》),以下:

//store.js
//重難點代碼  很難理解 須要多看幾遍慢慢理解
let formatMenuList = (menuList) => {
  function r(pid) {
    //filter過濾數組,返回一個知足要求的數組
    return menuList.filter(menu => {
      //格式化菜單變成咱們須要的結果
      if (menu.pid === pid) {        
        let children = r(menu.id);
        menu.children = children.length ? children : null;
        return true;
      }
    })
  }
  return r(-1);
}

actions: {
    //異步  async await 參考個人文章<異步解決方案>
    async getMenuList({ commit }) {
      //去server.js(後端api)獲取數據
      let { data } = await axios.get('http://localhost:3000/role');

      let menuList = data.menuList
      //把獲取的數據傳入上面寫的方法裏,轉換格式
       menuList = formatMenuList(menuList)
      //配置徹底局路由(main.js)後,可本身打印出來看一下
      // console.log(menuList);
    },
  }

複製代碼

須要保存成的數據格式,拿我以前寫過的一個樹形數據來讓你們看看,只看結構就好,不用看數據:

treeData: {
        title: "Web全棧架構師",
        children: [
          {
            title: "Java架構師"
          },
          {
            title: "JS高級",
            children: [
              {
                title: "ES6"
              },
              {
                title: "動效"
              }
            ]
          },
          {
            title: "Web全棧",
            children: [
              {
                title: "Vue訓練營",
                expand: true,
                children: [
                  {
                    title: "組件化"
                  },
                  {
                    title: "源碼"
                  },
                  {
                    title: "docker部署"
                  }
                ]
              },
              {
                title: "React",
                children: [
                  {
                    title: "JSX"
                  },
                  {
                    title: "虛擬DOM"
                  }
                ]
              },
              {
                title: "Node"
              }
            ]
          }
        ]
      }
複製代碼

而後在main.js中添加路由 (涉及到導航守衛問題,請參考個人文章Vue 導航守衛(路由的生命週期)) 以下:

//main.js
//只要頁面切換就執行的鉤子
//根據權限動態添加路由(咱們的路由要分紅兩部分:一部分是有權限的,另外一部分是沒有權限的)
router.beforeEach(async (to,from,next)=>{
  //判斷當前有沒有獲取過權限,若是獲取過了,就不要再獲取了
  if(!store.state.hasRules){
    //獲取權限,調用獲取權限的接口,去action中獲取數據
    await store.dispatch('getMenuList')
  }else{
    //若是已經獲取了權限就能夠訪問頁面了
    next()
  }
})
複製代碼

而後訪問主頁面,效果以下:

圖片加載失敗!

在上面的數據有個auth項,表明有沒有這個權限。若是沒有這個權限,到時候會把這個菜單從路由規則中刪除掉。把全部的auto都過濾出來,以下:

圖片加載失敗!

圖片加載失敗!

圖片加載失敗!

而後在主頁面測試以下(記的把上面的console.log代碼的註釋解開):

圖片加載失敗!

若是把後端數據(menuList)改變了,那麼從新刷新一下頁面,那麼就會動態的改變數據,這裏就不演示了,本身能夠註釋一個看一下

接下來渲染菜單數據,
main.js使用element-ui:

//main.js
//安裝 element-ui 並使用
import ElementUI from "element-ui"
//引入element-ui裏的樣式
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
複製代碼

把咱們剛纔獲得的數據渲染到Home上面:

圖片加載失敗!

圖片加載失敗!

在 Home 中使用mapState方法:

圖片加載失敗!

當在咱們渲染的時候,咱們須要一個遞歸組件來動態的獲取傳遞過來的數據,動態的進行渲染,因此確定是不能寫死的,遞歸組件的使用請參考個人文章《Vue 遞歸組件(構建樹形菜單)》,這裏不作過多介紹,直接拿來用

在 views 下面添加一個 ReSubMenu 組件,代碼以下:

<template>
    <el-submenu :index="data.path">
      <template slot="title">
          <router-link :to="data.path">{{data.name}}</router-link>
      </template>
      <template v-for="c in data.children">
        <ReSubMenu :key="c.auth" v-if="data.children" :data="c">
        </ReSubMenu>
        <el-menu-item :key="c.auth" v-else :index="c.path">
            {{c.name}}
        </el-menu-item>
      </template>
    </el-submenu>
</template>
<script>
export default {
    name:"ReSubMenu",
    props:{
        data:{
            type:Object,
            default:()=>({})
        }
    }
}
</script>
複製代碼

而後再 Home 中使用這個組件

圖片加載失敗!

template 中的代碼以下:

<template>
  <div class='home'>
    <!-- <router-view></router-view> -->
    <el-menu default-active="2" class="el-menu-vertical-demo" :router="true">
      <template v-for="m in menuList">
        <!-- 渲染確定是不能寫死的,因此我使用以前封裝過的遞歸組件,具體內容參考個人文章<Vue 遞歸組件(構建樹形菜單)> -->
        <ReSubMenu :data="m" :key="m.auth" v-if="m.children"></ReSubMenu>
        <el-menu-item v-else :key="m.auth" :index="m.path">{{m.name}}</el-menu-item>
      </template>
    </el-menu>
    <!-- <router-view></router-view> -->
  </div>
</template>
複製代碼

效果圖:

圖片加載失敗!

這樣就能根據傳遞過來的數據動態的生成咱們須要的結構

可是若是後端沒有給咱們返回profile,也是攔不住的,以下:

圖片加載失敗!

這樣也是能夠訪問到 profile 的,以下:

圖片加載失敗!

這樣是不行的,由於後端並無給咱們返回 profile

如何解決?

答:根據權限動態添加路由 (咱們的路由要分紅兩部分 一部分是有權限 另外一部分是沒權限的)

修改 router.js 中的代碼以下:

import Vue from 'vue'
import Router from 'vue-router'
//引入組件
import Home from "./views/Home.vue"

Vue.use(Router)

let defaultRoutes = [
  {
    //訪問'/'時,重定向到home頁面
    path:'/',
    redirect:'/home'
  },
  {
    path:'/home',
    name:'home',
    component:Home
  },
  {
    path:'*',
    component:()=>import('@/views/404.vue')
  }
]

export let authRoutes = [
  {
    path:'/cart',
    name:'cart',
    //使用懶加載,當使用這個組件的時候再加載資源,當組件資源較大時,不建議使用,可能會出現白屏現象
    //並且最好使用絕對路徑,@是絕對路徑的意思,至關於src下
    component:()=>import('@/components/menu/cart.vue'),
    //配置子路由
    children:[
      {
        //當配置子路由時,最好不要在前面加'/',好比:'/cart-list'
        path:'cart-list',
        name:'cart-list',
        component:()=>import('@/components/menu/cart-list.vue'),
        //配置子路由
        children:[
          {
            path:'lottery',
            name:'lottery',
            component:()=>import('@/components/menu/lottery.vue')
          },
          {
            path:'product',
            name:'product',
            component:()=>import('@/components/menu/product.vue')
          }
        ]
      }
    ]
  },
  {
    path:'/profile',
    name:'profile',
    component:()=>import('@/components/menu/profile.vue')
  },
  {
    path:'/shop',
    name:'shop',
    component:()=>import('@/components/menu/shop.vue')
  }
]

//須要查看用戶權限來動態添加路由
export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  //默承認以訪問的路由
  routes: defaultRoutes
})

複製代碼

stroe.js中導入:

圖片加載失敗!

store.js中添加代碼:

圖片加載失敗!

圖片加載失敗!

修改main.js中的代碼(重點):

圖片加載失敗!

測試ok:

圖片加載失敗!

固然你也能夠再註釋一個測試一下

因此這個路由是由後端返回的,若是沒有權限,根本看不到

好了,這個案例就到此爲止了

案例源碼:源碼地址

本章就到此結束了哦


^_<

相關文章
相關標籤/搜索