”转转“公司权限分配设计 「项目开发」
# 设计思路
RBAC模型
满足了转转绝大部分业务场景,并且开发成本远低于 ABAC模型
的权限系统,所以新权限系统选择了基于 RBAC模型
来实现。
标准的 RBAC模型
是完全遵守 用户 -> 角色 -> 权限
这个链路的,也就是用户的权限完全由他所拥有的角色来控制,但是这样会有一个缺点,就是给用户加权限必须新增一个角色,导致实际操作起来效率比较低。所以我们在 RBAC模型
的基础上,新增了给用户直接增加权限的能力,也就是说既可以给用户添加角色,也可以给用户直接添加权限。最终用户的权限是由拥有的角色和权限点组合而成。
新权限系统的权限模型:用户最终权限 = 用户拥有的角色带来的权限 + 用户独立配置的权限,两者取并集。
新权限系统方案如下图:
新权限系统方案
- 首先,将集团所有的用户(包括外部用户),通过 统一登录与注册 功能实现了统一管理,同时与公司的组织架构信息模块打通,实现了同一个人员在所有系统中信息的一致。
- 其次,因为新权限系统需要服务集团所有业务,所以需要支持多系统权限管理。用户进行权限管理前,需要先选择相应的系统,然后配置该系统的 菜单权限 和 数据权限 信息,建立好系统的各个权限点。
- 最后,创建该系统下的不同角色,给不同角色配置好权限点。比如店长角色,拥有店员操作权限、本店数据查看权限等,配置好这个角色后,后续只需要给指定人增加这个角色,就可以让他拥有店长对应的权限。
完成上述配置后,就可以进行用户的权限管理了。有两种方式可以给用户加权限:
- 先选用户,然后添加权限。该方式可以给用户添加任意角色或是菜单/数据权限点。
- 先选择角色,然后关联用户。该方式只可给用户添加角色,不能单独添加菜单/数据权限点。
# 权限系统自身的权限管理
对于权限系统来说,首先需要设计好系统自身的权限管理,也就是需要管理好 ”谁可以进入权限系统,谁可以管理其他系统的权限“,对于权限系统自身的用户,会分为三类:
- 超级管理员:拥有权限系统的全部操作权限,可以进行系统自身的任何操作,也可以管理接入权限的应用系统的管理操作。
- 权限操作用户:拥有至少一个已接入的应用系统的超级管理员角色的用户。该用户能进行的操作限定在所拥有的应用系统权限范围内。权限操作用户是一种身份,无需分配,而是根据规则自动获得的。
- 普通用户:普通用户也可以认为是一种身份,除去上述 2 类人,其余的都为普通用户。他们只能申请接入系统以及访问权限申请页面。
# 权限类型的定义
新权限系统中,我们把权限分为两大类,分别是:
- 菜单功能权限:包括系统的目录导航、菜单的访问权限,以及按钮和 API 操作的权限
- 数据权限:包括定义数据的查询范围权限,在不同系统中,通常叫做 “组织”、”站点“等,在新权限系统中,统一称作 ”组织“ 来管理数据权限
# 默认角色的分类
每个系统中设计了三个默认角色,用来满足基本的权限管理需求,分别如下:
- 超级管理员:该角色拥有该系统的全部权限,可以修改系统的角色权限等配置,可以给其他用户授权。
- 系统管理员:该角色拥有给其他用户授权以及修改系统的角色权限等配置能力,但角色本身不具有任何权限。
- 授权管理员:该角色拥有给其他用户授权的能力。但是授权的范围不超出自己所拥有的权限。
经过这么区分,把 拥有权限 和 拥有授权能力 ,这两部分给分隔开来,可以满足所有的权限控制的场景。
# 核心模块设计
# 系统/菜单/数据权限管理
把一个新系统接入权限系统有下列步骤:
- 创建系统
- 配置菜单功能权限
- 配置数据权限(可选)
- 创建系统的角色
其中,1、2、3 的步骤,都是在系统管理模块完成,具体流程如下图:
用户可以对系统的基本信息进行增删改查的操作,不同系统之间通过 系统编码
作为唯一区分。同时 系统编码
也会用作于菜单和数据权限编码的前缀,通过这样的设计保证权限编码全局唯一性。
例如系统的编码为 test_online
,那么该系统的菜单编码格式便为 test_online:m_xxx
。
系统管理界面设计如下:
# 菜单管理
新权限系统首先对菜单进行了分类,分别是 目录
、菜单
和 操作
,示意如下图
它们分别代表的含义是:
- 目录:指的是应用系统中最顶部的一级目录,
- 菜单:指的是应用系统左侧的多层级菜单,也是最常用的菜单结构
- 操作:指页面中的按钮、接口等一系列可以定义为操作或页面元素的部分。
菜单管理界面设计如下:
菜单权限数据的使用,也提供两种方式:
- 动态菜单模式:这种模式下,菜单的增删完全由权限系统接管。也就是说在权限系统增加菜单,应用系统会同步增加。这种模式好处是修改菜单无需项目上线。
- 静态菜单模式:菜单的增删由应用系统的前端控制,权限系统只控制访问权限。这种模式下,权限系统只能标识出用户是否拥有当前菜单的权限,而具体的显示控制是由前端根据权限数据来决定。
# 角色与用户管理
角色与用户管理都是可以直接改变用户权限的核心模块,整个设计思路如下图:
这个模块设计重点是需要考虑到批量操作。无论是通过角色关联用户,还是给用户批量增加/删除/重置权限,批量操作的场景都是系统需要设计好的。
# 权限申请
除了给其他用户添加权限外,新权限系统同时支持了用户自主申请权限。这个模块除了常规的审批流(申请、审批、查看)等,有一个比较特别的功能,就是如何让用户能选对自己要的权限。所以在该模块的设计上,除了直接选择角色外,还支持通过菜单/数据权限点,反向选择角色,如下图:
# 操作日志
系统操作日志会分为两大类:
- 操作流水日志:用户可看、可查的关键操作日志
- 服务 Log 日志:系统服务运行过程中产生的 Log 日志,其中,服务 Log 日志信息量大于操作流水日志,但是不方便搜索查看。所以权限系统需要提供操作流水日志功能。
在新权限系统中,用户所有的操作可以分为三类,分别为新增、更新、删除。所有的模块也可枚举,例如用户管理、角色管理、菜单管理等。明确这些信息后,那么一条日志就可以抽象为:什么人(Who)在什么时间(When)对哪些人(Target)的哪些模块做了哪些操作。
这样把所有的记录都入库,就可以方便的进行日志的查看和筛选了。
至此,新权限系统的核心设计思路与模块都已介绍完成,新系统在转转内部有大量的业务接入使用,权限管理相比以前方便了许多。权限系统作为每家公司的一个基础系统,灵活且完备的设计可以助力日后业务的发展更加高效。
# 后端设计实现
# 一、权限系统框架
权限系统主要解决两个问题:
- 前端渲染:接入系统用户登录后,获取自己有权限的菜单,也就是前端sdk请求权限系统获取有权限的菜单并进行自动渲染。
- 后端鉴权:用户请求接入系统后端,拒绝没有权限的接口访问,防止无权限用户获取后端接口地址后直接访问无权限的接口。
为了解决这两个问题,必然需要引入一些配套内容,其中重要的功能点如下:
- 用户管理:统一登录系统,支撑权限系统识别登录用户。
- 权限管理:系统管理、角色管理、菜单管理、数据管理、角色人员绑定等功能,方便为用户绑定相应的权限。
- 后端鉴权:访问接入系统接口调用时使用。
# 二、用户管理
权限系统支持转转、转转精神、发条爱客、自注册四种来源的用户数据,支持用户在一个系统登录在其他系统保持登录状态功能(统一登录),支持员工入职自动加入权限系统(员工同步),这里重点介绍统一登录。
# 2.1、统一登录
先从接入权限的域名都是*.zhuanspirit.com说起,统一登录利用浏览器携带Cookie特点:不同二级域名可以携带一级域名的Cookie。统一登录系统为接入系统种植一级域名Cookie,例如OA系统oa.zhuanspirit.com,权限系统自身id.zhuanspirit.com统一种植域名为*.zhuanspirit.com的Cookie,这样OA、权限系统等接入统一登录系统的系统就可以做到一处登录处处访问。现在抛出两个问题:
- 接入系统并未引入统一登录系统的任何代码,未登录用户是怎样跳转到统一登录页的?
- 如何校验Cookie是否合法的?
从以OA系统为例的图中可以看出问题答案:
- ngix会对*.zhuanspirit.com域名的请求做Cookie合法校验,校验不通过跳转到登录页,同时携带原url信息。
- 校验合法性是通过Cookie<sso_uid,sso_code>的值是否和Redis中一致,不一致就需要重新登录。
# 三、权限管理
转转权限管理系统是一个基于经典RBAC权限管理模型的实现,上图为转转权限系统的主要功能,本身实现复杂度并不高。对于简单的管理系统来说,灵魂在数据。对应到转转权限系统,血肉就是管理UI,它的使命就是方便为数据表填充数据,具体一点就是为用户表、系统表、菜单表、数据表、角色表以及中间关系表填充数据。下面就展现转转权限系统的灵魂--数据库表关系。
从上图中可以看到转转权限系统和RBAC一些差异:用户直接可以与菜单或者数据绑定,增加灵活性。
# 四、后端鉴权
# 4.1、工作原理
如上图所示,后端鉴权sdk通过切面编程的思想,对请求url或者code进行拦截鉴权,如果登录用户没有权限,则返回错误。
# 4.2、核心方法
res := authentication.Check(authentication.NewAppCodeAuthParmBuilder(APP_CODE, CODE, request))
上面是sdk面向用户的接口很容易可以看出来,进入非常简单,仅仅需要三个参数:
- APP_CODE:系统的系统编号
- CODE:权限编码(可为空,为空时根据url鉴权)
- request:HttpServletRequest(从中获取Cookie信息解析出登录用户,以及url信息判断用户是否有此url权限)
# 4.3、使用demo
package main
import (
"fmt"
"net/http"
)
// AppCodeAuthParmBuilder 结构
type AppCodeAuthParmBuilder struct {
AppCode string
Code string
Request *http.Request
}
// AuthResult 结构
type AuthResult struct {
Success bool
Code int
Msg string
}
// Authentication 结构
type Authentication struct {
// 你的 Authentication 结构的定义
}
// Check 函数用于进行认证检查
func (a *Authentication) Check(builder *AppCodeAuthParmBuilder) *AuthResult {
// 进行认证检查的实际逻辑
// ...
// 返回 AuthResult 结果
return &AuthResult{
Success: true, // 根据实际情况设置
Code: 200, // 根据实际情况设置
Msg: "OK", // 根据实际情况设置
}
}
// ParseUser 函数用于解析用户信息
func (a *Authentication) ParseUser(request *http.Request) {
// 解析用户的逻辑
// ...
}
func ZZLockInterceptor(authentication *Authentication, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 在这里实现类似于 preHandle 的逻辑
res := authentication.Check(&AppCodeAuthParmBuilder{
AppCode: "arch_ipms",
Code: "ro_zzlock",
Request: r,
})
if res.Success && res.Code == 200 {
next.ServeHTTP(w, r)
} else {
user := authentication.ParseUser(r)
fmt.Printf("菜单 zzlock_get user %s 鉴权结果 code %d msg %s\n", user.LoginName, res.Code, res.Msg)
w.Write([]byte(fmt.Sprintf("user: %s no auth", user.LoginName)))
w.WriteHeader(http.StatusUnauthorized)
}
})
}
func main() {
authentication := &Authentication{} // 实例化 Authentication 结构
// 创建一个新的路由器
r := http.NewServeMux()
// 将 ZZLockInterceptor 中间件应用于路由器的所有路由
r.Handle("/", ZZLockInterceptor(authentication, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 实际的处理程序逻辑
w.Write([]byte("ZZLock operation"))
})))
// 启动 HTTP 服务器
http.ListenAndServe(":8080", r)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 五、总结
本篇文章着重介绍转转权限系统的后端实现,从使用方的视角出发,也就是前端渲染和接口鉴权,引出转转权限系统如何识别用户(统一登录),如何存储权限数据(权限管理),如何实现后端鉴权。
简而言之,权限系统的主要功能:权限系统UI编辑权限数据,用户登录后,获取配置好的菜单和数据,并且校验用户访问的后端接口。