前言
在开讲之前,先列举几个场景:
Hi,今天那个销售总监说要设立几个销售经理的职位,然后每个经理管理自己小组的销售员,我们把用户的销售数据按组分开来吧。
场景二Mario,今天那个市场部的说要分立几个板块,公众号的管理所有文章投稿评论,推广管各平台宣传策略方案与实施,对,竞品的相关资料数据也要分立出来,我们要把这些分开来。之前讨论的销售部的事情和这个没关系哦。
场景三那个,xx店的店主说,他管理的两家门店,想同时处理两家店的事情,需要兼顾两家的销售、采购的数据。人家提的需求场景有道理,你给他做一下吧,对了,不要影响其他店的管理哦!
场景四今天总部的运营小x说,他们需要以门店、部门、单个用户的角度看所有的运营数据,给他们做一下吧,每天导 excel 要疯了。
开始
当我们谈及权限的时候,很容易想到在用户发起请求时,对接口做一层权限验证,或者是对数据库读写限制权限,简单粗暴。因为是以用户操作为出发点进行限制,所以我们在一个接口的 handler 中定义:
if not user in [‘xxx’, ‘yyy’, ‘zzz’]:
return ‘has no permission!’
或者在处理数据模型的 model 层定义:
access_users = [‘xxx’, ‘yyy’, ‘zzz’]
这样限制住了用户的操作行为。不过这个硬编码的形式简直惨不忍睹,今天加一个接口,我们在 handler 代码中来一段这个权限定义,明天另一个 handler 的权限变动了,我们再去改一下那个代码。你说写在一个装饰器中统一管理,ok,那么在添加、修改 handler 的同时,还要去找到对应的具有该权限的用户,在装饰器中进行相关定义。在一个产品不断迭代的情景下,这样是不能被接受的。所以我们有必要引入一个模型来规范化权限控制。
结构化管理
通过以上情景,我们很容易得到一种权限结构模型(参考 django)以 user(单一用户)、group(用户组)、permissions(单一权限)三层两两之间多对多的关系,实现了当用户请求接口 APIa,此时进行获取用户,获取用户所属组,获取所属组所具备的权限,若获取不到记录,则限制掉此次请求。运用这个模型,我们每次将 permissions 定义成:module、function 的形式,同时定义 request method 。这样将具有权限的 group 关联到一个 permission(即一个 handler)上。同时将用户关联到用户组,从而可以在不断变动权限的情况下,配置一次对应关系,将用户权限限制到单个 handler 上。
用 python 的同学知道,由于 python decorator @的语法糖,我们很喜欢用这种形式来处理这些与业务逻辑无关的代码,例如:日志记录、检查缓存、检查 user session等。有时候在一个方法装饰器写的像盖楼一样,稍不留神就忘记了。有时候开发者对项目不熟悉,在写一个新的接口之后,配置了权限,却忘记加装饰器,导致这部分一直没有被限制。
@route()
@login_required
@check_cache
@add_log
@permission_verify
@transaction.atomic
def interface_a():
所以在这里引入一个中间件,专门处理这个事情,不必每次增加额外代码。
中间件运行发生在后端 route dispatching 的时候,每次的请求我们根据请求的 session 找到 user、找到对应的 user group, 再找到 permissions,通过定义的具有权限 module function 与请求的 handler 比对,若不同则限制掉。
def permission_middleware():
permissions = request.get.user_permissions()
if ‘{}_{}’.format(handler.__class__, handler.func_name) not in permissions:
return permission_denied
这样后端限制逻辑比较结构化,节省了很多麻烦,方便修改和管理。同时具有较好的扩展性,我们将 user 定义为单一用户对象,将 permissions 看作单一 handler 对象,可以把所有门店、部门、小组、小群体等均划分到 group 一层 。几乎大部分的业务需求均可以对应实现。逻辑准确,嗯,很完美。
限制闭环,对象细化
第二天 PM 来了,说 xx 部门的小 x 讲他们部门里面的工作内容需要分类,(我在点头,心里想:嗯嗯,我给你们每一个分类加一个 group 配置 user 权限)一个工作分类有许多工作者,有一个管理者,管理者需要看所有工作者的工作进度,工作内容管理者也同样可以进行(心里想:嗯嗯,我再加一个管理者的 group 所有权限一样,单独加一个工作进度查看的权限)同时!1分类下的管理者,还可以看2、3工作分类下的小 y 小 z 的工作进度!(我心里想:what?这不科学吧)PM 继续说,因为他们工作要对接,是需要查看的功能(我想:那管理者不作分类不就好了,所有的内容都可以看)并不能全部看到!PM 接着说,他的主要工作还是本分类下的工作内容,只是查看一下其他分类下的几个人的进度,你不能给他把所有人的内容都展示出来吧,况且有些还是保密的。
这确实是一个问题,不仅仅是在这种特殊的情况,只要涉及到针对某一个用户,或者某一个具体内容做操作限制,就是实现不了的。经过辗转反侧,发现我们之前做的只是将权限限制在操作行为上,没有形成闭环,操作哪些对象没有定义。每一次限制默认是所有对象。这样虽然方便管理,但是有些场景不能满足需求。于是引入权限操作对象范围的概念:
在之前的 permissions 一层上加入:
1. 是否有对象粒度限制
2. 限制的哪个 model 的 object
3. 可操作的object id list。
由于这部分是对于数据过滤的处理,所以我们无法在中间件完成这个操作。将这部分放入数据处理层(DAO 层)进行过滤是个不错的主意。首先我们在数据处理方法中,在上下文获取本次请求的 user 对象,按老办法将他的 permissions 统统获取到,这时,按照我们预先定义的:是否有对象粒度限制进行判断,需要限制则根据当前处理的 model 过滤出操作的 object id list。这样在返回数据的时候,所有记录均为对其具有操作权限的对象。
object_id_list = request.user.permissions.get_object_id_list()
if need_limit_object and model == request.user.permissions.get_model:
sql = ‘select * from model where id in {}’.format(tuple(object_id_list))
return fetch_data(sql)
现在我们满足了所有(至少是目前为止所遇到的)业务需求,后续权限的所有需求也可以按照这个思路进行相关配置处理,并得以解决(美好的愿望^_^)。
前端限制提高体验
至此,我们开发工作会变得很愉快,逻辑清晰且结构严谨。与此同时,给 c 端用户的体验就是,哪里不会点哪里,点完哪里不给你。给一个美如画的页面或者是弹窗还算好的,跳个403、404或是500想必也见过。此时用户只有一句 mmp,什么破玩意不玩了。
所以,权限在前端预先限制势在必行,好在我们多数情况下用到了模版引擎,很方便的在页面上进行权限判断。在服务器端渲染页面的时候,根据当前请求的用户,获取到所有权限,在某一个 DOM 上简单随意的写上一句判断代码,将我们在中间件中匹配 handler 的过程放到模版渲染的过程,这样将没有权限的条目过滤掉,还用户一个清静的世界,同样可以避免点到不该点的东西。
{% if user.permissions.can_modify_xxx %}
<a href=”/modify_xxx/”>修改xxx</a>
{% endif %}
如果是在不用模版引擎的情况下,我们就需要在页面在浏览器渲染的时候,加一个 ajax 请求加载到用户的权限列表,用js后期去掩盖掉不该看到的东西。
前端组件化整理
这样极大程度的提高了用户体验,但是回顾一下我们的做法,编写一个新的功能,处理掉权限所有关系配置之后,需要根据这个 module function 字符串在页面上所有用到的地方写一句判断语句,这样非常麻烦。而且如果想要修改一个权限的定义,简直是灾难。为了解决这个问题,我们不得不借鉴一下组件化的设计模式,即模版复用。用模版引擎的继承、导入可以实现这个目的。当然 react 是天生的组件化设计理念。我们在一个最上级组件初始化的时候将该用户具有的权限列表从服务器端拉取,存储本地,然后依次向下流入到各级子组件中,每个组件配置一个 id(可以与 module function 字符串一致,也可以做一次映射)当组件在权限列表中找到自己的对应的 id 则进行渲染。最终整个页面根据权限列表将所有可以显示的部分全部渲染出来。
刚才提到的组件id这是前后端限制的关键,后端根据 module function 判断 handler 处理,前端根据 id 判断渲染与否,那么这个配置应该是前端写好组件由后端配置,还是后端提供配置权限入口,由前端自行配置呢?
前后端分工配合
当我们讨论开发模式的时候,总会遇到这样一个问题:
前后端分离还是不分离
-
分离时前端工作量加重,路由、组件加载、部署等都要处理,有时候项目不是很大,这种分离得不偿失,但是好处是前后端各司其职,处理自己的部分,不会产生不必要的工作量。
-
不分离的情况就是运用模版引擎,前端提供静态页面,服务器端渲染,这样有的时候在后端处理数据的同时可能将页面样式或者动画效果破坏掉,然后又要两端反复修改,好处是开发简单,一些功能简单且比较单一,可以直接一并处理。
无论分离还是不分离,提供一个配置服务这个中间产物是有必要的,分离时我们可以让前端在写好组件后,将自己的组件id配置在系统中,后端按照正常的限制逻辑进行。不分离时,直接根据前端页面的编写的 id 进行配置,这部分可以由后端完成。有了这个公共区的系统,想必大家都是很开心的,又能一起愉快的玩耍(coding)了。现在处理这个问题的开源项目有很多,比如基于 django admin、flask admin 等流行的后台管理项目,就可以很好的搭建这种定制化的公共配置部分。
当然,具体的设计结构需要根据系统需求作相应的变通,可能有些场景限制粒度无要求,我们可以不作操作对象的闭环,如果是权限严格要求的场景(Mario 遇到的场景)就显得有必要了。前端限制在追求用户体验的情况是有必要的,组件化会将结构理得非常清晰,同时工作量也有所增加。公共区的配置模块就像一个前后端的枢纽,或者说是一个契约,不仅是在权限限制上,在所有配合开发的场景都是有所帮助的。
Mario,岂安研发工程师
主要负责岂安 STALKER 项目的设计与研发工作,python 爱好者,对于 python 在 web 和自动化领域中有所探究。天生对用户体验至上的产品有极大的好感。梦想是将一切极 low 且繁杂的工作交给计算机或优雅地提供给他人处理。