38

基于 mongodb 设计灵活后台管理权限

 5 years ago
source link: https://studygolang.com/articles/14432?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

基于 mongodb 设计灵活后台管理权限

mongodb 是一款基于 文档 结构的 nosql 数据库,目前社区比较火,在文档的存储灵活性有着天然的优势(同集合可以任何形式的行数据,当然我们不会这样去烂用这种灵活性 :) ),且有不俗的性能表现,以及副本集的高可用!

此款权限粒度存在范围与个体的概念,可以理解为 功能性权限 和 数据归属性权限。数据权限是功能权限的再次细分,作为子集。下面,我会一点点剖析,下面的案例会给出所有表结构,和关键操作的 sql 语句(基于 golang 语言的 (mgo mongodb driver))和设计图。当然这可能并不是最佳实践,只是我自己设计的一点心得,已便解决实际问题。

背景

之前在做一款早期 serverless 架构的雏形(现在只能算模版系统 :) )后台管理系统的时候,公司的人员结构很稳定,又为了快速内部上线提供使用,我们想到一款最简单的基于部门实现泛权限的功能权限管理,意思就是,这个部门下面的组拥有这个组创建的所有查看权限,这个组拥有的功能权限由角色定义,如果为资深开发者角色,那么管理员会分配 模版编辑权限,模版清缓存/发布等一套可以从代码web ide 上编辑到外网访问发布的整个流程权限,初级可能只有编辑权限等。

但是我们很快发现问题,公司虽然有统一的权限系统,但是控制粒度太粗,所以开始我们是把人员的权限放入到自己的库里维护,时隔几月,随着公司的大举扩张,人员结构,部门等发生翻天覆地的变化,人事变更意味着之前基于部门设计的权限变成了脏数据,部门之间的数据没法自动实现访问,只能通过我们简单的数据权限操作机制,让某一人拥有多个部门的权限,这样处理因为不方便的原因可能存在越权处理,因为在 A 部门你拥有发布权限, 在 B 部门我只想让你有查询功能等,这样有一段时间我们就陷入了不断为人调整权限的售后工作!而且在跨部门处理问题都会面临不方便。

结构图

思考

当时我们面临一个元素种类比较多的后台系统,且权限组合种类比较多,那么怎样能够设计一款能够自由组合,能够自驱玩转权限,权限的下方不再由单人控制,以及在做工作交接的时候权限与资源能够一键交接。所以列了如下几点特性。

  1. 权限控制下放,开发人员不再费力维护权限。
  2. 权限有角色管理控制,有数据权限的细分概念。
  3. 权限以及资源能够一键转让。
  4. 权限基于人为个体,细分到,某个人下的某条数据。
  5. 权限可以临时组成,不同开发人员,可以同时拥有一套或多套权限,方便跨部门协作。
  6. 使用要简单,与用户基本信息隔离
  7. 性能要出众。可以做成 web 服务 / sdk

实践

接下来就是进行表结构的梳理,所有结构都会给出 最简洁关键字段及注解

要想细分到数据权限,那么人/组必须要有一个元素与资源进行绑定,在此我们用 signkey 作为一种签名,某个人拥有这个签名就有对这个资源有一定的操作权限。一定要有中间签名的纽带来建立联系,不然数据量的存储,转让,变动都会出现问题。 如果说把资源属于哪些人来处理的话,可能一个字段就有很多工号来识别。

所以我们权限表里面的 user 信息表这样设计

type UserDoc struct {
    Name      string          // 姓名
    UserId    string          // 工号 唯一标识
    SignKey   map[string]string // 签名 key是签名 24字符id mongodb _id (可换成任意 唯一 字符串),value是签名的描述。
}

可以看到一个人自己可以创建多个私有 签名

GroupName+UserId 作联合唯一索引

那么我们的角色权限是怎么设计的呢?一个角色拥有多种职能,我们把每一种职能看作一个接口实现了不同的方法,那么角色权限,就是由一个或多个接口实现的组合,接口实现又体现在 url + method 上面,然而问题是,有些接口涉及到操作数据,对数据有写操作,有的接口,只是单纯的某一次事件,不涉及到任何数据查询和变更。那么在这里就会划分此接口是否开启对数据做认证。通过不同的接口实现组合,这样就拥有了角色权限的维护。

