前端如何配合後端完成RBAC權限控制

補充一下模塊源代碼html


關聯上一篇Vue 前端應用實現RBAC權限控制的一種方式,這篇寫的比較懶,哈哈,仍是謝謝支持的朋友。前端

承蒙李楊的邀請,就寫了這篇文章,平時寫的很少,失誤之處,請你們多多包涵。vue

由於工做的緣由要找一個管理端模板,用於開發一個網銀系統的前端界面骨架,我就找到了d2-admin,看着十分對胃口,接着我就想着先作一個先後端分離的demo,來看一下d2-admin模板是否知足咱們項目的需求,那麼一個權限管理demo我以爲很適合拿來作實驗,就此我作了jiiiiiin-security權限系統項目,如下是部分功能截圖:java

爲何咱們須要前端實現RBAC

在說咱們前端爲何要實現權限控制以前,你們勢必要了解一下咱們要實現的東西的本質是什麼,下面簡單引用兩句介紹:linux

RBAC 以角色爲基礎的訪問控制(英語:Role-based access controlRBAC),RBAC認爲權限受權其實是Who、What、How的問題。在RBAC模型中,who、what、how構成了訪問權限三元組,也就是「Who對What(Which)進行How的操做」。ios

RBAC是一種思想,任何編程語言均可以實現,其成熟簡單的控制思想 愈來愈受廣大開發人員喜歡。git

更多內容,請你們不熟悉的勢必自行google;github

我認爲先後端是相輔相成的,因此要作好前端的權限控制,若是能提早了解後端的權限分配規則和數據結構是可以更好的進行相互配合的,固然若是徹底不理會後臺的權限劃分,硬性來作上面的兩個需求也是能實現的,只是不掌握全局,就很難理解這樣作的意義何在,因此建議你們在考慮這個問題的時候(這裏指前端同窗),仍是要大概去看看RBAC的概念,屬性經典的表結構,從而屬性後臺權限分別的業務規則。ajax

「權限管理」通常你們的印象中都屬於後端的責任,可是這兩年隨着SPA應用的興起,不少應用都採用了先後端分離的方式進行開發,可是純前端的開發方式就致使不少之前由後端模板語言硬件解決的問題,如今勢必要從新造一次輪子,而這個時候前端我認爲是配合後端對應語言的安全框架根據自身的業務須要來實現,在這裏就說說咱們的需求:正則表達式

  1. 完善咱們本身的Vue插件vue-viewplus的業務模塊(這個插件是咱們通過一年的內部使用,用來將一些開發應用所需的公共需求,抽取爲一個個模塊,方便進行快速的應用開發所寫)
  2. 咱們認爲若是在前端根據後端配置的權限規則就能攔截一些沒必要要的請求,就能減小後端沒必要要的資源損耗,也能更快的提示正經常使用戶
  3. 咱們須要解決管理端界面菜單和按鈕根據後端權限配置隱藏顯示的需求
  4. 咱們須要解決前端視圖可訪問性根據後端權限配置動態調整的需求

以上二、三、4點在先後端未曾分離的時候,這些事情都是由後類html模板語言(如傳統的java中的jsp)所包辦的,相似這樣:

<html>
<sec:authorize access="hasRole('supervisor')">

This content will only be visible to users who have
the "supervisor" authority in their list of <tt>GrantedAuthority</tt>s.

</sec:authorize>
</html>
複製代碼

docs.spring.io/spring-secu…

實現目標

  • 咱們但願在進行頁面導航的時候能先根據登陸用戶所具備的權限判斷用戶是否能訪問該頁面
  • 實現可見頁面的局部UI組件的可以使用性或可見性控制,即基於自定義v-access指令,對比聲明的接口或資源別是否已經受權
  • 實現發送請求前對待請求接口進行權限檢查,若是用戶不具備訪問該後端接口的權限,則不發送請求,而是友好的提示用戶

實現方式

