從0到1搭建element後臺框架許可權設計與優化

zyhing發表於2019-04-16

首先還是謝謝各位童鞋的大大的贊贊,你們的支援是我前進的動力!上週寫了一篇從0到1搭建element後臺框架,很多童鞋留言提到許可權問題,這一週就給大家補上。GitHub

一、jwt授權認證

現在大多數專案都是採用jwt授權認證,也就是我們所熟悉的token登入身份校驗機制,jwt的好處多多,由於jwt是由服務端生成,中間人修改密串後,服務端會校驗不過,安全有效。一般呆在請求頭上的Authorization裡面。前端童鞋一般獲取token後通過vuex儲存起來,隨後資料持久化存到session中。
首先在路由跳轉的時候需要驗證vuex是否儲存了token,如果沒有token的話直接跳到登陸頁面獲取token。

    if (to.path !== '/login' && !store.state.token) {
        next('/login')
        NProgress.done() // 結束Progress
      } else {
        next();
     }
複製程式碼

詳細請看專案中的router.js
本地存在token之後,我們在每次請求介面的時候都需要帶上token來驗證token的合法性。

    //在請求前攔截
    if (store.state.token) {
            config.headers["Authorization"] = "Bearer " + store.state.token;
        }
複製程式碼

如果token不合法,全域性錯誤處理,直接跳到登陸頁面

     case 401:
        messages("warning", "使用者登陸過期,請重新登陸");
        store.state.commit('COMMIT_TOKEN','')
        setTimeout(() => {
            router.replace({
                path: "/login",
                query: {
                    redirect: router.currentRoute.fullPath
                }
            });
        }, 1000);
        break;
複製程式碼

詳細程式碼看專案中的request.js

二、選單許可權

本專案中,我主要是通過後端傳過來的角色型別來判斷導航選單的顯示與隱藏。
也就是說首先前端請求介面,後端返回token,以及對應的角色,比如專案中用admin登陸的話,roles=['admin'],用user登陸的話roles=['user']
接下來我這邊設計了一份選單表和一份路由表,路由表主要是為了註冊路由,不需要考慮層級關係。而選單表需要考慮層級關係,裡面可以配置主選單,子選單,圖示等等一系列的東西,當然選單表最好是通過介面資料從後端傳過來。值得注意的是無論是選單表,還是路由表,裡面都有一個meta配置項。裡面可以配置我們的角色許可權。路由表對應的選單表角色許可權需要一致。沒有配置角色許可權的選單預設都開放。
menu.js其中一項premission選項中配置了一個roles:['admin']

    {
        icon: "el-icon-question",
        index: "premission",
        title: "許可權測試",
        subs: [{
            index: "permission",
            title: "選單測試",
            meta: {
                roles: ['admin']
            }
        },
        {
            index: "permissionBtn",
            title: "按鈕許可權",
        },
    
        ]
    }
複製程式碼

router.js其中一項premission選項中配置了一個roles:['admin']

   {
      path: '/permission',
      component: getComponent('permission', 'permission'),
      meta: {
        title: '選單許可權',
        roles: ['admin']
      }
    },
複製程式碼

現在我們開始編寫選單邏輯,進入Aside.vue,首先根據角色過濾選單表menu.js

    /**
     * @param {Arrary} menus 選單
     * @param {Arrary} roles 角色
     * @return {Arrary} res 過濾後的選單
     */
    filterMenus(menus, roles) {
      const res = [];
      menus.forEach(route => {
        const tmp = { ...route };
        //hasPermission判斷許可權是否匹配
        if (this.hasPermission(roles, tmp)) {
          if (tmp.subs) {
            tmp.subs = this.filterMenus(tmp.subs, roles);
          }
          res.push(tmp);
        }
      });
      return res;
    },
複製程式碼
    /**
     * 通過meta.role判斷是否與當前使用者許可權匹配
     * @param roles
     * @param menu
     */
    hasPermission(roles, menu) {
      if (menu.meta && menu.meta.roles) {
        return roles.some(role => menu.meta.roles.includes(role));
      } else {
        return true;
      }
    },
複製程式碼

獲得過濾後的路由表

    computed: {
        items() {
          let items = this.filterMenus(menu, this.$store.state.roles);
          return items;
        }
      },
複製程式碼

