20210610网迹【权限优化】

Jun 16, 2021

网迹【权限优化】

各服务目前都有基于用户权限来限制接口的使用的功能。比如系统管理员处理系统的事务;厨师进厨房拿刀切菜烧菜;律师进事务所处理案件等。

当前网迹权限长这样

普及下权限

比如user(普通用户)、superman(超人)等角色,我们用以下模型来表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
code: "user",
name: "普通用户",
permissions: ["mod_collect"]
},
{
code: "onlyEditor",
name: "上传者",
permissions: ["mod_collect", "mod_sysUser_edit", "mod_sysUser_del"]
},
{
code: "superman",
name: "超人",
permissions: ["mod_collect", "mod_sysUser", "mod_sysUser_edit", "mod_sysUser_del", "mod_sysRole"] // 没有采集修改权限
}

对应接口如:

1
2
3
4
5
{
path: "/collect",
method: "GET",
permission: "mod_collect"
}

这时很简单的方案就是直接判别数组内是否有该权限,有当然就能使用该接口啦。

但是此时会发现一个问题,数组查找字符串,其实是一个很耗时的操作。数组["mod_collect", "mod_collect_edit"].include("mod_collect") 会先for 循环查找第一个字符串(mod_collect),第一个字符串对比从第一个字符开始一个个比对。

你会说,那也很快啊,CPU买买买,快快快。

有这想法的人得自己深思了,这里的判别是作用在所有中间件,当然是尽可能快,能提高用户使用、设备使用。大多在中间件出现的数据,一定要尽可能的缓存或者优化。

OK,那怎么解?我们都知道字符串判别是一个一个字符比较O(n),如果是数字判别,则是类似于O(1)的时间复杂度。我们将现有的所有permission都转化为一个数字: Bitmap. 比如: mod_collect: 1 << 2 = 4; mod_collect_edit: 1 << 3 = 8; mod_sysUser: 1 << 4 = 16; mod_sysUser_edit: 1 << 5 = 32; mod_sysUser_del: 1 << 6 = 64; mod_sysRole: 1 << 7 = 128; ...

以上得到关联: user 有一个权限: [4]; onlyEditor有三个权限, 我们使用 |运算符给他串起来: 8 | 32 | 64 = 104, 我们这时可以说 user用户的权限是4, onlyEditor用户的权限是104, superman超人的权限是244.

我们将上述的数据量不大,基本不怎么变动的数据都内存缓存下来:

1
2
3
4
5
6
7
8
9
10
11
roles: {
"mod_collect": 4,
"mod_colelct_edit": 8,
"mod_sysUser": 16,
...
}
permissions: {
"user": 4,
"onlyEditor": 104,
"superman": 244
}

此时如果接口进入中间件:

1
2
3
4
5
6
7
// 假装我是中间件内部判别权限
const curRoutePermission = "mod_collect_edit" // 当前接口必须的权限
function hasPermission(role:string) {
const tPerm = permissions[role]
const curRole = roles[curRoutePermission]
return tPerm & curRole // `与`符能得到交集 -> 这个用户是否有该接口的权限
}

如果是user判别需要"mod_collect_edit"接口 => 4 & 8 = 0; onlyEditor => 104 & 8 = 8, 说明是有权限的!

使用位运算&比较,如果 > 0 则表明这是有权限的。

以上是权限基础普及,下面开始进入权限升级部分: ↓↓↓

设计思路

设定数据范围、权限及访问路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum DataScope {
All = 1, // 全局
ThisWithSubordinate = 2, // 本单位及下属单位
This, // 仅本单位
self // 本人
}
enum UrlMap {
browseCollection = "/browse/collection", // 采集管理
sysUser = "/sys/user",
sysRole = "/sys/role"
}
const Urls = [
UrlMap.browseCollection,
UrlMap.sysUser,
UrlMap.sysRole
]
const roleSchema = {
name: String, // 用于记录角色名称,比如 `超人`
code: {type: String, unique: true}, // 用于声明角色编码,如superman/user/u001等代号
permissions: [{type: String}], // 该角色拥有的权限数组
urls: [{type: String}], // 该角色展示的页面数组
dataScope: {type: Number, enum: DataScope}, // 数据范围声明
remark: String // 加个简介好记忆?
}

角色表包含有数据权限dataScope,页面列表urls,权限列表permissions。

数据范围定义数据获取等级。侧重点是支持中间件将该用户的数据权限,写到用户数据中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 将已构建的角色对应结构保存在内存中
staticbuildInRoleMaps:IBuildInRoleMap
static async dataScopeMiddleware(req:Request, res:Response, next:NextFunction) {
if (!req.hasPermission("admin")) {
if (!req.user.departmentNo) return res.status(403).send("您没有设置组织机构")
} else {
req.user.dataScope = DataScope.All
return next()
}

const requserRole = Utils.getUserFirstRole(req.user)
if (!_.includes(["anon"], requserRole)) {
const theRole = Role.buildInRoleMaps[requserRole]
if (!theRole) return next(new Error("权限获取出错,请联系管理员"))
req.user.dataScope = theRole.dataScope
}
next()
}

将改变不大的数据存在内存中,可以很快速的进行获取数据。

数据范围设定(根据业务划分不同的接口)

下面是我所使用的获取数据过滤方式之一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static dataScopeForCollect(user:{dataScope:DataScope, departmentNo:string, policeNo:string}) {
let cond:{departmentNo?:string, code?:RegExp, policeNo?:string} = {}
if (!_.isNumber(user.dataScope)) return cond
if (user.dataScope === DataScope.All) return cond
else if (user.dataScope === DataScope.ThisWithSubordinate) {
cond.code = Utils.orgReg(user.departmentNo)
} else if (user.dataScope === DataScope.This) {
cond.departmentNo = user.departmentNo
} else if (user.dataScope === DataScope.self) {
cond.policeNo = user.policeNo
if (user.departmentNo) cond.departmentNo = user.departmentNo
}
return cond
}

于是我很快就能知道对应的获取数据方案。

限制页面路由

这方面由李豪同学来交流下。当初也是李豪同学提出来,我保存好,由他来约束这块的逻辑。点赞。

接口权限的组合

这块复用第一章节的逻辑。第一章节将静态的数据,在这里转化为可调节配置的权限组合。

注意点

Number 类型是32位的。使用Number 进行位运算,当角色超过32位时, 1 << 32 = 0.

使用BigInt 可能突破这个限制: BigInt(1) << BigInt(x).当使用BigInt 作为json 传输时需要将其转为字符串,在Chrome67之后版本支持BigInt 类型。前段收到对应角色编码,将其转化为BigInt:

1
2
$ BigInt("17179869185")
$ 17179869185n