要實現【咱們但願在進行頁面導航的時候能先根據登陸用戶所具備的權限判斷用戶是否能訪問該頁面】這個目標,咱們的方案是:
  1. 得到登陸用戶的可訪問前端頁面的path列表
  2. 一個公共的path列表
  3. router進行導航的beforeEach前置鉤子中判斷當前用戶所請求的頁面是否在以上兩個集合之中,若是是則放行,若是不是,則通知插件調用方,讓其本身處理失敗的狀況

下面是代碼實現:

/** * RBAC權限控制模塊 */
import _ from 'lodash';
let _onPathCheckFail
let _publicPaths = []
let _authorizedPaths = []

/** * 是不是【超級管理員】 * 若是登陸用戶是這個`角色`,那麼就無需進行各類受權控制檢測 * @type {boolean} * @private */
let _superAdminStatus = false

const _compare = function(rule, path) {
  let temp = false
  if (_.isRegExp(rule)) {
    temp = rule.test(path)
  } else {
    temp = _.isEqual(path, rule)
  }
  return temp
}

/** * 檢測登陸用戶是否具備訪問對應頁面的權限 * 1.校驗是否登陸 * 2.校驗帶訪問的頁面是否在`loginStateCheck#authorizedPaths`受權`paths`集合中 * @param to * @param from * @param next * @private */
const _rbacPathCheck = function(to, from, next) {
  if (_superAdminStatus) {
    next();
    return;
  }
  try {
    // 默認認爲全部資源都須要進行權限控制
    let isAllow = false
    const path = to.path;
    // 先檢測公共頁面集合
    const publicPathsLength = _publicPaths.length
    for (let i = publicPathsLength; i--;) {
      const rule = _publicPaths[i];
      isAllow = _compare(rule, path)
      if (isAllow) {
        break;
      }
    }
    // 非公共頁面 && 已經登陸
    if (!isAllow && this.isLogin()) {
      // 檢測已受權頁面集合
      const authorizedPathsLength = _authorizedPaths.length;
      for (let i = authorizedPathsLength; i--;) {
        const rule = _authorizedPaths[i];
        isAllow = _compare(rule, path);
        if (isAllow) {
          break;
        }
      }
    }

    if (isAllow) {
      next();
    } else {
      if (_.isFunction(_onPathCheckFail)) {
        if (_debug) {
          console.error(`[v+] RBAC模塊檢測:用戶無權訪問【${path}】,回調onPathCheckFail鉤子`);
        }
        this::_onPathCheckFail(to, from, next);
      } else {
        next(new Error('check_authorize_paths_fail'));
      }
    }
  } catch (e) {
    if (_debug) {
      console.error(`[v+] RBAC模塊檢測出錯: ${e.message}`);
    }
    if (_.isFunction(_errorHandler)) {
      this::_errorHandler(e)
    }
  }
};