type RoleDoc struct {
    RoleName        string          // 角色名称
    Desc            string          // 角色描述
    IsDefault       bool            // 是否为默认角色,就是用户登陆进来自带的角色权限
    UserIds         []string        // 角色下面有哪些用户,  这里是一个优化点,可以使用外键表关联。 视系统大小可以调整
    PathsDataVerify map[string]bool // 该接口是否开启数据验证权限,用于 sql 快速查询数据
    PathMethods     []PathMethod    // 该角色有哪些功能即 API 的组合, 详细数据,是一份冗余数据,方便配置时使用 通过方法把详细数据转化为快速查询数据
    Typ             int             // 角色类型,用于做特权处理, 例如 超级管理员角色,无需验证权限 则 type = 0
}

// restful API 风格设计 一个 path 有多个 method,
// method 使用 int 代替 GET=1 POST=2 PUT=4 DELETE=8 HEAD=16... 可以用到 mongodb 位运算
// 如果一个 path 的 get  post 方法都赋予了这个角色, 则 MethodInt=3
type PathMethod struct {
    Path      string // 请求路径
    MethodInt int // 方法的 int 和   9 代表 GET 和 DELETE 方法
}

上述表结构 PathsDataVerify key="1_/template" value=true 因为 1 表示 GET 所以这种表示描述就为 模版的查询方法,需要开启数据认证权限

roleName+Typ 作联合唯一索引

有了 Path 与角色的关联,就自然会有 Path 的详情,以方便管理,作为组成角色的权限的源,同时可以设置该接口是否开启数据验证权限。

type RouterDoc struct {
    Path      string                   // 请求路径  /template
    Desc      string                   // 路径实现功能的大类, 例如 模块的 CRUD
    MethodMap map[string]MethoedDetail // key 方法 具体表现  "1"  "2" ..
}

type MethoedDetail struct {
    DataVerify bool   // 该方法是否开启 数据验证
    Desc       string // 模版的删除 则 MethodMap key = 8
}

通过上面的简单表述,就可以建立 url + method 等于方法的实现,可以很方便的进行权限管理

Path 作唯一索引

那么我们的数据权限验证又是怎么实现的呢。它与角色权限又是什么样的关系。与人又怎么绑定的,注解如下

type SignDoc struct {
    SignKey      string         // 签名, 某人的私有签名 + 用户唯一标识  可以理解成 这个人拥有这个签名的哪些数据权限
    CreateUserId string         // 签名的创建者
    UserId       string         // 这个成员也拥有这个签名的一部分/全部权限, 相当于创建者把自己的签名共享出,用于多人使用
    RouterMap    map[string]int // key=Path value=Method   $bitsAllSet 计算  {"/template":14} 表述 这个人拥有此签名下面的模版数据 增/改/删的权限
}

SignKey+UserId 作联合唯一索引

这样创建者可分配的权限为自己的角色权限的子集,可以把一个私有key 暴露成公共的授权个他人,那么他人就对此签名下面所有的资源,都有相应你配置的权限,RouterMap 代表他拥有哪些权限,这样你就可以把自己的签名,分给不同的人,不同的人,拥有对此签名不同的权限。完全实现了,自己的数据自己做主管理。用于多种协调工作的场景。比如临时有一个修改活动逻辑的任务,有3个人做,那么他们组负责人可以创建一个公共的 key ,赋予这三个人,用于此次任务的所有权限操作。

所有的表结构都注解了,那么它们是怎么关联协作的,下面我们将从一个用户的角度使用权限系统里面所涉及的 sql 语句做简单解析,以便熟悉整个请求的流转。

配置权限流程

初始化路由 Method 描述以及是否开启数据权限 ---> 创建角色 ---> 把才创建好的路由与创建的角色进行绑定 ---> 为角色添加用户 ---> 角色创建自己的签名 ---> 每一次请求携带上签名 ---> 创建资源的时候携带上此签名与资源绑定

查看个人自己拥有什么权限

// userid 查询 RoleDoc 然后把所有返回的 PathMethods 做聚合
    // 通过 PathMethods 也可以结合 RouterDoc 查询具体的数据验证权限

查看别人授予的权限

