0. 系统设计
1. 数据管理
2. 身份认证
3. 权限管理
身份认证处理的是 “你是谁的问题”,而权限管理处理的是 “你能干什么的问题”。“你能干什么的问题”这句话里有两个关键的点:一个是 “你”,“你” 是用户;另一个是“干什么”,“干什么” 是 处理,在 WEB 系统中所有的处理都是由客户端对服务器发起的 HTTP资源请求。对于 http 资源请求一般有能力请求和没有能力请求,对应于 http 状态码则是200、403。在有 http 资源访问权限的基础上一般对于该请求所返回的 数据资源 也会有不同程度的限制,例如作者和管理员能看到和处理的文章数量不同。
把以上这HTTP资源和数据资源都抽象成系统资源,那么权限管理就简化成了系统中用户和系统资源的关系映射。
没图
用户分组
在公司里具有相同职责的人在系统中相应会有相同的权限,而不同的职责又跟公司的组织架构有关,所以很多时候设计权限管理的时候会吧公司组织结构也设计进来。事实上用相同能力和公司成员做映射的话完全可以把公司组织结构给扔掉,这种映射也就是 角色。
利用以上分析现在可以做这么一个映射关系:用户-->角色-->系统资源。即通过给用户授予角色来实现用户->资源的映射过程。
在角色的处理上一般有单一角色和多角色两种方案,同时在这两种方案上对能单独对用户增减权限还要有额外的设计。
在公司里通常是一个萝卜一个坑,即一个人只有一种角色,但是实际过程中身兼多职的事情常常发生,所以一般我做权限设计底层都是多角色。
资源管理
如开头所说,把 “HTTP” 资源和系统中的 “数据资源” 都抽象成 “系统资源” 来处理,在系统中进行检查权限检查就可以变成 “是否拥有” 这么简单的事情了。到数据层面,用户拥有哪些权限也只是一维的数据,到这一步怎么存储就随意了。[我初次设计的时候是用关联表进行存储的,后来听从别人的建议并产考 sentry 权限控制组件
,采用的 json 字符串方式进行存储。]
根据这些现在权限抽象处理和权限持久化差不多都解决了,但是这里会有第二个问题:如何管理这些权限?
对于一个小系统,十几二十几个功能的系统可能只需做个小表格把这些权限都列举一下,管理员根据角色去勾选一下就好,但是对于功能繁多的系统这样明显是行不通的,谁会去在几十上百个都差不多的名称里去找到你需要的那个权限名称去!
所以这些权限如何呈现给用户让他们自己去管理这又是一个问题!
呈现方式
这里先回顾一下后台菜单的排布方式,大概有一下几种
1和2的形式都比较简单,基本没什么可说的。就直接说三级导航的这种后台怎么处理,然后一级二级这种的基本都不是问题。
处理三级导航系统的后台时我一般遵循这么一个原则:
- 一级:以业务划分,相同业务线的在同一个一级导航下,同时在代码层面也进行命名空间的分割
- 二级:以功能相关性划分,相似或有关联的功能化为一组。我通常二级导航不负责真正的功能只是对三级菜单的分组合并
- 三级:这一级其实已经是具体的功能页了。具体到权限管理时会有菜单,页面,功能等区别
- 处理三级导航的后台除了图三所示的那种所有导航都在左边,其实还可以一级导航在顶部,二三级在左边
在处理只到一级或者二级菜单的系统时,菜单是否展示往往依靠硬编码就解决了,因为这时候功能少,设计复杂了反而影响修改的灵活性,但是对于三级菜单的系统来讲要权限和菜单同步仅靠硬编码就很困难了,要做到足够的灵活可配置就需要设定一个规则进行约定了。
数据结构
先写一个三级菜单的菜单数据结构
[
{
"name":"一级导航1",
"url": "/nav1",
"chirdren":[
{
"name":"二级导航1", // 一般为菜单分组,只提供折叠功能而不提供导航到页面的功能
"url": "/nav1/nav2-1",
"chirdren": [
{ // 这一级才是真正的菜单
"name":"三级导航1",
"url":"/nav1/nav2-1/path1",
},
{
"name":"三级导航2",
"url":"/nav1/nav2-1/path2",
},
]
},
{
"name":"二级导航2",
"url": "/nav1/nav2-2",
"chirdren": [
{
"name":"三级导航1",
"url":"/nav1/nav2-2/path1",
},
{
"name":"三级导航2",
"url":"/nav1/nav2-2/path2",
},
]
}
]
},{
...
}
]
上面一级导航和二级导航都有一个 URL,实际上二级作为分组只有折叠的作用,一级导航如果在顶部,那个 URL 其实点击后跳转到的也是该分组下的一个三级导航。
另外这个数据结构只处理了一级导航>二级导航>三级导航
的数据结构,三级导航是列表页的情况下时该三级导航可能还会关联 "新建页面[GET]","新建提交[操作|POST]","编辑页面[GET]","编辑提交[操作|PUT]","删除提交[操作|DELETE]", 这些项虽然不在菜单中展示,但是在权限管理页面这些却是必须的。
- 从这一段开始我不再用一级导航、二级导航、三级导航这样的名称,而是用
导航>分组>菜单
这样功能比较明确的名称
上一节【呈现形式】有提到如何管理但是并没有真正解决三级菜单下的如何呈现的问题。现在提问:如果用和导航布局一样的层现方式来呈现权限管理是否可行呢?
这里还有另外一个问题,就是当我们打开菜单下的一个编辑页面不管我刷不刷新浏览器改页面上层的菜单和导航应该都是高亮的,这又该如何解决?
下面给一个我现在使用的权限管理数据结构
[
{
"name":"导航1",
"index": "welcome",
"groups": {
"分组1": [
"home.dashboard.index1",
"home.dashboard.index2",
],
"分组2": [
"home.dashboard.index3",
],
},
"routes":{
"welcome":{
"name":"二级导航1",
"url": "/nav1/nav2-1",
},
"home.dashboard.index1": {
"name":"菜单",
"url": "/home/dashboard/index1",
"type":"menu"
},
"home.dashboard.index1-1": {
"name":"页面1-1",
"url": "/home/dashboard/index1-1",
"type": "page",
"refer": "home.dashboard.index1",
},
"home.dashboard.index1-1": {
"name":"页面1-2",
"url": "/home/dashboard/index1-2",
"type": "page",
"refer": "home.dashboard.index1",
},
"home.dashboard.index2": {
"name":"菜单",
"url": "/home/dashboard/index2",
"type":"menu"
},
"home.dashboard.index2-1": {
"name":"页面3-1",
"url": "/home/dashboard/index2-1",
"type": "page",
"refer": "home.dashboard.index2",
},
"home.dashboard.index2-1": {
"name":"页面2-2",
"url": "/home/dashboard/index2-2",
"type": "page",
"refer": "home.dashboard.index2",
},
"home.dashboard.index3": {
"name":"菜单",
"url": "/home/dashboard/index3",
"type":"menu"
},
"home.dashboard.index3-1": {
"name":"页面3-1",
"url": "/home/dashboard/index3-1",
"type": "page",
"refer": "home.dashboard.index3",
},
"home.dashboard.index3-1": {
"name":"页面3-2",
"url": "/home/dashboard/index3-2",
"type": "page",
"refer": "home.dashboard.index3",
},
}
},{
...
}
]
- 因为有 Request Method所以在 http 请求中一个 uri 并不能确定唯一一个请求,二者加一起才能唯一确定一个请求。而laravel框架刚好提供了路由别名的方案,即你可以给一个任意一个请求起一个别名,并且一个别名只能对应唯一一个 http request。我们可以根据这个别名生成 url,也能根据 Request 实例获得该实例的别名。关于别名的另一个好处就是只要我起名的方案不变 url 需要改变是不用担心的。所以在实际应用中我也是用别名设计的配置文件。
事实上在实际应用中我的配置文件路由节点长的是这个样子的
'works'=>[
'name' =>'工作记录',
'uri' =>'/system/work', 'method'=>'get',
'uses' =>'HomeController@work',
'limit-on'=>false,
'log.file' => '【{{user.name}}】访问了操作日志页',
'throttle'=>100,
],
因为跟别名关联的除了路由,还有行为日志、访问限制、权限管理等行为,所以直接设计在一块好了,一个配置文件搞定一切,这样当配置项增加的时候也不用担心顾此失彼的事情了。
单页面应用下的权限控制
单页面应用前后端是完全分离的状态的,前端路由和后端API路由没有必然的联系。像前端 /#/post/1234
这样的路由调用后端接口则可能为post?id=1234
这样的形式,二者的关联只在业务逻辑代码的 ajax 请求那块,一般情况下是靠前后端程序员约定。而且前端路由相对会比后端少,因为前端的一个功能复杂的页面会对应后端许多接口。所以前后端的权限怎么控制?
原则上可以制定一套规则同时适应于前后端路由和权限控制,但由于要考虑前后端权限控制和路由的问题以及一些未定义的场景这个方案将会很复杂。而且如果是团队协同开发,那么前后端成员都必须在了解这个方案和命名体系的前提下才能愉快的协作。事实上多人协作开发的过程中最大的问题就是知识同步。
另一个问题,在上一大节中我们可以了解到权限控制可以简化成一个类似查 hash 表的情景,那么这个 hash 表谁来维护,前端?后端?还是前后端同时维护一份。在这里面任意一种方案都有一个不可逃避的问题就是配置同步,1.如果前端维护那么这个hash 表要同步给后端,2.如果后端维护要同步给前端,3. 如果同时维护,那就要保持同步修改。 一般来讲(1.)的实现会更困难,(2.)实现的前提是前端必须在后端服务的基础上进行开发,而且很大层面上是在前后端用一个方案这个前提下。方案(3.)相对来讲权限表的维护成本更高一点,但其实这个维护也只是在设计之初前后端双方同时维护而已,产品成型之后的修改远没刚开始的修改更频繁,而且方案(3.)可以解开上一段所描述的知识同步的问题,因为只要约定某个 key 是控制某个功能
,双方根据这个约定去开发各自的功能基本不会有什么大问题,同时各自路由的命名方案也可以由此解开。
github: https://github.com/chen-wen/admin
github: https://github.com/chen-wen/vue-spa