const rbacModel = {
  /** * 【可選】有些系統存在一個超級用戶角色,其能夠訪問任何資源、頁面,故若是設置,針對這個登陸用戶將不會作任何權限校驗,以便節省前端資源 * @param status */
  rabcUpdateSuperAdminStatus(status) {
    _superAdminStatus = status;
    this.cacheSaveToSessionStore('AUTHORIZED_SUPER_ADMIN_STATUS', _superAdminStatus)
  },
  /** * 添加受權路徑集合 * 如:登陸完成以後,將用戶被受權能夠訪問的頁面`paths`添加到`LoginStateCheck#authorizedPaths`中 * @param paths */
  rabcAddAuthorizedPaths(paths) {
    this::rbacModel.rabcUpdateAuthorizedPaths(_.concat(_authorizedPaths, paths))
  },
  /** * 更新受權路徑集合 * @param paths */
  rabcUpdateAuthorizedPaths(paths) {
    _authorizedPaths = [...new Set(paths)]
    this.cacheSaveToSessionStore('AUTHORIZED_PATHS', _authorizedPaths)
  },
  /** * 更新公共路徑集合 * @param paths */
  rabcUpdatePublicPaths(paths) {
    _publicPaths = [...new Set(paths)];
    this.cacheSaveToSessionStore('PUBLIC_PATHS', _publicPaths)
  },
  /** * 添加公共路徑集合 * @param paths */
  rabcAddPublicPaths(paths) {
    this::rbacModel.rabcUpdatePublicPaths(_.concat(_publicPaths, paths))
  },
  install(Vue, {
    /** * [*] 系統公共路由path路徑集合,便可以讓任何人訪問的頁面路徑 * {Array<Object>} * <p> * 好比登陸頁面的path,由於登陸以前咱們是沒法判斷用戶是否能夠訪問某個頁面的,故須要這個配置,固然若是須要這個配置也能夠在初始化插件以前從服務器端獲取,這樣先後端動態性就更高,可是通常沒有這種需求:) * <p> * 數組中的item,能夠是一個**正則表達式字面量**,如`[/^((\/Interbus)(?!\/SubMenu)\/.+)$/]`,也能夠是一個字符串 * <p> * 匹配規則:若是在`LoginStateCheck#publicPaths`**系統公共路由path路徑集合**中,那麼就直接跳過權限校驗 */
    publicPaths = [],
    /** * [*] 登陸用戶擁有訪問權限的路由path路徑集合 * {Array<Object>} * <p> * 數組中的item,能夠是一個**正則表達式字面量**,如`[/^((\/Interbus)(?!\/SubMenu)\/.+)$/]`,也能夠是一個字符串 * <p> * 匹配規則:若是在`LoginStateCheck#authorizedPaths`**須要身份認證規則集**中,那麼就須要查看用戶是否登陸,若是沒有登陸就拒絕訪問 */
    authorizedPaths = [],
    /** * [*] `$vp::onPathCheckFail(to, from, next)` * <p> * 訪問前端頁面時權限檢查失敗時被回調 */
    onPathCheckFail = null,
  } = {}) {
    _onPathCheckFail = onPathCheckFail;
    router.beforeEach((to, from, next) => {
      this::_rbacPathCheck(to, from, next);
    });
  }
};

export default rbacModel;

複製代碼

