vuejs单页应用的权限管理实践

原文转载自: juejin.im/post/5abaec…

在众多的B端应用中,简单如小型企业的管理后台,仍是大型的CMS,CRM系统,权限管理都是一个重中之重的需求,过往的web应用大多采起服务端模板+服务端路由的模式,权限管理天然也由服务端进行控制和过滤.可是在先后端分离的大潮下,若是采用单页应用开发模式的话,前端也无可避免要配合服务端共同进行权限管理,接下来会以vuejs开发单页应用为例,给出一些尝试方案,但愿也能给你们提供一些思路.注意采用nodejs做为中间层的先后端分离不在此文讨论范围.html

目标

关于权限管理,因为本人对服务端并不能算得上十分了解,我只能从我以往的项目经验中进行总结,并不必定十分准确.前端

通常权限管理分为如下几部分.vue

  • 应用使用权
  • 页面级别权限
  • 模块级别权限
  • 接口级别权限

接下来会逐一讲解上述部分.完整的实例代码托管在github-funkyLover/vue-permission-control-demo上.node

应用使用权-登陆状态管理与保存

首先应用使用权其实就是简单的判断登陆状态而已.在不少C端应用,登陆以后能使用更多的功能在必定程度上也能够算做权限管理的一部分.而在B端应用中通常表现为不登陆则不能使用(固然还能使用相似找回密码之类的功能).ios

以往登陆状态的保持通常经过session+cookie/token管理,用户在打开网页时就带上cookie/token,由后端逻辑判断并进行重定向.在SPA的模式下,页面跳转是由前端路由进行控制的,用户状态的判断则须要由前端主动发送一次自动登陆的请求,根据返回结果进行跳转.git

这个自动登陆的逻辑能够深挖作出多种实现,例如登陆成功以后把用户信息加密并经过localstorage在多个tab之间公用,这样再新打开tab时就不须要再次自动登陆.这里就以最简单的实现来进行讲解,基本流程以下:github

  1. 用户请求页面资源
  2. 检查本地cookie/localstorage是否有token
  3. 若是没有token,无论用户请求打开的是哪一个路由,都一概跳转到login路由
  4. 若是检查到token,先请求自动登陆的接口,根据返回的结果判断是进入用户请求的路由仍是跳转到login路由

而关于用户状态的判断,通常应该针对进入login路由(包括忘记密码之类的路由)和进入其余路由进行判断,在基于vuejs@2.x的前提下,能够在router的beforeEach钩子上进行用户状态判断并切换路由便可.下面给出部分代码:web

const routes = [
  {
    path: '/',
    component: Layout,
    children: [
      {
        path: '',
        name: 'Dashboard',
        component: Dashboard
      }, {
        path: 'page1',
        name: 'Page1',
        component: Page1
      }, {
        path: 'page2',
        name: 'Page2',
        component: Page2
      }
    ]
  }, {
    path: '/login',
    name: 'Login',
    component: Login
  }
]

const router = new Router({
  routes,
  mode: 'history'// 其余配置
})

router.beforeEach((to, from, next) => {
  if (to.name === 'Login') {
    // 当进入路由为login时,判断是否已经登陆if (store.getters.user.isLogin) {
      // 若是已经登陆,则进入功能页面return next('/')
    } else {
      return next()
    }
  } else {
    if (store.getters.user.isLogin) {
      return next()
    } else {
      // 若是没有登陆,则进入login路由return next('/login')
    }
  }
})
复制代码

在设定好跳转逻辑后,咱们则须要在login路由中检查是否有token并进行自动登陆ajax

// Login.vueasync mounted () {
  var token = Cookie.get('vue-login-token')
  if (token) {
    var { data } = await axios.post('/api/loginByToken', {
      token: token
    })
    if (data.ok) {
      this[LOGIN]()
      Cookie.set('vue-login-token', data.token)
      this.$router.push('/')
    } else {
      // 登陆失败逻辑
    }
  }
},
methods: {
  ...mapMutations([
    LOGIN
  ]),
  async login () {
    var { data } = await axios.post('/api/login', {
      username: this.username,
      password: this.password
    })
    if (data.ok) {
      this[LOGIN]()
      Cookie.set('vue-login-token', data.token)
      this.$router.push('/')
    } else {
      // 登陆错误逻辑
    }
  }
}
复制代码

同理退出登陆时把token置空便可.注意这里给出的逻辑实现相对粗糙,实际应该根据需求进行改动,例如在进行自动登陆的时候给用户适当的提示,把读取/存储token的逻辑放进store中进行统一管理,处理token的过期逻辑等.vue-router

页面级别权限-根据权限生成router对象

这里能够借助vue-router/路由独享的守卫来进行处理.基本思路为在每个须要检查权限的路由中设置beforeEnter钩子函数,并在其中对用户的权限进行判断.

const routes = [
  {
    path: '/',
    component: Layout,
    children: [
      {
        path: '',
        name: 'Dashboard',
        component: Dashboard
      }, {
        path: 'page1',
        name: 'Page1',
        component: Page1,
        beforeEnter: (to, from, next) => {
          // 这里检查权限并进行跳转
          next()
        }
      }, {
        path: 'page2',
        name: 'Page2',
        component: Page2,
        beforeEnter: (to, from, next) => {
          // 这里检查权限并进行跳转
          next()
        }
      }
    ]
  }, {
    path: '/login',
    name: 'Login',
    component: Login
  }
]
复制代码

