前端如何配合后端完成RBAC权限控制

关联上一篇 Vue 前端应用实现RBAC权限控制的一种方式 ,这篇写的比较懒,哈哈,还是谢谢支持的朋友。

承蒙 李杨 的邀请,就写了这篇文章,平时写的不多,失误之处,请大家多多包涵。

因为工作的原因要找一个管理端模板,用于开发一个网银系统的前端界面骨架,我就找到了 d2-admin ,看着十分对胃口,接着我就想着先做一个前后端分离的demo,来看一下d2-admin模板是否满足我们项目的需求,那么一个权限管理demo我觉得很适合拿来做实验,就此我做了 jiiiiiin-security权限系统 项目,以下是部分功能截图:

为什么我们需要前端实现RBAC

在说我们前端为什么要实现权限控制之前,大家势必要了解一下我们要实现的东西的本质是什么,下面简单引用两句介绍:

RBAC 以角色为基础的访问控制 (英语: Role-based access controlRBAC ),RBAC认为权限授权实际上是Who、What、How的问题。在RBAC模型中,who、what、how构成了访问权限三元组,也就是“Who对What(Which)进行How的操作”。

RBAC是一种思想,任何编程语言都可以实现,其成熟简单的控制思想 越来越受广大开发人员喜欢。

更多内容,请大家不熟悉的势必自行google;

我认为前后端是相辅相成的,所以要做好前端的权限控制,如果能提前了解后端的权限分配规则和数据结构是能够更好的进行相互配合的,当然如果完全不理会后台的权限划分,硬性来做上面的两个需求也是能实现的,只是不掌握全局,就很难理解这样做的意义何在,所以建议大家在考虑这个问题的时候(这里指前端同学),还是要大概去看看RBAC的概念,属性经典的表结构,从而属性后台权限分别的业务规则。

“权限管理”一般大家的印象中都属于后端的责任,但是这两年随着SPA应用的兴起,很多应用都采用了前后端分离的方式进行开发,但是纯前端的开发方式就导致很多以前由后端模板语言硬件解决的问题,现在势必要重新造一次轮子,而这个时候前端我认为是 配合 后端对应语言的安全框架根据自身的业务需要来实现,在这里就说说我们的需求:

  1. 完善我们自己的Vue插件 vue-viewplus 的业务模块(这个插件是我们经过一年的内部使用,用来将一些开发应用所需的公共需求,抽取为一个个模块,方便进行快速的应用开发所写)
  2. 我们认为如果在前端根据后端配置的权限规则就能拦截一些不必要的请求,就能减少后端不必要的资源损耗,也能更快的提示 正常用户
  3. 我们需要解决管理端界面菜单和按钮根据后端权限配置隐藏显示的需求
  4. 我们需要解决前端视图可访问性根据后端权限配置动态调整的需求

以上2、3、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,哎,哈哈哈。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章