這裏解釋一下:

  1. 整個代碼最終導出了一個普通的json對象,做爲vue-viewplus的一個自定義模塊,將會被mixin到其插件內部做爲一個自定義模塊:

    // 應用入口mian.js
    import Vue from 'vue'
    import router from './router'
    import ViewPlus from 'vue-viewplus'
    import viewPlusOptions from '@/plugin/vue-viewplus'
    import rbacModule from '@/plugin/vue-viewplus/rbac.js'
    
    Vue.use(ViewPlus, viewPlusOptions)
    
    ViewPlus.mixin(Vue, rbacModule, {
      moduleName: '自定義RBAC',
      router,
      publicPaths: ['/login'],
      onPathCheckFail(to, from, next) {
        NProgress.done()
        const title = to.meta.title
        this.dialog(`您無權訪問【${_.isNil(title) ? to.path : title}】頁面`)
          .then(() => {
            // 沒有登陸的時候跳轉到登陸界面
            // 攜帶上登錄成功以後須要跳轉的頁面完整路徑
            next(false)
          })
      }
    })
    複製代碼

    你們若是沒有使用或者不想使用這個插件(vue-viewplus也無所謂,這裏只要知道,導出的這個對象的install會在應用入口被調用,並傳入幾個install方法幾個必須的參數:

    • 路由對象
    • 應用的公共頁面paths列表
    • 權限校驗失敗以後的處理函數

    這樣咱們就能在初始化函數中緩存應用公共頁面paths列表,註冊路由鉤子,監聽路由變化。

    這裏我使用這個插件爲的還有第二個目的,利用其來管理用戶登陸狀態,詳細看下面我爲何要使用這個狀態

  2. 在監聽到某個公共頁面訪問的時候,_rbacPathCheck函數將會:

    • 首先判斷當前用戶是不是超級管理員,你能夠理解爲linux中的root用戶,若是是則直接放行,這樣作是爲了減小判斷帶來的開銷,固然若是須要實現這個效果,須要在登陸以後,根據後端返回的用戶信息中查看用戶的角色,是不是超級管理員,若是是,則調用文件導出的rabcUpdateSuperAdminStatus方法,在這裏是頁面實例的this.$vp.rabcUpdateSuperAdminStatus方法(vue-viewplus將每一個模塊導出的api綁定到頁面實例即vm的$vp屬性之下):
    // 登陸頁面提交按鈕綁定方法
    submit() {
          this.$refs.loginForm.validate(valid => {
            if (valid) {
              // 登陸
              this.login({
                vm: this,
                username: this.formLogin.username,
                password: this.formLogin.password,
                imageCode: this.formLogin.code
              }).then((res) => {
                // 修改用戶登陸狀態
                this.$vp.modifyLoginState(true);
                // 解析服務端返回的登陸用戶數據,獲得菜單、權限相關數據
                const isSuperAdminStatus = parseUserRoleIsSuperAdminStatus(res.principal.admin.roles);
                this.$vp.toast('登陸成功', {
                  type: 'success'
                });
                // 重定向對象不存在則返回頂層路徑
                this.$router.replace(this.$route.query.redirect || '/')
              })
            } else {
              // 登陸表單校驗失敗
              this.$message.error('表單校驗失敗')
            }
          })
        }
    複製代碼
    • 若是不是則檢測待訪問的頁面的path是否在**應用的公共頁面paths列表_publicPaths**中,若是是則放行

      而作這個判斷的前提是應用登陸成功以後須要將其得到受權的前端paths設置this.$vp.rabcUpdateAuthorizedPaths給插件:

      submit() {
            this.$refs.loginForm.validate(valid => {
              if (valid) {
                // 登陸
                this.login({
                  vm: this,
                  username: this.formLogin.username,
                  password: this.formLogin.password,
                  imageCode: this.formLogin.code
                }).then((res) => {
                  this.$vp.rabcUpdateAuthorizedPaths(authorizeResources.paths);
                })
              } else {
                // 登陸表單校驗失敗
                this.$message.error('表單校驗失敗')
              }
            })
          }
      複製代碼

      數據的格式以下:

      ["/mngauth/admin", "/index", "/mngauth"]
      複製代碼

      而且,數組的值支持爲正則表達式;

    • 若是不是則檢查待訪問頁面的path是否在**登陸用戶擁有訪問權限的路由path路徑集合_authorizedPaths**中,若是是則放行,若是不是則整個校驗結束,判斷用戶無權訪問該頁面,調用_onPathCheckFail回調函數,通知應用,這裏應用則會打印dialog提示用戶

    由於咱們的目的是抽象整個業務,因此這裏才以回調的方式讓應用有實際去感知和處理這一狀況;

    這樣咱們就完成了第一個目標;

要實現【實現可見頁面的局部UI組件的可以使用性或可見性控制,即基於自定義v-access指令,對比聲明的接口或資源別是否已經受權】這個目標,咱們的方案是:
  1. 得到登陸用戶的:

    • 被受權角色所擁有的資源列表,對應的資源別名

      數據格式相似:

      ["MNG_USERMNG", "MNG_ROLEMNG"]
      複製代碼
    • 被受權角色所擁有的資源列表(或資源)所對應的後端接口集合

      數據格式相似:

      ["admin/dels/*", "admin/search/*/*/*", "admin/*/*/*", "role/list/*", "admin/*"]
      複製代碼

      可是默認但願的是RESTful格式:

      [{url: "admin/dels/*", method: "DELETE"}, ....]
      複製代碼

      固然一樣支持js正則表達式;

    經過以上兩組(二選一)受權數據,咱們就能夠對比用戶在指令中聲明的條件權限進行對比。

  2. 定義一個Vue指令,這裏命名爲access,其須要具有如下特色:

    • 可讓用戶聲明不一樣的權限表達式,如這個按鈕是須要一組接口,仍是一個資源別名
    • 可讓用戶控制,在不知足權限檢查以後,是讓UI組件不顯示仍是讓其不可用

    固然要理解上面的數據結構後端是怎麼構建的,能夠參考表結構和權限說明

咱們繼續往上面的代碼中添加邏輯,下面是代碼實現:

const rbacModel = {
  //....
  /** * 更新受權接口集合 * @param interfaces */
  rabcUpdateAuthorizeInterfaces(interfaces) {
    _authorizeInterfaces = [...new Set(interfaces)]
    this.cacheSaveToSessionStore('AUTHORIZED_INTERFACES', _authorizeInterfaces)
  },
  /** * 添加受權接口集合 * @param interfaces */
  rabcAddAuthorizeInterfaces(interfaces) {
    this::rbacModel.rabcUpdateAuthorizeInterfaces(_.concat(_authorizeInterfaces, interfaces))
  },
  /** * 更新資源別名集合 * @param alias */
  rabcUpdateAuthorizeResourceAlias(alias) {
    _authorizeResourceAlias = [...new Set(alias)]
    this.cacheSaveToSessionStore('AUTHORIZED_RESOURCE_ALIAS', _authorizeResourceAlias)
  },
  /** * 添加資源別名集合 * @param alias */
  rabcAddAuthorizeResourceAlias(alias) {
    this::rbacModel.rabcUpdateAuthorizeResourceAlias(_.concat(_authorizeResourceAlias, alias))
  },
  install(Vue, {
    //....
    /** * [可選] 登陸用戶擁有訪問權限的資源別名集合 * {Array<Object>} * <p> * 數組中的item,能夠是一個**正則表達式字面量**,如`[/^((\/Interbus)(?!\/SubMenu)\/.+)$/]`,也能夠是一個字符串 * <p> * 匹配規則:由於若是都用`LoginStateCheck#authorizeInterfaces`接口進行匹配,可能有一種狀況,訪問一個資源,其須要n個接口,那麼咱們在配置配置權限指令:v-access="[n, n....]"的時候就須要聲明全部須要的接口,就會須要對比屢次, * 當咱們系統的接口集合很大的時候,勢必會成爲一個瓶頸,故咱們能夠爲資源聲明一個別名,這個別名則能夠表明這n個接口,這樣的話就從n+減小到n次匹配; */
    authorizeResourceAlias = [],
    /** * [*] 登陸用戶擁有訪問權限的後臺接口集合 * {Array<Object>} * <p> * 1.在`v-access`指令配置爲url(默認)校驗格式時,將會使用該集合和指令聲明的待審查受權接口列表進行匹配,若是匹配成功,則指令校驗經過,不然校驗不經過,會將對應dom元素進行處理 * 2.TODO 將會用於在發送ajax請求以前,對待請求的接口和當前集合進行匹配,若是匹配失敗說明用戶就沒有請求權限,則直接不發送後臺請求,減小後端沒必要要的資源浪費 * <p> * 數組中的item,能夠是一個**正則表達式字面量**,如`[/^((\/Interbus)(?!\/SubMenu)\/.+)$/]`,也能夠是一個字符串 * <p> * 匹配規則:將會用於在發送ajax請求以前,對待請求的接口和當前集合進行匹配,若是匹配失敗說明用戶就沒有請求權限,則直接不發送後臺請求,減小後端沒必要要的資源浪費 * <p> * 注意須要根據`isRESTfulInterfaces`屬性的值,來判斷當前集合的數據類型: * * 若是`isRESTfulInterfaces`設置爲`false`,則使用下面的格式: * ```json * ["admin/dels/*", ...] * ``` * 若是`isRESTfulInterfaces`設置爲`true`,**注意這是默認設置**,則使用下面的格式: * ```json * [[{url: "admin/dels/*", method: "DELETE"}, ...]] * ``` */
    authorizeInterfaces = [],
    /** * [*] 聲明`authorizeInterfaces`集合存儲的是RESTful類型的接口仍是常規接口 * 1. 若是是(true),則`authorizeInterfaces`集合須要存儲的結構就是: * [{url: 'admin/dels/*', method: 'DELETE'}] * 即進行接口匹配的時候會校驗類型 * 2. 若是不是(false),則`authorizeInterfaces`集合須要存儲的結構就是,即不區分接口類型: * ['admin/dels/*'] */
    isRESTfulInterfaces = true
  } = {}) {
    //....
    this::_createRBACDirective(Vue)
  }
};

export default rbacModel;
複製代碼

首先咱們在插件中添加幾個字段和對應的設置接口:

  • isRESTfulInterfaces
  • authorizeInterfaces
  • authorizeResourceAlias

這樣咱們就能夠維護用戶擁有的受權資源別名列表、資源(對應接口)後端接口數據列表,並默認認爲接口爲RESTful數據結構;

接着咱們就能夠定義指令(在插件初始化方法install中),並在指令的bind聲明週期,解析對應UI組件聲明的所需權限信息,並和持有的資源列表進行對比,若是對比失敗則對UI組件作相應的顯示或者disable操做:

/** * 推薦使用資源標識配置:`v-access:alias[.disable]="'LOGIN'"` 前提須要注入身份認證用戶所擁有的**受權資源標識集合**,由於這種方式能夠較少比較的次數 * 傳統使用接口配置:`v-access:[url][.disable]="'admin'"` 前提須要注入身份認證用戶所擁有的**受權接口集合** * 兩種都支持數組配置 * v-access:alias[.disable]="['LOGIN', 'WELCOME']" * v-access:[url][.disable]="['admin', 'admin/*']" * 針對於RESTful類型接口: * v-access="[{url: 'admin/search/*', method: 'POST'}]" * 默認使用url模式,由於這種方式比較通用 * v-access="['admin', 'admin/*']" * <p> * 其中`[.disbale]`用來標明在檢測用戶不具備對當前聲明的權限時,將會把當前聲明指令的`el`元素添加`el.disabled = true`,默認則是影藏元素:`el.style.display = 'none'` * <p> * 舉例:`<el-form v-access="['admin/search']" slot="search-inner-box" :inline="true" :model="searchForm" :rules="searchRules" ref="ruleSearchForm" class="demo-form-inline">...</el-form>` * 上面這個檢索表單須要登陸用戶具備訪問`'admin/search'`接口的權限,纔會顯示 * @param Vue * @private */
const _createRBACDirective = function(Vue) {
  Vue.directive('access', {
    bind: function(el, { value, arg, modifiers }) {
      if (_superAdminStatus) {
        return;
      }
      let isAllow = false
      const statementAuth = _parseAccessDirectiveValue2Arr(value)
      switch (arg) {
        case 'alias':
          isAllow = _checkPermission(statementAuth, _authorizeResourceAlias)
          break
        // 默認使用url模式
        case 'url':
        default:
          if (_isRESTfulInterfaces) {
            isAllow = _checkPermissionRESTful(statementAuth, _authorizeInterfaces)
          } else {
            isAllow = _checkPermission(statementAuth, _authorizeInterfaces)
          }
      }

      if (!isAllow) {
        if (_debug) {
          console.warn(`[v+] RBAC access權限檢測不經過:用戶無權訪問【${_.isObject(value) ? JSON.stringify(value) : value}】`);
        }
        if (_.has(modifiers, 'disable')) {
          el.disabled = true;
          el.style.opacity = '0.5'
        } else {
          el.style.display = 'none';
        }
      }
    }
  })
}


/** * 校驗給定指令顯示聲明所需列表是否包含於身份認證用戶所具備的權限集合中,若是是則返回`true`標識權限校驗經過 * @param statementAuth * @param authorizeCollection * @returns {boolean} * @private */
const _checkPermission = function(statementAuth, authorizeCollection) {
  let voter = []
  statementAuth.forEach(url => {
    voter.push(authorizeCollection.includes(url))
  })
  return !voter.includes(false)
}

/** * {@link _checkPermission} 附加了對接口類型的校驗 * @param statementAuth * @param authorizeCollection * @returns {boolean} * @private */
const _checkPermissionRESTful = function(statementAuth, authorizeCollection) {
  let voter = []
  const expectedSize = statementAuth.length
  const size = authorizeCollection.length
  for (let i = 0; i < size; i++) {
    const itf = authorizeCollection[i]
    if (_.find(statementAuth, itf)) {
      voter.push(true)
      // 移除判斷成功的聲明權限對象
      statementAuth.splice(i, 1)
    }
  }
  // 若是投票獲得的true含量和須要判斷的聲明權限長度一致,則標識校驗經過
  return voter.length === expectedSize
}

const _parseAccessDirectiveValue2Arr = function(value) {
  let params = []
  if (_.isString(value) || _.isPlainObject(value)) {
    params.push(value)
  } else if (_.isArray(value)) {
    params = value
  } else {
    throw new Error('access 配置的受權標識符不正確,請檢查')
  }
  return params
}
複製代碼

在使用指令以前,咱們還須要解決插件所需權限列表的設置:

submit() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          // 登陸
          this.login({
            vm: this,
            username: this.formLogin.username,
            password: this.formLogin.password,
            imageCode: this.formLogin.code
          }).then((res) => {
            // 修改用戶登陸狀態
            this.$vp.modifyLoginState(true);
            //...
            const authorizeResources = parseAuthorizePaths(res.principal.admin.authorizeResources);
            this.$vp.rabcUpdateAuthorizeResourceAlias(authorizeResources.alias);
            const authorizeInterfaces = parseAuthorizeInterfaces(res.principal.admin.authorizeInterfaces);
            this.$vp.rabcUpdateAuthorizeInterfaces(authorizeInterfaces);
          	//...
        }
      })
    }