上面代码是足以完成需求的,再配合上vue-router/路由懒加载也能够实现对于没有权限的路由不会加载相应页面组件的资源.不过上述实现仍是有一些问题.

  1. 当页面权限足够细致时,router的配置将会变得更加庞大难以维护
  2. 每当后台更新页面权限规则时,前端的判断逻辑也要跟着改变,这就至关于先后端须要共同维护一套页面级别权限.

第一个问题尚且能够经过编码手段来减轻,例如把逻辑放到beforeEach钩子中,又或者借助高阶函数对权限检查逻辑进行抽象.可是第二个问题倒是无可避免的,若是咱们只在后端进行路由的配置,而前端根据后端返回的配置扩展router呢,这样就能够避免在先后端共同维护一套逻辑了,根据这个思路咱们对以前逻辑进行一下改写.

// Login.vueasync mounted () {
  var token = Cookie.get('vue-login-token')
  if (token) {
    var { data } = await axios.post('/api/loginByToken', {
      token: token
    })
    if (data.ok) {
      this[LOGIN]()
      Cookie.set('vue-login-token', data.token)
      // 这里调用更新router的方法this.updateRouter(data.routes)
    }
  }
},
// ...
methods: {
  async updateRouter (routes) {
    // routes是后台返回来的路由信息const routers = [
      {
        path: '/',
        component: Layout,
        children: [
          {
            path: '',
            name: 'Dashboard',
            component: Dashboard
          }
        ]
      }
    ]
    routes.forEach(r => {
      routers[0].children.push({
        name: r.name,
        path: r.path,
        component: () => routesMap[r.component]
      })
    })
    this.$router.addRoutes(routers)
    this.$router.push('/')
  }
}
复制代码

这样就实现了根据后端的返回动态扩展路由,固然也能够根据后端的返回生成侧栏或顶栏的导航菜单,这样就不须要再在前端处理页面权限了.这里仍是要再提醒一下,本文的例子只实现最基本的功能,省略了不少可优化的逻辑

  1. 每打开新的tab(非login路由)时都会从新自动登陆并从新扩展router
  2. 每打开新的tab,自动登陆以后依然会跳转到/路由,就算新打开的url为/page1

解决思路是把用户登陆信息和路由信息存储在localstorage中,当打开新tab时直接经过localstorage中存储的信息直接生成router对象.借助store.jsvuex-shared-mutations一类的插件能够必定程度上简化这部分逻辑,这里不展开讨论.

模块级别权限-组件权限

模块级别的权限很好理解,其实就是带权限判断的组件.在React中借助高阶组件来定义须要过滤权限的组件是很是简单且容易理解的.请看下面的例子

const withAuth = (Comp, auth) => {
  returnclassAuthComponentextendsComponent{
    constructor(props) {
      super(props);
      this.checkAuth = this.checkAuth.bind(this)
    }

    checkAuth () {
      const auths = this.props;
      return auths.indexOf(auth) !== -1;
    }

    render () {
      if (this.checkAuth()) {
        <Comp { ...this.props }/>
      } else {
        returnnull
      }
    }
  }
}
复制代码

上面的例子展现的就是有权限时展现该组件,没有权限时则隐藏组件们能够根据不一样权限过滤需求来定义各类高阶组件来处理.

而在vuejs中可使用经过render函数来实现

// Auth.vue
import { mapGetters } from 'vuex'

export default {
  name: 'Auth-Comp',
  render (h) {
    if (this.auths.indexOf(this.auth) !== -1) {
      return this.$slots.default
    } else {
      return null
    }
  },
  props: {
    auth: String
  },
  computed: {
    ...mapGetters(['auths'])
  }
}
// 使用
<Auth auth="canShowHello">
  <Hello></Hello>
</Auth>
复制代码

vuejs中的render函数提供彻底编程的能力,甚至还能在render函数使用jsx语法,得到接近React的开发体验,详情参考vuejs文档/渲染函数&jsx.

接口级别权限

接口级别的权限通常就与UI库关联不大,这里简单讲一下如何处理.

  1. 首先从后端获取容许当前用户访问的Api接口的权限

  2. 根据返回来的结果配置前端的ajax请求库(如axios)的拦截器

  3. 在拦截器中判断权限,根据需求提示用户便可

    axios.interceptors.request.use((config) => { // 这里进行权限判断if (/* 没有权限 */) { returnPromise.reject('no auth') } else { return config } }, err => { returnPromise.reject(err) })

其实我的认为前端也不必定有必要对请求的api进行权限判断,毕竟接口不像路由,路由如今已经由前端来管理了,可是接口最终都须要经过服务器的校验.能够视需求加上.

后记

写得比较乱,像流水帐似的,完整的实例代码在github-funkyLover/vue-permission-control-demo,若有问题或者意见请评论留言,我必虚心受教.

相关文章
相关标签/搜索