现如今我们访问的每个网站,或多或少都会存在「权限控制」。
以一个博客系统为例,系统内共有三种角色
- 1、游客
- 2、会员
- 3、管理员
不同角色支持不同的操作,比如
- 1、游客可以访问博客列表页、博客详情页
- 2、会员除游客权限外,还可以提交博客、提交评论、对博客进行编辑、删除操作等。
- 3、管理员除上述权限外,还可访问用户管理页对用户进行管理。
上述说明「什么用户能够进行什么操作,不能进行什么操作」,可以理解为就是「权限」,根据这些权限,做出正确的处理,就是「权限控制」,而「角色」,是「权限」的集合,也方便我们对用户的定义。
一、为什么要做权限控制
最主要的原因是有了「用户」的概念,需要对不同的用户提供不同的服务。
如果一个博客系统没有权限控制,游客可以任意发布、编辑、删除博客,整个系统就乱了套,无法进行有效的管理。
不过,这些都是针对后端而言的,对前端来说,做权限控制,很大程度上是为了用户体验,下面会解释这个观点。
二、如何做权限控制
1、后端的权限控制
既然提到了「权限控制仅针对后端有用」,那么后端是如何做权限控制呢?
以Django
为例,初始化项目后,就会预置'django.contrib.auth'
应用,该应用提供了现成的用户表、登录注册功能以及用户权限相关的功能,无需开发者自己实现。
它的权限很简单,除了是否为管理员,就是对指定「数据表」是否有add
、change
和delete
的权限了。
当后端接收到请求,会做如下处理:
- 1、获取当前请求属于哪个用户发出
- 2、获取该用户拥有什么权限
- 3、想要进行什么操作
- 4、是否有该操作的权限
- 5、成功操作或失败
通过条件判断,给出用户操作的结果,就完成了后端的权限控制。
2、前端的权限控制
前端的权限控制,往往通过隐藏DOM
元素实现,比如博客页面,作者和管理员可以看到「删除博客」按钮,游客则看不到。
仅仅这样就够了吗?是的,这样就够了。
作为前端开发,我们都听过「前端不可信」,即后端无论如何都不能相信前端发送的请求数据。
无论Web
端还是移动端,都是可以被「破解」的,通过抓包等手段,可以得到发送的实际请求,从而伪造请求并发送。
假设会员 A 写了一篇博客,博客序号为 20,通过控制台得到删除该博客的请求为:
1 | DELETE /api/blog/20/ |
会员A 退出登录后,身份变为了游客,当他访问另一篇序号为 21 的博客时,恶意的使用控制台发送DELETE /api/blog/21/
的请求。
如果后端完全相信前端而没有做权限判断,那博客 21 的作者就无故丢失了一篇博客。所以后端仍需要对发起请求的用户进行权限的判断,并给出正确的处理。
问:既然后端都做好了权限控制,前端再做一次是不是没有必要?
从「处理权限并返回处理结果」上来说,的确是没有必要。
如果前端没有对游客隐藏「删除博客」按钮,游客点击删除后,返回提示「没有权限操作」,游客将十分困惑,「为什么能让我点击却又告诉我没有权限?」。
从另一方面,尝试过一次「没有权限」的提示后,大部分都不会再去点了,但是可能会有一部分继续「疯狂操作」,就为了看看错误提示。。。
对应这种情况,前端隐藏删除按钮后,提高了「误操作」成本,可以减轻一部分服务器的压力,避免处理一些无意义的请求。
但是对于恶意用户,总是可以找出办法「疯狂操作」,所以说前端的权限控制,很大程度上是为了更好的用户体验。
三、前端权限控制具体方案
在之前后端渲染模板的做法,当用户请求页面时,就可以得到用户角色,并判断是否有权限,如果没有则重定向到登录页或者错误页。
模板渲染时,也可以在模板内根据用户角色决定是否渲染和权限有关的DOM
。
而现在流行的单页面应用,路由都交给了前端控制,模板内也无法直接获取到用户从而决定是否渲染,一切都交给了js
处理。
总体来说,分为三步,对,就和「如何将大象装进冰箱」这个问题的答案一样。
- 1、获取用户角色
- 2、保存用户角色
- 3、判断用户角色并作出不同处理
1、获取用户角色
要根据用户角色做不同的处理,首先要获取到用户角色。可以在登录时一并返回,也可以提供一个独立的接口用以请求。
2、保存用户角色
角色往往和其他用户信息一样,保存在localStorage
,也是因为除了这里,没有其他合适的地方了。
也许会说保存在
localStorage
会不会不安全,很容易被篡改。的确可能,所以前面也提到,前端的权限,只是为了更好的用户体验,真正的权限控制必须要由后端来做。
3、根据角色进行处理
当然,最难的就是这一步了,核心原理,就是if
条件判断(废话),假设我们有这些角色:
1 | const ROLE_MAP = { |
这是已登录用户,如未登录,则用户角色为「游客」。登录后会返回如下信息:
1 | { |
用户角色为 2,表示是「管理员」,所以能够看到「删除博客」按钮。
以vue
举例,简单在指定组件上使用v-if
指令即可。
1 | <template> |
如果是react
则稍微麻烦一丢丢:
1 | class App extends React.Component { |
完美解决~下班回家
产品:等下,我觉得吧,管理员还是不能删博客,另外再加一个「专门删博客管理员」角色,只有他能删吧。
…好的没问题,简单。增加角色,并将2
改成了3
(专门删博客管理员)。
产品:为什么博客列表页管理员还是有删除按钮啊?
啊,博客列表页还有删除按钮啊,我再补一下。。。
1 | class BlogList extends React.Component { |
产品:我又有个新想法。。。
四、权限组件
为了避免这种枯燥且易出错的的方式,我们可以参考后端,将用以与用户交互的组件定义为「资源」,同时维护一组「角色」拥有的「资源」对照表。
1 | import React from "react"; |
然后就可以在页面上使用该组件了:
1 | class App extends React.Component { |
如果出现需求反复变更的情况,只需要编辑PERMISSION_MAP
这一个对象即可,即使删除博客按钮会出现在多个地方。
这种方式可以减轻一部分维护成本。
1、核心逻辑
核心逻辑还是之前提到的if
判断,只是多了一个组件,将if
条件判断移到了该组件内。
所以任何框架都适用,再以vue
为例,要实现上述功能,该怎么做。
2、vue 实现
1 | <template> |
使用:
1 | <Permission source="deleteBlog"> |
3、jQuery 实现
不同于使用框架可以决定是否渲染指定组件,采用jQuery
只能隐藏或者移除指定的DOM
,而且也不够优雅。
1 | <body> |
五、页面访问权限
上面实现的权限组件,似乎只能处理页面内的组件,能处理页面吗?
现在博客系统新增需求,游客无法访问所有页面,只能注册成为会员,登录后进行访问。
使用vue
的情况下,官方推荐在路由钩子内,进入页面前对当前用户权限进行判断并处理,能够进入,或者重定向到登录页。
同样的,我们可以将「页面」视为资源,判断指定角色是否拥有该资源即可。
使用react
则麻烦一些,由于react-router
没有提供路由钩子,无法像vue
一样统一处理,
但仍然可以和处理页面组件权限一样,有两种方案:
- 1、修改页面组件
- 2、修改
Route
组件
1、修改页面组件
下面是博客列表页的代码:
1 | class Blogs extends React.Component { |
虽然能够在没有权限的情况下无法看到页面了,但显示一片空白也不好,我们可以在Permission
内不返回null
,而根据条件比如指定当前是处理页面,就返回一个NotFound
页面。
1 | export default class PermissionWrapper extends React.Component { |
2、修改 Route 组件
1 | export default class PermissionRoute extends React.Component { |
核心逻辑还是一致的,判断用户权限,如果有权限,则返回正常的<Route>
,就可以渲染出预期的页面。否则显示NotFound
页面。
六、缺点
虽然解决了一部分问题,但也引入了「资源」的维护成本,对资源的描述需要足够准确,才能直观的从权限对照表中看出资源对应的组件到底是什么。
同时对于一些复杂的权限处理无法胜任,还是以「删除博客」按钮为例,博客的作者对博客有删除权限。
那么,博客删除按钮只应该出现在作者属于「当前登录用户」的博客页面,如果访问其他作者的博客详情页,只是简单判断当前用户「是否有权限」,还是能够看到「删除博客」按钮。
即不仅是「有没有权限」,还涉及到「是不是」的问题。