複製代碼

這裏的parseAuthorizePathsparseAuthorizeInterfaces的做用是解析後端返回的登陸用戶資源和接口列表,這個因人而異,就不貼了;

還須要注意的一點就是,this.$vp.modifyLoginState(true),是vue-viewplus插件登陸身份控制模塊所提供的一個接口,其能夠爲應用維護登陸狀態,好比在監控到後端返回會話超時時候自動將狀態設置爲false,更多請查看*這裏*,這也是邏輯複用的一個好處了;

固然若是你只是想實現本身的權限控制模塊,並不想抽象的這麼簡單,也能夠硬編碼到項目中;

這樣咱們就完成了第二個目標;

哦哦哦忘了寫一下,咱們怎麼用這個指令了,補充一下:

<el-form v-access="{url: 'admin/search/*/*/*', method: 'POST'}" slot="search-inner-box" :inline="true" :model="searchForm" :rules="searchRules" ref="ruleSearchForm" class="demo-form-inline">
      //...
    </el-form>
複製代碼

上面是一個最簡單的例子,即聲明,若是要使用該檢索功能,須要用戶擁有:{url: 'admin/search/*/*/*', method: 'POST'這個接口權限;

另外指令的更多聲明方式,請查看這裏

要【實現發送請求前對待請求接口進行權限檢查,若是用戶不具備訪問該後端接口的權限,則不發送請求,而是友好的提示用戶】這個目標,咱們的方案是:
  1. 得到登陸用戶的:

    • 被受權角色所擁有的資源列表(或資源)所對應的後端接口集合,這一步在實現第二個目標的時候已經完成,即在登陸成功以後:this.$vp.rabcUpdateAuthorizeInterfaces(authorizeInterfaces);,這裏只要複用便可
  2. 攔截請求,這裏咱們應用請求都是基於vue-viewplus的util-http.js 針對axios進行了二次封裝的ajax模塊來發送,它的好處是我80%的請求接口不用單獨寫錯誤處理代碼,而是由改模塊自動處理了,回到正題,咱們怎麼攔截請求,由於該ajax插件底層使用的是axios,對應的其提供了咱們攔截請求的鉤子https://github.com/Jiiiiiin/jiiiiiin-security#表結構和權限說明)

    在具有以上條件以後咱們好像就能夠寫代碼了,嘿嘿:)

咱們繼續往上面的代碼中添加邏輯,下面是代碼實現:

const rbacModel = {
  //...
  install(Vue, {
    //...
    /** * [*] `$vp::onPathCheckFail(to, from, next)` * <p> * 發送ajax請求時權限檢查失敗時被回調 */
    onAjaxReqCheckFail = null
  } = {}) {
    _onAjaxReqCheckFail = onAjaxReqCheckFail;
    this::_rbacAjaxCheck()
  }
};
複製代碼

仍是在插件對象中,首先聲明瞭所需配置的onAjaxReqCheckFail,其次調用_rbacAjaxCheck進行axios攔截聲明:

/** * 用於在發送ajax請求以前,對待請求的接口和當前集合進行匹配,若是匹配失敗說明用戶就沒有請求權限,則直接不發送後臺請求,減小後端沒必要要的資源浪費 * @private */
const _rbacAjaxCheck = function() {
  this.getAjaxInstance().interceptors.request.use(
    (config) => {
      const { url, method } = config
      const statementAuth = []
      let isAllow
      if (_isRESTfulInterfaces) {
        const _method = _.toUpper(method)
        statementAuth.push({ url, method: _method });
        isAllow = _checkPermissionRESTful(statementAuth, _authorizeInterfaces)
        // TODO 由於攔截到的請求`{url: "admin/0/1/10", method: "GET"}` 沒有找到相似java中org.springframework.util.AntPathMatcher;
        // 那樣能匹配`{url: "admin/*/*/*", method: "GET"}`,的方法`temp = antPathMatcher.match(anInterface.getUrl(), reqURI)`
        // 故這個需求暫時無法實現 :)
        console.log('statementAuth', isAllow, statementAuth, _authorizeInterfaces)
      } else {
        isAllow = _checkPermission(statementAuth, _authorizeInterfaces)
      }
      if (isAllow) {
        return config;
      } else {
        if (_debug) {
          console.warn(`[v+] RBAC ajax權限檢測不經過:用戶無權發送請求【${method}-${url}】`);
        }
        if (_.isFunction(_onAjaxReqCheckFail)) {
          this::_onAjaxReqCheckFail(config);
        } else {
          throw new Error('check_authorize_ajax_req_fail');
        }
      }
    },
    error => {
      return Promise.reject(error)
    }
  )
}
複製代碼

