在前后端分离的架构下进行用户权限管理是一件相对麻烦的事情,传统 MVC 架构的后台系统中,URL 路由跳转是由服务端解析执行的,因而很容易可以在页面跳转以前拦截请求,完成权限检查。相对的在前后端分离的情况下,以 Vuejs 全家桶为例,在使用 vue-cli + vue-router + vuex 脚手架开发的网页中,路由解析的任务由 vue-router 完成,虽然同样可以在页面跳转以前拦截请求来检查权限,但客户端 javascript 始终是不安全的,用户很容易可以绕过权限检查的逻辑来直接访问原来没有权限访问的页面。

在对安全要求极端苛刻的条件下,权限检查应该始终依赖服务器端,也就是说每当客户端要访问一个页面之前,都应该向后台发起请求来询问是否具有权限,同时为了请求不被篡改,所有权限相关的请求和数据都最好使用非对称加密算法进行加密,不难想象,这样操作带来的性能开销和用户使用体验的损失都是巨大的。

既然纯前端或者纯后端的权限管理都有问题,那么,在不降低用户体验的前提下,权限管控的最理想方案应该是前端+后端相互配合的方案。

前后端配合完成权限检查,同样也会带来以下的疑惑:

  • 前后端应该如何分工?

  • 权限信息应该放在前端还是后台?

  • 如果用户的权限信息放在前台,具体该如何存储?使用 localstorage 还是 vuex,或者其他?数据要不要加密?如果加密如何解密……

为了解决上面的困惑,这里首先需要明确两个观点:

  • 无论在客户端如何巧妙地管理权限信息,客户端总是有办法绕过权限检查获得访问权限;

  • 权限管理的作用对象应该分为两部分:页面 + 数据,即使客户端绕过了前端权限检查,页面展示的数据还是要从后端请求获取;

理想情况下,客户端进行权限管理的目的应该只是为了方便页面展示,具体的数据获取权限应该由后台来管理,即使客户端绕过前端权限检查,进入页面后获取数据时候后台同样可以返回 403 错误,这样就能最大程度上降低系统被攻破的风险,同时还能有更好的用户体验。

回到上面的问题:

  • 前后端如何分工:前端通过获取的用户权限列表控制相关链接或按钮是否展示,后台负责检查请求数据的 API 接口权限;

  • 权限信息应该放在前端还是后台:其实都可以,方便起见,可以在登录后就获取用户的权限列表存在客户端,就不用每次都去后台请求;

  • 权限信息如何存放的问题:既然前端权限只是服务于页面展示,那就可以不用考虑过多安全因素(反正怎么都不安全……),存储的介质当然就没有要求了,只要方便页面展示放哪里都可以。

talk is cheap, show me the code

前端部分实现

以之前 vuejs 全家桶实现的项目为例,前端权限检查主要的实现代码:

1、权限定义,规定系统中所有可用的权限以及前后端权限名称的对于关系:permissions.js

export default {
  ...
  // 规定对象属性作为前端使用的权限名称,值为后台使用的权限名称
  viewUserList: 'user.list.view'
  ...
}

2、权限检查工具:util.js

import permissions from './permissions'

export default {
  has: function (access) {
    let userAccessList = this.getUserAccessList()
    return access in permissions && userAccessList.indexOf(access) > -1
  },
  getUserAccessList: function () {
    // 获取客户端已保存的用户权限列表
  }
}

3、路由列表中在 meta 字段定义每个页面路由对应的权限:router.js

{
  ...
  path: '/user/list',
  meta: {
    permission: 'viewUserList'
  }
  ...
}

4、路由跳转之前检查权限:

import util from './util'
import router from './router'

router.beforEach((to, from, next) => {
    let access = to.meta.permission
    if (!util.has(access)) {
      alert('access denied!')
      next(false)
    } else {
      next()
    }
})

5、为了方便在页面上控制按钮或者链接的展示,通过封装 mixin 全局函数进行权限检查:mixin.js

import util from './util'

export default {
  methods: {
    has: function (access) {
      return util.has(access)
    }
  }
}

在页面中使用方法例如:

<button type="submit" v-if="has('viewUserList')">User List</button>

也可以封装自定义指定:

import util from './util'

Vue.directive('has', {
  binding: function (el, binding) {
    if (!util.has(binding.value)) {
      el.parentNode.removeChild(el)
    }
  }
})

使用方法:

<button type="submit" v-has="'viewUserList'">User List</button>

后台实现

利用 Laravel 框架提供的中间件功能可以在用户请求到来之前检查是否具有相关的权限,否则返回 403 异常:AccessMiddleware.php

class AccessMiddleware extends Middleware
{
    ...
    public function handle($request, Closure $next)
    {
        // 如果当前用户不具备要访问接口的权限,直接返回 403 forbidden
        if (! canAccess($this->user, $this->access)) {
            abort(403);
        }

        $next();
    }
}