如何优雅实现应用鉴权之Auth鉴权组件

1,421 阅读7分钟

鉴权是前端应用的重要组成部分,对于绝大多数应用来说,鉴权是其必不可以少的组成部分。此外,鉴权功能一般也是在应用中多个地方不同页面中重复出现的。如果我们简单粗暴地在需要进行鉴权的地方利用 if else 条件做判断,那么将会出现一下问题。

  • 代码多处重复,不利于维护。 如果需要进行权限修改,经常需要修改多个地方,难以保证不会有遗漏的地方
  • 不利于代码调试。 如果需要验证不同权限下的页面功能,通常需要手动改动多处代码,调试起来非常麻烦
  • 对于不同页面或者不同模块往往鉴权策略也不一样,比方说:路由鉴权和可见性鉴权。这时候多处关于鉴权的代码和多处不同鉴权策略代码出现在项目代码中,很难保证代码的可读性。

既然如此,那么有什么方法可以解决这种问题呢?

下面我们来介绍一下关于Auth鉴权组件(本文鉴权组件是基于vue的,但是其中思想同样适用 react/angular 框架)。需要提前说明的是Auth鉴权组件只是解决了以上鉴权功能实现的一部分问题,其他问题的解决方式我们放到下一篇文章再详谈。

Auth 组件主要解决的问题是关于页面模块的可见性(是否visibility)和功能可操作性(是否disabled)的。一般来说,对于有鉴权需要的系统,针对不同用户页面的功能和模块看到的内容也是不同的。我们需要根据用户当前所具备的权限给用户展示不同的内容或者让用户操作不同的功能模块。比方说,普通用户只具备播放不需要会员的影片,而黄金会员用户可以播放普通用户之外的黄金会员可见的影片,而更高级的会员铂金会员则可以观看所有片源,此外还有管理员可以查看所有片源和操作所有片源(增删查改)。那么针对这样的鉴权功能,其中需要实现的逻辑有哪些呢?下面我们简单列举一下:

  • 系统权限有:普通用户, 黄金会员, 铂金会员,管理员
  • 普通用户:播放普通用户可播放的片源,可以看见其他会员可看的片源但是不可以播放
  • 黄金会员:可以播放除普通用户可播放的片源外加黄金会员可播放片源,可见铂金会员可播放的片源
  • 铂金会员:可以播放所有片源
  • 管理员: 可以播放所有片源,此外可以操作所有片源,操作片源功能对于用户不可见

仔细分析我们可以看出鉴权有两种方式,一种是可见但是不可以操作,一种是连看都看不到,完全不能觉察功能的存在对于没有权限的用户而言。此外,对于内容的可见性,一般实现方式也存在以下3种:

  • v-if 即是否生成DOM
  • v-show 即DOM的display 是否为none
  • visibility 即DOM占位了但是对用户可不可见(主要是使用了visivility 属性 (此外还有opacity,但是opacity应用可能非常少))

以上三种方式什么时候选择哪种,是有所要求的,对于上面的播放片源需求就应该使用v-if,因为这一部分我们压根就不应该让用户有看到的可能性。(v-show和visibility都可以通过操作css属性让隐藏元素可见)而v-show和visivblity方式适用于内容不重要只是在不同场合时间选择是否显示,其中v-show不占位而visibility占位但绑定事件不会出发,两种方式选择也是要根据业务场景做判断。

下面我们来实现Auth组件。

Auth组件的原理也是利用v-if v-show和visibility属性实现的,Auth组件的本质是一个高阶组件。对传入其中的slot(DOM)进行权限判断后选择是否显示内容如何显示内容。但是需要注意的是我们引入Auth组件不应该对原来DOM结构产生影响,也就说,有权限的情况下传入的DOM显示,也只显示传入的DOM,不会对原有的传入的DOM做修改(增加或者减少标签)这主要是考虑到可以无痛引入Auth组件也可以无痛移除Auth组件。因为如果修改了DOM元素会在加Auth和不加Auth情况下页面的布局产生影响。举个例子:

     <Auth>
           <div class="div1">div1<div>
           <div class="div2>div2<div>
     <Auth>