// userid 查询 SignDoc  然后对返回,做聚合即可

在权限的配置和管理上面,没有任何复杂的 sql ,当然在实际项目当中有很多的 sql 组合来满足不同的配置方式。总体下来是很方便的。

复杂分配举例,以对资源的 CRUD 为例

  1. u1 u2 u3, u1 拥有 u2 签名 u2-s-1 下的 ceph资源读取权限, u2 拥有 u1-s-3 下的 template CRU 权限,u3-s-1 下的ceph CU 权限。 u3拥有 u1-s-3 所有数据的 R 权限

数据储存表现

// RoleDoc
[
    {"userId":"u1", "signKey":{"u1-s-3":"ceph kv manager"}},
    {"userId":"u2", "signKey":{"u2-s-1":"template manager"}},
    {"userId":"u3", "signKey":{"u3-s-1":"template manager"}}
]
// RouterDoc
[
    {"path":"/ceph","methodMap":{
        "1":{"dataVerify":true,"desc":"query ceph value"},
        "2":{"dataVerify":true,"desc":"create ceph value"},
        "4":{"dataVerify":true,"desc":"update ceph value"},
        "8":{"dataVerify":true,"desc":"delete ceph value"},
        }
    },
    {"path":"/template","methodMap":{
        "1":{"dataVerify":true,"desc":"query template value"},
        "2":{"dataVerify":true,"desc":"create template value"},
        "4":{"dataVerify":true,"desc":"update template value"},
        "8":{"dataVerify":true,"desc":"delete template value"},
        }
    }
]
// RoleDoc 此角色没有删除权限
[
    {"reoleName":"ceph&template manager","userIds":["u1","u2","u3"],
    "pathMethods":[
        {"path":"/ceph","methodInt":7},
        {"path":"/template","methodInt":7}
    ]}
]
// SignDoc 签名数据权限关联
[
    {"signKey":"u1-s-3","createUserId":"u1","userId":"u2","routerMap":{"/template":7}},
    {"signKey":"u3-s-1","createUserId":"u3","userId":"u2","routerMap":{"/ceph":6}},
    {"signKey":"u2-s-1","createUserId":"u2","userId":"u1","routerMap":{"/ceph":1}},
    {"signKey":"u1-s-3","createUserId":"u1","userId":"u3","routerMap":{"/template":1,"/ceph":1}}
]

当数据量特大的嵌套文档,都可以外键单独行存储。

验证权限

用户(统一单点)登陆后 ---> 判断是否为初始用户(默认角色) ---> 返回角色类型,拥有哪些权限(前端可以根据拥有的权限对 ui 进行渲染控制)

用户请求 携带 userid + signkey 请求获取 path + method (路由如果支持 动态路由, 用 httprouter 解析成后台配置的路由)

使用 userid + path + method 查询 roleDoc 表 验证是否存在角色权限(如果角色为超级管理员,则后面无需验证)

根据角色权限 PathsDataVerify 字段 value 值可以判断是否验证数据权限。

如果需要判断数据权限,则 携带 userid + signkey + path + method 查询 signDoc 表,是否存在 有权限则把相应的 权限信息写入 context 供后续方法使用

如果是查询接口 用户无需携带 signkey , 需要查询的 signkey 由权限处理中间方法计算组成 signkey 数组 , mongodb 使用 $in 查询数据

signkey 数组: 自己的私有 key , 别人授权的数据权限 key 通过 userid + path + method 查询到所有授权的 signDoc 把里面的 signkey 放到数组里

总结

这一套权限验证是解决之前固有依赖部门设计的权限而提出的,此权限不存在与其他的依赖,只取决与签名与人与路由之间的绑定关系。可以任意组合!

数据权限是角色权限的子集,必须要满足角色权限。比如 u1 没有删除 /template 的角色权限, u2 授权 u1 拥有 u2-s-1 的 /template 的删除权限,也是不可以的。

通过这套权限把之前的数据进行了拆分,结构便于理解。当然此套表结构也是适用于其他数据库的。如果对性能有要求是可以用 redis 对权限做缓存的。当然目前我们基于200 + 用户, 200+ 路由配置,权限过滤和所有的管理结构都是在 20ms 下的。对于后台系统是可以接受的。

设计不好,仅供参考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK