”转转“公司权限分配设计 「项目开发」

11/18/2023 项目开发

# 设计思路

RBAC模型 满足了转转绝大部分业务场景,并且开发成本远低于 ABAC模型 的权限系统,所以新权限系统选择了基于 RBAC模型 来实现。

标准的 RBAC模型 是完全遵守 用户 -> 角色 -> 权限 这个链路的,也就是用户的权限完全由他所拥有的角色来控制,但是这样会有一个缺点,就是给用户加权限必须新增一个角色,导致实际操作起来效率比较低。所以我们在 RBAC模型 的基础上,新增了给用户直接增加权限的能力,也就是说既可以给用户添加角色,也可以给用户直接添加权限。最终用户的权限是由拥有的角色和权限点组合而成。

新权限系统的权限模型:用户最终权限 = 用户拥有的角色带来的权限 + 用户独立配置的权限,两者取并集。

新权限系统方案如下图:

img

新权限系统方案

  • 首先,将集团所有的用户(包括外部用户),通过 统一登录与注册 功能实现了统一管理,同时与公司的组织架构信息模块打通,实现了同一个人员在所有系统中信息的一致。
  • 其次,因为新权限系统需要服务集团所有业务,所以需要支持多系统权限管理。用户进行权限管理前,需要先选择相应的系统,然后配置该系统的 菜单权限数据权限 信息,建立好系统的各个权限点。
  • 最后,创建该系统下的不同角色,给不同角色配置好权限点。比如店长角色,拥有店员操作权限、本店数据查看权限等,配置好这个角色后,后续只需要给指定人增加这个角色,就可以让他拥有店长对应的权限。

完成上述配置后,就可以进行用户的权限管理了。有两种方式可以给用户加权限:

  1. 先选用户,然后添加权限。该方式可以给用户添加任意角色或是菜单/数据权限点。
  2. 先选择角色,然后关联用户。该方式只可给用户添加角色,不能单独添加菜单/数据权限点。

# 权限系统自身的权限管理

对于权限系统来说,首先需要设计好系统自身的权限管理,也就是需要管理好 ”谁可以进入权限系统,谁可以管理其他系统的权限“,对于权限系统自身的用户,会分为三类:

  1. 超级管理员:拥有权限系统的全部操作权限,可以进行系统自身的任何操作,也可以管理接入权限的应用系统的管理操作。
  2. 权限操作用户:拥有至少一个已接入的应用系统的超级管理员角色的用户。该用户能进行的操作限定在所拥有的应用系统权限范围内。权限操作用户是一种身份,无需分配,而是根据规则自动获得的。
  3. 普通用户:普通用户也可以认为是一种身份,除去上述 2 类人,其余的都为普通用户。他们只能申请接入系统以及访问权限申请页面。

# 权限类型的定义

新权限系统中,我们把权限分为两大类,分别是:

  • 菜单功能权限:包括系统的目录导航、菜单的访问权限,以及按钮和 API 操作的权限
  • 数据权限:包括定义数据的查询范围权限,在不同系统中,通常叫做 “组织”、”站点“等,在新权限系统中,统一称作 ”组织“ 来管理数据权限

# 默认角色的分类

每个系统中设计了三个默认角色,用来满足基本的权限管理需求,分别如下:

  • 超级管理员:该角色拥有该系统的全部权限,可以修改系统的角色权限等配置,可以给其他用户授权。
  • 系统管理员:该角色拥有给其他用户授权以及修改系统的角色权限等配置能力,但角色本身不具有任何权限。
  • 授权管理员:该角色拥有给其他用户授权的能力。但是授权的范围不超出自己所拥有的权限。

经过这么区分,把 拥有权限拥有授权能力 ,这两部分给分隔开来,可以满足所有的权限控制的场景。

# 核心模块设计

# 系统/菜单/数据权限管理

把一个新系统接入权限系统有下列步骤:

  1. 创建系统
  2. 配置菜单功能权限
  3. 配置数据权限(可选)
  4. 创建系统的角色

其中,1、2、3 的步骤,都是在系统管理模块完成,具体流程如下图:

img

用户可以对系统的基本信息进行增删改查的操作,不同系统之间通过 系统编码 作为唯一区分。同时 系统编码 也会用作于菜单和数据权限编码的前缀,通过这样的设计保证权限编码全局唯一性。

例如系统的编码为 test_online,那么该系统的菜单编码格式便为 test_online:m_xxx

系统管理界面设计如下:

img

# 菜单管理

新权限系统首先对菜单进行了分类,分别是 目录菜单操作,示意如下图

img

它们分别代表的含义是:

  • 目录:指的是应用系统中最顶部的一级目录,
  • 菜单:指的是应用系统左侧的多层级菜单,也是最常用的菜单结构
  • 操作:指页面中的按钮、接口等一系列可以定义为操作或页面元素的部分。

菜单管理界面设计如下:

