本人通过对 ant design pro、vue-element-admin 以及对网上后台管理系统教程的学习,得出以下观点,欢迎探讨
权限
解决方案
- 前端方案:用户登录后返回的权限列表,通过权限列表去比对路由表,生成当前用户具有的权限可访问的路由表,通过 router.addRoutes 动态挂载到 router 上。
- 后端方案:如果又更复杂的权限需求,比如每个页面的权限是动态配置的,则应让后端返回当前用户的路由表。
一般来说,使用前端方案就可以满足需求了,毕竟只要你开心,要整多复杂不可以?
前端方案的实现细节:
路由表:路由表是包含整个应用的所有路由,可以分为
- 基础路由(常量路由):一些页面,比如登录、注册、404之类的页面,不需要登录就可以访问的
- 异步路由:需要确定当前用户有权限访问才加载的路由,比如商品管理页面则需要运营的权限才加载该路由
如果是采用该前端权限方案,则路由表里面的 路由记录 需要被标记成需要什么样的权限才能访问该路由(我们这里就由是否有相应的权限来决定是否加载该路由就好了,不用角色来决定),该信息存放在路由元信息里面,也就是路由对象的 meta 属性里面,该属性是 vue-router 定义的,比如你自己定义一个 info 之类的是访问不到的。路由元信息里面一般还放一些路由标题,这样可以在面包屑里面根据路由标题列出当前的路径位置,并可以用路由标题作为 document.title。最后,路由元信息里面还可以放一个 icon,作为该菜单菜单在菜单栏里面的图标
需要考虑的细节问题:
- 假如用户需要跳转到 A 页面,跳转的时候发现用户没有登录,于是自动跳转到登录,等用户登录后如何自动跳到 A 页面?
记录重定向路径:
beforeEach 之类的路由守卫在跳转到登录页面的时候,带上查询参数,类似于:/login?redirect=目标路由
next(`/login?redirect=${to.path}`)
next({ path: loginRoutePath, query: { redirect: to.fullPath } })
跳转到目标路径:
登录成功之后,查看是否有该跳转参数,有的话,就直接跳转到相应页面,否则就跳转到首页
登录成功之后,直接跳转到首页,但是路由守卫里面有设置,不管是什么路由,只要发现有跳转参数,就直接跳转到相应的路径
加载异步路由:
概述:当一个用户打开后台的首页或者某个别的后台页面,这个时候,全局路由守卫(ant design pro、vue-element-admin都用beforeEach这个api)会先检查他有没有登录,一般来说就是检查有没有 token,如果没有,则说明没有登录,会先重定向至登录页面,如果有,也不能保证 token 还依然有效,这个时候会先认为它有效,反正在进入相应的页面的时候,总会向后台发起某些请求的,请求拦截器会带上它那可能已经失效的 token,服务端也肯定会在处理请求之前,在中间件里面检查该 token 的有效性,如果无效则直接返回一个状态码,比如 401(token失效)、403(token有效,但是权限不足),后端对于一个失效的 token 规范一点来说应该返回 401而不是403,前端可以在请求响应拦截器中统一针对返回 401 的情况重定向到登录页面。总的来说,前后端为了提高效率,应共同协商好怎么处理(统一逻辑、状态码等)
访问控制模型
一般可以用 RBAC 模型,即权限跟角色相关,而不是跟某个用户直接相关,无法直接控制某个用户拥有的权限,只能控制该用户的角色,或是修改该用户的角色对应的权限。领导可能会想要这样的功能:能不能直接给某个用户添加一项权限?操作角色与权限之间的关系来控制某个用户的权限,好麻烦。这个时候,千万要慎重,不要以为直接加一个角色权限中间表就完事了,万一某一天,又给你提出一个要求,能不能单独给某个用户删除一项权限?操作角色与权限之间的关系来控制某个用户的权限,好麻烦。。。。其实说到底就是一种权衡,如果用户跟权限直接关联,灵活调整个人的权限,适合用户群体小的场景;如果用角色跟权限关联,则可以高效控制所有用户的权限,适合用户全体多的场景。
前端并不知道角色与权限之间的关系,这种关系记录在后端数据库的 角色-权限 中间表里面。前端只需要获取当前用户的权限数组即可,通过这个数组,去匹配路由表中的路由记录,最后得到当前用户的路由表,用于生成菜单。
关于权限:
权限在后端应该是以权限树的结构来体现的,也就是某个权限下面可能还有子权限,通过类似与pid的字段来关联,比如:
由此产生一个问题,如果用户只拥有 查看人员 的权限,那该用户算不算拥有人员操作的权限呢?也就是后端返回的权限数组应不应该包含 人员操作 ?
应该说算比较合理:
(1)方便前端处理:当用户拥有查看人员的权限时,菜单肯定也要显示人员操作的这一上级菜单项
(2)逻辑更加严谨:不包含的情况下,会导致不一致的行为:如果 人员操作 里面刚开始只有 查看人员 这一项,那显然返回的权限数组就应该包含 人员操作 当添加了 更新人员、删除人员之后,人员操作 就不止 查看人员 这一项了,因此后端返回的权限数组又变得不包含 人员操作 了,这回让人误以为用户的权限被编辑过,而实际上并没有。
token 的处理
token 需要有两个性质:
- 长期性:token 根据业务需求,可能是一天过期,也可能是永久过期,所以它必须要能长期存储,不能只放在 vuex 中,这样刷新页面就没了
- 易用性:存放在 cookie、storage里面不方便使用,为了方便存取,token 也应该放在 vuex 之类的地方,特别是根据是否有 token 来确定页面的组件是否显示
为了满足以上两点,token 应该同时存放在两种不同的地方,典型的是 storage + vuex。放在这两个地方之后,要保证它们的一致性,例如:一般不能出现 token 在 vuex 里面存在,但是在 storage 里面又不存在,或者反之。所以应该在一个函数里面同时对两个地方进行操作,ant design pro 和 vue-element-admin 是在 vuex 里面的 action 里面统一处理的
封装
什么时候需要封装:
当功能、逻辑需要复用的时候,比如:
一个 Message、Notification 组件,虽然这些功能或许非常的简单,但是却非常常用,它们会出现在各个页面中功能、或逻辑可能发生整体改动的时候,比如:
- 存储 token 的实现从 cookie 改为 storage
- 网络请求库从 axios 换成别的库
什么时候不必封装:
- api 封装:由于 ant design pro、vue-element-admin 以及网上教程对 api 的封装无非就是返回一个 promise, 而且这种 api 很少有复用的场景,比如,一个 login api,通常只会出现在登录页面,如果其他页面也需要登录,一般都是先跳转到登录页面,登录成功之后再跳至相应页面,像 ant design pro 的一个封装的 api 函数:
export function getSmsCaptcha (parameter) {
return request({
url: userApi.SendSms,
method: 'post',
data: parameter
})
}
请问这样的封装相比直接在页面里写有什么意义?