這樣就獲得了許可權選單
到目前為止,許可權控制基本完成,不過在專案執行的過程中,還發現一個bug。本專案中存在一個tagList,也就是開啟的導航標籤,當使用者從admin切換到user的時候開啟的導航標籤依舊存在,也就是說使用者可以通過導航標籤進入premission頁面。此時我這邊直接通過路由攔截來處理此時的情況。

    if(to.meta.roles){
        to.meta.roles.includes(...store.getters.roles)?next():next('/404')
      }else{
        next();
     }
複製程式碼

沒有許可權的頁面一律進入404頁面。

三、按鈕許可權控制

按鈕級別的許可權說實話一般都通過資料介面來控制是否展示,點選等等情況。如果光有前端來控制絕對不是可行之道。
專案中按鈕許可權註冊全域性自定義指令來完成的。首先src下面新建一個directive資料夾,用於註冊全域性指令。在資料夾下新建一個premissionBtn.js。如果對自定義指令不熟的話可以查閱官方文件

    import Vue from 'vue'
    import store from '@/store/store'
    //註冊一個v-allowed指令
     Vue.directive('allowed', {
        inserted: function (el, bingding) {
            let roles = store.getters.roles
            //判斷許可權
            if (Array.isArray(roles) && roles.length > 0) {
                let allow = bingding.value.some(item => {
                    return roles.includes(item)
                })
                if (!allow) {
                    if (el.parentNode) {
                        el.parentNode.removeChild(el)
                    }
                }
            }
        }
    })
複製程式碼

全域性引用main.js

    import './directive/premissionBtn'
複製程式碼

那自定義指令如何使用呢?

     <div class="premissionBtn">
        <el-button type="primary" v-allowed="['admin']">我是隻有admin的時候才能顯示</el-button>
        <br>
        <el-button type="info" v-allowed="['user']">我是隻有user的時候才能顯示</el-button>
        <br>
        <el-button type="warning" v-allowed="['admin','user']">我是admin或者user才能顯示</el-button>
        <br>
        <el-button type="danger">任何角色都可以顯示</el-button>
    </div>
複製程式碼

四、優化

上一週專案打包出現了問題,由於打包後的專案體積比較大,以及跟路徑設定問題(~有點尷尬~),導致webpack報錯,專案執行不起來,這裡先跟大家道個歉。
本次主要用了兩個webpack的外掛,第一個是檔案壓縮外掛compression-webpack-plugin,另一個是去除console的外掛uglifyjs-webpack-plugin,首先開始安裝。

    cnpm install compression-webpack-plugin uglifyjs-webpack-plugin --save
複製程式碼

開始配置外掛vue.config.js

           let path = require('path');
            //去console外掛
            const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
            //gzip壓縮外掛
            const CompressionWebpackPlugin = require('compression-webpack-plugin')
            function resolve(dir) {
              return path.join(__dirname, dir)
            }
            module.exports = {
              //基本路徑
              publicPath: './',
              //輸出檔案目錄
              outputDir: 'dist',
              //放置生成的靜態資源 (js、css、img、fonts) 的 (相對於 outputDir 的) 目錄。
              assetsDir: 'static',
              chainWebpack: config => {
                //這裡是對環境的配置,不同的環境對應不同的BASE_URL
                config.plugin('define').tap(args => {
                  args[0]['process.env'].BASE_URL = JSON.stringify(process.env.BASE_URL)
                  return args;
                });
                //設定別名
                config.resolve.alias
                  .set('@', resolve('src'))
              },
              //  webpack外掛配置
              configureWebpack: config => {
                let plugins = [
                  new UglifyJsPlugin({
                    uglifyOptions: {
                      compress: {
                        warnings: false,
                        drop_debugger: true,
                        drop_console: true,
                      },
                    },
                    sourceMap: false,
                    parallel: true,
                  }),
                  new CompressionWebpackPlugin({
                    filename: '[path].gz[query]',
                    algorithm: 'gzip',
                    test: new RegExp(
                      '\\.(' +
                      ['js', 'css'].join('|') +
                      ')$',
                    ),
                    threshold: 10240,
                    minRatio: 0.8,
                  }),
                ]
                if (process.env.NODE_ENV !== 'development') {
                  config.plugins = [...config.plugins, ...plugins]
                }
              },
              devServer: {
                open: true
              },
              //定義scss全域性變數
              css: {
                loaderOptions: {
                  sass: {
                    data: `@import "@/assets/scss/global.scss";`
                  }
                }
              }
            }
複製程式碼

說到這裡也是對上週寫的一篇文章進行補充和完善。文章有點囉嗦~~,謝謝觀看。

相關文章