img 菜单权限数据的使用,也提供两种方式:

  • 动态菜单模式:这种模式下,菜单的增删完全由权限系统接管。也就是说在权限系统增加菜单,应用系统会同步增加。这种模式好处是修改菜单无需项目上线。
  • 静态菜单模式:菜单的增删由应用系统的前端控制,权限系统只控制访问权限。这种模式下,权限系统只能标识出用户是否拥有当前菜单的权限,而具体的显示控制是由前端根据权限数据来决定。

# 角色与用户管理

角色与用户管理都是可以直接改变用户权限的核心模块,整个设计思路如下图:

img

这个模块设计重点是需要考虑到批量操作。无论是通过角色关联用户,还是给用户批量增加/删除/重置权限,批量操作的场景都是系统需要设计好的。

# 权限申请

除了给其他用户添加权限外,新权限系统同时支持了用户自主申请权限。这个模块除了常规的审批流(申请、审批、查看)等,有一个比较特别的功能,就是如何让用户能选对自己要的权限。所以在该模块的设计上,除了直接选择角色外,还支持通过菜单/数据权限点,反向选择角色,如下图:

img

# 操作日志

系统操作日志会分为两大类:

  1. 操作流水日志:用户可看、可查的关键操作日志
  2. 服务 Log 日志:系统服务运行过程中产生的 Log 日志,其中,服务 Log 日志信息量大于操作流水日志,但是不方便搜索查看。所以权限系统需要提供操作流水日志功能。

在新权限系统中,用户所有的操作可以分为三类,分别为新增、更新、删除。所有的模块也可枚举,例如用户管理、角色管理、菜单管理等。明确这些信息后,那么一条日志就可以抽象为:什么人(Who)在什么时间(When)对哪些人(Target)的哪些模块做了哪些操作。

这样把所有的记录都入库,就可以方便的进行日志的查看和筛选了。

至此,新权限系统的核心设计思路与模块都已介绍完成,新系统在转转内部有大量的业务接入使用,权限管理相比以前方便了许多。权限系统作为每家公司的一个基础系统,灵活且完备的设计可以助力日后业务的发展更加高效。

# 后端设计实现

# 一、权限系统框架

img

权限系统主要解决两个问题:

  1. 前端渲染:接入系统用户登录后,获取自己有权限的菜单,也就是前端sdk请求权限系统获取有权限的菜单并进行自动渲染。
  2. 后端鉴权:用户请求接入系统后端,拒绝没有权限的接口访问,防止无权限用户获取后端接口地址后直接访问无权限的接口。

为了解决这两个问题,必然需要引入一些配套内容,其中重要的功能点如下:

  1. 用户管理:统一登录系统,支撑权限系统识别登录用户。
  2. 权限管理:系统管理、角色管理、菜单管理、数据管理、角色人员绑定等功能,方便为用户绑定相应的权限。
  3. 后端鉴权:访问接入系统接口调用时使用。

# 二、用户管理

权限系统支持转转、转转精神、发条爱客、自注册四种来源的用户数据,支持用户在一个系统登录在其他系统保持登录状态功能(统一登录),支持员工入职自动加入权限系统(员工同步),这里重点介绍统一登录。

# 2.1、统一登录

先从接入权限的域名都是*.zhuanspirit.com说起,统一登录利用浏览器携带Cookie特点:不同二级域名可以携带一级域名的Cookie。统一登录系统为接入系统种植一级域名Cookie,例如OA系统oa.zhuanspirit.com,权限系统自身id.zhuanspirit.com统一种植域名为*.zhuanspirit.com的Cookie,这样OA、权限系统等接入统一登录系统的系统就可以做到一处登录处处访问。现在抛出两个问题:

  1. 接入系统并未引入统一登录系统的任何代码,未登录用户是怎样跳转到统一登录页的?
  2. 如何校验Cookie是否合法的?

从以OA系统为例的图中可以看出问题答案:

  1. ngix会对*.zhuanspirit.com域名的请求做Cookie合法校验,校验不通过跳转到登录页,同时携带原url信息。
  2. 校验合法性是通过Cookie<sso_uid,sso_code>的值是否和Redis中一致,不一致就需要重新登录。

# 三、权限管理

img

转转权限管理系统是一个基于经典RBAC权限管理模型的实现,上图为转转权限系统的主要功能,本身实现复杂度并不高。对于简单的管理系统来说,灵魂在数据。对应到转转权限系统,血肉就是管理UI,它的使命就是方便为数据表填充数据,具体一点就是为用户表、系统表、菜单表、数据表、角色表以及中间关系表填充数据。下面就展现转转权限系统的灵魂--数据库表关系。

img

从上图中可以看到转转权限系统和RBAC一些差异:用户直接可以与菜单或者数据绑定,增加灵活性。

# 四、后端鉴权

# 4.1、工作原理

img

如上图所示,后端鉴权sdk通过切面编程的思想,对请求url或者code进行拦截鉴权,如果登录用户没有权限,则返回错误。

# 4.2、核心方法
res := authentication.Check(authentication.NewAppCodeAuthParmBuilder(APP_CODE, CODE, request))
1

上面是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)
}
1
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编辑权限数据,用户登录后,获取配置好的菜单和数据,并且校验用户访问的后端接口。