這裏可能this.getAjaxInstance()不知道是什麼,在調用_rbacAjaxCheck是咱們指定了this,即this::_rbacAjaxCheck(),而這個this就是$vp對象,即vue-viewplus綁定到Vue實例的$vp屬性;

其餘的就很簡單了,根據配置的_isRESTfulInterfaces屬性看咱們要校驗的是RESTful接口仍是普通接口,若是校驗經過則返回axios所需請求config,若是失敗則調用配置的_onAjaxReqCheckFail通知應用,讓應用去處理權限失敗的狀況,通常也是彈出一個toast提示用戶權限不足。

這樣好像咱們就完成了全部目標,哈哈哈。

寫文章真是比敲代碼累得多呀。

可是不幸的是咱們並無實現第三個目標,問題就在於,上面代碼片斷的TODO中所描述,我沒有解決RESTful PathValue類型接口的權限對比,後端我用的庫是經過:

log.debug("內管權限校驗開始:{} {} {}", admin.getUsername(), reqURI, reqMethod);
                for (Role role : roles) {
                    boolean temp;
                    for (Resource resource : role.getResources()) {
                        for (Interface anInterface : resource.getInterfaces()) {
                            temp = antPathMatcher.match(anInterface.getUrl(), reqURI) && reqMethod.equalsIgnoreCase(anInterface.getMethod());
                            if (temp) {
                                hasPermission = true;
                                break;
                            }
                        }
                    }
                }
複製代碼

org.springframework.util.AntPathMatcher提供的方法來完成的,可是js我沒有找到合適的庫來對比:

{url: "admin/*/*/*", method: "GET"} <> {url: "admin/0/1/10", method: "GET"}
複製代碼

這樣的兩個對象,因此有耐心看到這裏的朋友,若是你解決了這個問題,請聯繫我,謝謝。

謝謝你耐心的看到這裏,若是以爲對你有所幫助,請幫忙支持一下個人兩個項目:

vue-viewplus

jiiiiiin-security

動動小手,求star,哎,哈哈哈。

相關文章
相關標籤/搜索