假设转换后会在原来DOM基础上加了一层 Div 套住传入的slot

     <div>
           <div class="div1">div1<div>
           <div class="div2>div2<div>
     <div>

如果在没有Auth组件时,div1/div2 都是flex item(即他们的父节点display:flex/inline-flex),那么在有Auth和没有Auth情况下元素布局是不同的。这也是我们在设计Auth组件应该避免的问题。

此外,为了开发调试方便,我们应该在Auth组件设置一个开关,这个开关可以让我们一键赋予最高权限,无视权限限制。一般而言,权限都是存在高低的,所以在设计权限时,我们可以用number类型代表权限,Infinity 代表最高权限,0 / 10 / 20 如此递增表示权限的高低,越大权限越高(之所以不设计成连续整数主要是为了方便后期权限的扩展)。所以权限限制就变成了简单的数值比较。当然,有些时候权限的限制比较灵活,不能仅仅用数值大小进行判断,这个时候应该允许采用自定义鉴权策略。

下面是 Vue 实现的 Auth 组件代码

export default {
  functional: true,
  name: 'Auth',
  props: {
    userPermission: {
      type: [String, Number, ],
      required: true,
    }, // 用户权限
    minLimit: {
      type: [String, Number, ],
      default: -Infinity,
    }, // minLimit 模块鉴权通过的最低限制
    privilege: {
      type: Boolean,
      default: false,
    }, // privilege (特权)为 true 时无视鉴权
    mode: {
      type: String,
      default: 'destroy',
      validator: (value) => {
        return ['destroy', 'display', 'visibility', 'manual', ].includes(value) // manual 可以让开发者拿到鉴权结果,利用结果对功能进行 enable 和 disable。
      },
    },
    hooks: {
      type: Object,
      default: () => ({}), // hook 用于获取鉴权结果
    },
    customStrategy: {
      type: Function,
      default: null,
    },// customStrategy 自定义鉴权策略
  },
  render (h, context) {
    const props = context.props
    const slots = context.scopedSlots
    const defaultSlot = slots.default && slots.default()
    const defaultStrategy = (userPermission) => (userPermission >= props.minLimit)
    const strategy = props.customStrategy ? props.customStrategy : defaultStrategy
    props.hooks.pass = false
    if (strategy(props.userPermission)) {
      if (props.mode === 'manual') {
        props.hooks.pass = true
      } else {
        return defaultSlot
      }
    } else {
      if (props.mode === 'destroy') {
        return void (0)
      } else if (props.mode === 'display') {
        const vShow = {
          expression: 'true',
          name: 'show',
          rawName: 'v-show',
          value: false,
        }
        defaultSlot.forEach((vnode) => {
          if (vnode ?.data ?.directives) {
            vnode.data.directives.push(vShow)
          } else {
            vnode.data.directives = [vShow, ]
          }
        })
        return defaultSlot
      } else if (props.mode === 'visibility') {
        defaultSlot.forEach((vnode) => {
          if (vnode ?.data ?.staticStyle) {
            vnode.data.staticStyle = { ...vnode.data.staticStyle, visibility: 'hidden', }
          } else {
            vnode.data.staticStyle = { visibility: 'hidden', }
          }
        })
        return defaultSlot
      } else if (props.mode === 'manual') {
        props.hooks.pass = false
      }
    }
  },
}

至此,我们关于Auth鉴权的组件的实现就讲完了。回顾一下可以发现,利用Auth组件,我们可以很方便的将应用中关于鉴权的逻辑集中到鉴权组件中,这样可以有效减少代码的重复和冗余。此外,对于用户权限我们可以存储在全局state中,这样子就不用多次去查询用户权限。而且,Auth 组件支持多种模式的鉴权模式'destroy', 'display', 'visibility', 'manual' 四种。同时,也支持自定义鉴权策略,可以说是非常灵活了,应用场景也是足够广泛的。

最后,行文仓促,难免存在纰漏,还望不吝指教。