如何使用 LDAP 基于微服务架构实现权限统一验证   2022-05-01


1 背景介绍

上个月刚找到工作, 入职不到三天, 老大就开始分配任务:
我们公司做了很多业务系统, 稳定运行也有一段时间了, 但是现在碰到了问题, 就是系统的权限控制啊, 非常的混乱, 之前用的是 RBAC 的方式来进行控制, 现在我们要做更改, 改用 LDAP 来控制, 巴拉巴拉…..
老大说了很多, 什么是 RBAC? 我不懂, 什么是LDAP? 更是云里雾里, 不过目的很明确, 就是老大对公司现有的权限控制系统不满意, 需要使用 LDAP 来进行控制。

经过一周的努力, 搜了一大堆的资料, 全是一些概念介绍, 然后就是如何安装配置的文章, 最后, LDAP 是如何进行权限控制的, 博主本人仍然是云里雾里!

这就是我们写这篇文章的目的, 我们要用最简洁的语言, 用普通人都能够听得懂的大白话, 把 LDAP 进行权限控制的原理说清楚.

2 目的

我们这里说的目的有两个:
A) 对公司的权限系统重新设计, 让公司的权限控制更有条理, 让权限控制更灵活。
B) 使用大白话把 LDAP 进行权限控制的原理说清楚.
上面这两个目的中, 我们会先对基础知识做个介绍, 然后围绕目的 A 的内容, 把目的 B 说清楚。

3 难点

看完网上的资料后, 博主本人以为, 只需要开发一个用户系统, 这个用户系统按照 LDAP 的方式来进行权限控制, 至于各个业务系统, 用户注册的时候往这边仍, 登录验证的时候来这里查就可以了, 至于权限如何控制, 哪些用户可以访问哪些东西, 我们就在后台进行手工配置就可以了. 但是出乎意料的是, 老大一再强调: 所有操作, 不能通过手工来配置, 必须自动化, 因此必须提供接口, 要不然你怎么运维?

让各个业务系统可以进行权限配置? 而且是自动化的? 这可怎么办?

4 基础知识介绍

4.1 ldap 是什么?

如果你查看资料, 毫无疑问, 网上有一大堆的资料, 它们会言之凿凿的跟你说, 所谓 LDAP, 就是轻量级目录访问协议, 即英文中的 Lightweight Directory Access Protocol, 不过, 我更乐意把它当作目录数据库访问协议, 他仅仅是个协议, 这个协议规定了如何去设计一个数据库, 然后用这个数据库用来保存具有树形结构(目录结构)的信息。

什么是树形结构?
就是有一个跟节点, 跟节点下面有多个子节点, 各个子节点下面又有孙节点, 就像是一棵树, 树根分出很多个树杈, 各个树杈又有很多个枝条。 像操作系统中的文件系统, 互联网的拓扑结构, 都是这种形式。

LDAP 协议就是用来规范这种数据库系统的.

4.2 什么是 RBAC? 他跟 LDAP 有什么不同?

讲到权限控制, 最直接的, 也是最容易想到的就是通过角色来进行权限控制, 我们先定义一个角色, 然后给人赋予角色, 再给角色赋予权限。 不同的人拥有不同的角色, 不同的角色拥有不同的权限, 这就是 RBAC 的权限控制思想。

但是 RBAC 的思想跟现实世界的情况不相符, 你看一个公司, 老板控制着下面的部门经理, 部门经理控制着组长, 组长控制着普通职员, 它是从上往下的权限控制方式, 上面的权限比下面的权限大, 最顶层的人权限最大, 最底层的人权限最小, RBAC 就无法体现这样的层级结构, 因此对于权限控制来讲, RBAC 不可避免的就会产生混乱。

而基于这种树形结构来进行权限控制, 正好是 LDAP 的思想, 当我们要给某一群人赋予权限的时候, 我只需要给这群人所处的最上面的节点赋予权限, 他们就都拥有权限了. 如果你不想让下面的某个节点拥有权限, 排除掉就可以了.

4.3 LDAP 跟 mysql 有什么不同?

准确的讲, 我不应该把 LDAP 跟 mysql 做比较, 因为他们不是同一类事物. LDAP  是一种协议, 是规范, 而 mysql 是数据库, 他不是规范, 因此无法比较。 但为了把 LDAP 讲清楚, 我们需要这样做.

既然 LDAP 协议是用来规范目录数据库系统的, 很显然, 他也是跟数据库有关系, mysql 也是数据库, 他们之间有什么区别?

数据库用来做什么? 是用来保存数据信息的. 
数据信息有哪些分类? 个人觉得可以分为两类: 
A) 对象信息 
B) 对象与对象之间的关系信息.

关系又可以分为平级关系和层级关系. mysql 这样的关系型数据库用来保存对象和对象之间的平级关系比较合适, 而根据 LDAP 规范设计出来的目录数据库, 用来保存对象与对象之间的层级关系比较合适.

4.4 实现 LDAP 规范的数据库系统有哪些?

博主略有耳闻的有:
A) OpenLdap: 开源免费
B) ApacheDS: 也是开源免费, 不过是用 java 实现的.
C) Microsoft Active Directory
D) IBM Security Directory Server 

完整的列表, 请查看这里:
ldap.com/directory-servers/

4.5 为什么需要 LDAP?

实际上, 讲到这里, 我们已经把这个问题回答了一半, 个人觉得, 即使不用另外说明也没有什么问题, 但我们需要做这个特殊说明, 原因有:

A) 网上的资料有一些文章再做说明的时候, 它会跟你说, LDAP 可以查询, 更新, 赋值, 权限认证。 每次我一看到这种回答, 都是气不打一处来, 它只是说明了 LDAP 的功能有哪些, 而这些功能, 关系型数据库也可以实现, 比如 mysql, 他也可以查询更新操作, 也可以进行复制和权限认证, 他们之间的区别是什么, 根本就没有说出来, 而这, 正好是我们想要知道的.

B) 当我们知道 LDAP 能够做什么了以后, LDAP 不能做什么我们也需要知道, 这样在做决策的时候才不会出错。

言归正传。

我们为什么需要 LDAP? 因为 LDAP 的树形结构对于权限认证来讲更加灵活, 比如公司新来了一名员工, 你只需要把它放置在某个节点, 在原有权限的体系下, 他自然而然的就获得了权限, 该有的权限都会有, 不会落下, 但是你用 RBAC 来实现, 用 mysql 来保存角色和权限, 我们还需要做一个动作, 就是给这个人赋予角色, 如果对角色所拥有的权限不理解, 那就会折腾半天, 搞不好操作失误还会出现大问题, 但是 LDAP 不会出现这种情况. 

那么, LDAP 不能做什么呢? 这就要讲到 LDAP 的缺点:
A) 通常来讲, 树形结构查询快, 但是更新和插入操作非常慢, 所以使用 LDAP 的时候, 必须考虑一个问题, 插入更新操作的频率必须远远小于查询操作, 至于比例是多少博主自己无法给出答案.
B) 在使用过程中发现, LDAP 无法维护多对多的关系, 如果非得要用 LDAP 来实现, 那真是噩梦.
C) 没有事务, 这意味着多个操作, 如果想让他们保持一致性那就很难, 需要自己去实现.

基于上面的理解, 如果你的系统需要基于树形结构的灵活性和优越的查询性能, 建议考虑 LDAP, 不单单是权限控制, 其他方面的系统也可以使用 LDAP.

4.6 LDAP 的底层是如何实现的?

4.6.1 LDAP 是如何识别一个对象的?
  在关系型数据库中, 比如 mysql, 当我们把某个对象的信息插入到数据表中的时候, 我都会给这个对象分配一个ID, 通过这个ID, 我们就能快速而且准确的获取到这个对象的信息, 那么, LDAP 如何快速而且准确的获取对象信息呢? 答案就是, 他使用 dn 来表示。
  
  什么是 dn ?
  最直接的想法, 如果我们要表示某个节点的时候, 我们需要给每个节点取一个独一无二的名称, 然后我们就可以说, 你把名字叫做xxx的节点的数据拿出来, 这样就可以了, 但这样速度快吗? 很显然, 如果树很大的话, 我们需要知道该节点在哪个层级, 我们还需要知道从跟节点开始, 一直到达该节点, 路径是什么样子的? 因此表示一个节点的最准确的方法是: 
  首先是跟节点, 然后是第二层的x节点, 再然后是第三层的y节点, 然后是第四层的z节点....以此类推.
  
  这就是 dn, dn 就是用来表示一个节点的, 它同时包含了路径, 因此当您接触到 LDAP 的时候, 看到的完整的 dn 基本上是这样的:
  "cn=张三,ou=人事部,ou=宇宙公司,dc=mydomain,dc=com"
  
  看到吗? 我为了让你认为上面那一行是一个整体, 是一个 dn, 特地使用双引号把它包起来. 然后是 dn 中的每一部分, 从左到右, 层级和层级之间使用逗号进行分隔, 最左边是最底层的节点, 最右边是最上层的节点, 不过, 你可能会认为, 上面这个 dn 表示的节点, 总共有5个层级, 不过你可能搞错了, 它很可能只有 4 个层级. 因为最右边的 dc=mydomain,dc=com 很可能用来表示跟节点, 跟节点需要两个, 这是大部分人的做法.
  
  那么, dn 中的 cn, ou, dc 是什么意思呢?
  一个节点用来表示一个对象, 既然是一个对象, 我们就需要知道它是什么类型的对象, 所以, cn, ou, dc 分别用来表示该节点是什么类型的数据, 我是这么理解, 其中 cn 就是姓名, 至于这个姓名是人名呢还是小狗的名字, 我就不清楚了, 然后 ou 是组织单位, dc 是域名零部件, 如果你需要找常用的类型名称, 请看这里: cnblogs.com/wilburxu/p/9174353.html
  
  
4.6.2 LDAP 的底层是如何知道, 有哪些数据类型可以使用?
  前面我们说过, 节点是用来表示一个对象的, 既然是对象, 那就包含三个部分的数据:
  A) 是什么类型的对象? 
     是猪? 狗? 牛羊?
     
  B) 他有哪些属性? 
     有颜色吗? 还是没有耳朵?
     
  C) 值是什么?
     属性值是什么? 节点所处的路径是什么?
     
  当我们讲到上面三种类型的数据的时候, 我们都容易理解, 毕竟能够使用 LDAP 的人, 大部分都是程序员, 程序员嘛, 对面向对象编程的思想早就熟悉了. 但在这里, 我们想提出一个问题: 如果我把外星来的某个物种的信息放在某个节点, LDAP  凭什么知道他是什么类型的呢? LDAP 又凭什么知道他有哪些属性呢? 当给这些属性赋值的时候, LDAP  凭什么知道, 赋值是否正确呢?
  
  因此, 在 LDAP 的内部, 一定存在着某种数据, 这些数据用来规定, LDAP 有哪些对象类型可以使用, 每一种对象类型都有哪些属性, 每个属性值应该是什么, 不应该是什么。
  这就是 schema, 我个人把它理解为模型.
  
  因此, 我们可以这样认为, schema 是用来做约束的, 这个约束规定了我们对 LDAP 进行操作的时候, 什么样的操作行为是可行的, 什么样的操作行为是不可行的, 什么样的数据是可用的, 什么样的数据是不可用的。
  
  LDAP 会把我们常用的对象类型的模型预设好, 这样我们使用的时候就不用再去配置了, 比如: Person, InetOrgPerson, OrganizationalUnit, 根据名称, 我们就很容易想到, Person 是用来表示人的, InetOrgPerson 是用来表示组织的员工的, OrganizationalUnit 是用来表示组织单位的.
  
  由于对象具有继承关系, 因此在 LDAP 中, 一个对象的最上层的类是 top 和 alias, 上面我们提到的 Person, 他的父类就是 TOP, 因此如果你想添加一种对象类型到 LDAP 中, 你至少要继承自 top 类或者 alias 类, 而在 LDAP 里面, 对象类型就用 ObjectClass 来表示, 我们添加一种对象类型到 LDAP 里面, 就是说我们添加了一个 ObjectClass 到 LDAP  里面.
  
  需要注意的是, ObjectClass 是有分类的, 用中文来表示, 说得绕口一点, 就是对象类型也是有类型的, 如何理解?
  在面向对象的编程中, 类不是分为抽象类和实现类吗? 在 LDAP 中, 它也需要知道, 哪些 ObjectClass(对象类) 只是作为一个模板来声明, 需要再继承之后才能实现具体的功能, 哪些 ObjectClass 就可以直接使用了, 因此, ObjectClass 有三种类别:
  A) 抽象型(Abstract): 比如 top, 需要注意的的是, 抽象型的 objectClass 不能直接使用, 需要子类继承后才能使用。
  B) 结构型(Structural): 比如 Person 和 OrganizationalUnit。
  C) 辅助型(Auxiliary)
  
  再次, 属性是有分类的, 分为必填属性和可选属性, 分别使用 MUST 和 MAY 来标识, 这个在自定义 ObjectClass 和属性的章节中进行说明.
  
  在上面三种类型中, 博主本人还不清楚在什么情况下会使用到辅助型的 ObjectClass.
  
  
4.6.3 LDAP 如何识别对象类型? 我们可不可以添加新的对象类型?
  在 LDAP 中, 每个节点, 就是一个 Entry, LDAP 是如何分辨不同的 Entry 呢?
  首先, 我们需要理解 3 个概念: 
  A) Attribute: 
    属性, 这里的属性指的是属性模板, 而不是指具体节点中的属性, 这两不一样。 
    换句话说, 只有提供了属性模板, 并且对这些模板的属性值进行了说明, 在节点的属性值进行操作时, LDAP  才知道有哪些属性可以使用, 以及赋值是否正确。
    因此, 如果我们想要给某个 ObjectClass 添加属性, 我们必须先定义属性模板.
    
  B) ObjectClass:
    对象类, 这个已经在前面做介绍, 再次强调的是, 这里仍然是指模板。
    
  C) Schema: 
    个人理解为模型, 他包括 ObjectClass 和 Attribute, 换句话说, Schema 中包含下面这些内容:
    1) 有哪些 ObjectClass 可以使用?
    2) 有哪些 Attribute 可以使用?
    3) 每个 ObjectClass 有多少 Attribute, 以及他们的名称是什么?
    4) 每个 Attribute 的值, 他是字符串还是数值? 如何进行匹配?
  
  我们必须明白, schema 中包含有很多个 ObjectClass, 每一个 ObjectClass 又拥有多个 Attribute, 根据 OpenLdap 的规定, 模型一旦添加, 不可修改, 因此:
  A) 我们想单独添加一个属性到现有的 ObjectClass 中, 不行, 因为这意味着对现有的 ObjectClass 进行修改, 因此只能添加新的 ObjectClass, 然后让新添加的 ObjectClass 来继承自原来的 ObjectClass。 由于博主本人只用过 OpenLdap, 而且博主还没有成功实现过, 所以得出这样的结论, 不清楚其他目录数据库是否提供该功能, 因此可能会有偏见.
  
  B) 当我们要添加新的 ObjectClass 的的时候, 一般我们会添加新的 schema, 然后在新添加的 schema 中定义 ObjectClass. 能否把新定义的 ObjectClass 直接添加到系统 schema 中, 博主不太清楚. 不过我们追加 ObjectClass 的时候, 可以追加到自定义的 schema 中.
  
  C) 我们也不能修改和删除属性模型, 哪怕是自己定义的也不行, 只能添加, 不能修改和删除. 如果我们想让 ObjectClass 拥有某个属性, 必须要先定义属性模型, 然后才能让 ObjectClass 拥有该属性. 对于系统中已经存在的属性模型, 我们无需重复定义, 直接使用即可.
  
  在博主写这篇文章的时候, OpenLdap 已经发行到了 2.4 版本, 根据说明, 对于 OpenLdap 已经存在的模型, 只能继承, 不能修改和删除, 这是官方的说法, 不过, 你仍然可以找到如何修改删除的文章, 这主要有3点原因:
  A) 文章中的修改删除, 很大可能是对节点中的对象进行修改和删除, 而不是对模型进行修改和删除, 我希望大家能够区分这一点. 模型只是模板, 而节点中的对象是具体的实现.
  
  B) 博主本人使用的是 OpenLdap, 其他目录服务器还没有使用过, 因此不清楚其他目录服务器是否提供该功能, 不过根据 LDAP 的规范, 不应该提供这样的功能, 因为模型一旦定义, 而且有节点在引用, 那么删除和修改就意味着数据库遭受破坏, 是不是其他目录数据库可以判断, 如果模型还没有被引用的情况下可以修改和删除, 目前不清楚.
  
  C) 非官方会提供删除的方法, 但他也会提醒说有不良后果, 博主本人就让目录数据库崩溃过, 由于 OpenLdap 安装在 docker 中, 崩溃后容器直接挂掉, 无法再启动. 至于有没有修改的方法, 博主本人还没有看到过.
  
  具体如何添加 Schema, 如何定义 ObjectClass, 如何给 ObjectClass 添加属性, 我们留到后面的章节去讲.
  
  这里还有另外的问题需要注意, 如果我们添加了新的 Schema, 系统会对所有 Schema 重新进行排序, 因此, schema 的 dn 中会出现数字, 比如:
  cn={9}xwanlion,cn=schema,cn=config
  
  对于添加新的 Schema, 我们无需考虑这个数字, 因为 LDAP 会自动生成, 但是对于向 Schema 中添加 ObjectClass, 就需要指定 Schema 的 dn 了, 而这个 dn 的数字是我们无法凭空想象出来的, 因此需要查询, 办法是进入配置文件的目录, 也就是模型数据保存的目录中查找, 我们会看到自定义的 Schema 名称和数字, 在目录中就可以直接看到. 配置文件一般保存在: 
    /etc/ldap/slapd.d
  
  而 schema 保存的路径是 
    /etc/ldap/slapd.d/cn=config/cn=schema
  当我们添加了新的 schema 了以后, 会在这个目录下看到我们新添加的 schema 文件, 扩展名是 .ldif, 文件名中会包含有我们需要的数字.
  
  至于 LDAP 为什么要对 Schema 进行排序, 按照名称来引用不就可以了吗? 为什么要加入数字呢? 我个人的理解是: 每个 Schema 的重要程度不同, 因此加载的优先顺序不同, 所以需要排序, 因此产生了数字, 至于我的理解对不对, 那就另当别论了.
  
  
  
4.6.4 LDAP 如何进行权限控制?
树形结构中的节点, 它仅仅是个节点, 当我们把对象的信息放在到节点中去的时候, 我们知道, 他的对象类型是什么? 它都有哪些属性可以使用, 不同的对象类型有不同的属性, 那么, 我们是如何凭着这些对象类型, 凭着这些属性来进行权限控制的呢? 

首先我们得明白, 不管对象有多少种类型, 他们都可以归属为两种之中的一种:
A) 人
B) 物

当我们进行授权的时候, 实际上表达的意思是说, 让某人可以操作另外的人或者物体, 因此, 只有节点中的对象是人的情况下, 他才获得权限, 他才可以对其他节点进行操作, 当然也可以对自身这个节点进行操作.
因此, 当节点的类型是人的时候, 他肯定拥有密码这个属性, 当他凭着密码连接到 LDAP 的时候, LDAP 可以判断得出来他是个人, 他处在树形结构中的位置在哪里. 通过权限配置后, LDAP 就可以知道, 连接进来的这个人, 对应的节点在哪里, 该节点可以对哪些节点进行操作, 这个是可以明确的. 

因此进行权限认证的时候, 实际上就是提供两个节点, 看看其中的一个节点是不是可以操作另外的节点. 这就是 LDAP 可以进行权限认证的原因.

这里有四个问题需要说明: 

A) 如果验证不通过, LDAP 是如何作出限制行为的呢?    
  实际上, LDAP  能够作出的限制行为就是, 验证不通过的情况下, 不对外提供任何信息, 除此之外, 没有其他办法了.      
  但问题是, 其他节点可能表示的是功能按钮, 控制功能按钮的限制行为是在业务系统中进行控制的, 不是在 LDAP 中进行控制, 对于这种情况, LDAP 只能对外提供验证结果, 这需要业务系统进行配合才能控制.
  
B) 对于对于系统访问的限制和自动放行
  我们假设, 如果节点 A 表示的是系统 X, 节点 A 下面的节点 B 表示的是系统 X 下的模块 Y, 如果权限认证后, 某个人对节点 B 拥有操作的权限, 也就是可以操作该模块 Y, 那我希望这个人可以访问系统 X, 换句话说, 我希望节点 A 能够自动放行, 而不是对他进行限制.

  这里涉及到节点的权重问题, 当父节点和子节点的权限设置发生冲突的情况下, 哪个的权重最高? 如果想通过 LDAP 来设置, 很显然, 有办法, 我们只能通过代码去实现这个功能. 

C) 那就是权限控制在树形结构中的上下方向问题, 这曾经让我困惑, 但是现在看起来没有什么问题. 举两个例子:
  1) 组织结构中的权利大小的方向, 上面的人权利越大, 下面的人权利越小, 换句话说, 下层节点不能拥有上层节点所拥有的全部权限.
  2) 组织中的人, 他可以操作公司中的打印机, 在树形结构中, 打印机很可能在公司这个节点的下面, 但是人却在部门下面, 因此人所处的节点的层级比打印机要低, 但这种情况下, 人这个节点又可以操作上层节点, 只是他们不是同属于一个路径的.

  这个看起来有冲突, 但其实没有问题.
  
E)如果验证不通过, 限制行为在哪里控制?
  上面我们提到过, 如果验证不通过, LDAP 除了不能对外提供信息外, 别无他法, 因为限制行为需要外面的业务系统来支持。 但是, 在微服务的架构中, LDAP  仅仅是个数据库, 我们往往需要在 LDAP 外面包一层壳, 换句话说, 我们需要用代码开发出用户管理系统, 然后再对外提供接口, 所有对 LDAP 的操作都是由用户管理系统来操作, 这样的话, 从微服务网关外面的请求进来的时候, 往往会先通过用户管理系统, 这样, 限制行为就可以在用户管理系统作出了, 但是, 有些对功能按钮的限制需要由业务系统本身去做, 因此, 什么情况下, 限制行为由用户管理系统来做,什么情况下限制行为由业务系统去做? 答案是, 凡是对系统级别的验证, 限制行为由用户管理系统去做, 凡是涉及到管理模块和功能按钮的验证, 用户管理系统提供验证结果, 限制行为由业务系统去做.

至于LDAP 中权限控制的具体操作, 那就使用 ACL 来进行控制, 后面我们会讲.
4.6.5 如何理解, 模型数据和节点对象的不同?
前面我们提到, 属性指的是属性模板, 而不是指具体节点中的属性, 这两不不一样, 如何理解?

实际上, 我不说这一点, 大部分的人都能够理解, 特地这么说就是为了让不明白的人更容易理解.

属性模板是指配置文件, 有了配置文件, 在给节点设置对象的时候, 就可以知道有哪些对象可以使用, 这些对象有哪些属性可以使用, 因此在 OpenLdap 中, 配置文件, 也就是模型数据, 存放在 /etc/ldap/slapd.d 目录下, 而设置节点对象后产生的数据, 保存在数据库中, 数据库文件的地址是在: /var/lib/ldap

需要注意的是: 由于 LDAP 是树形结构的信息, 因此使用目录文件来保存数据库中的数据, 这跟 mysql 不同.

4.7 如何使用 LDAP ?

由于博主本人使用的是 OpenLdap, 因此当我们讲述如何使用 LDAP 的时候, 实际上讲的就是如何使用 OpenLdap, 如果由设计到其他 LDAP 服务器, 会特别说明.

4.7.1 如何安装 OpenLdap?

博主本人把 OpenLdap 安装在 Docker 里面, 这样当我们进行测试的时候, 如果操作失误, 就不会影响到机器本身, 顶多是 docker 容器崩溃, 我们重新安装就可以了, 因此, 博主强烈建议大家把 OpenLdap 安装在 docker 中, 下面这条命令就是在 Docker 中安装 OpenLdap 的:

1
2
3
4
5
6
7
8
9
sudo docker run -p 389:389 -p 636:636 --name xwanlion-openldap \
--env LDAP_ORGANISATION="xwan" \
--env LDAP_DOMAIN="xwanlion.github.io" \
--env LDAP_ADMIN_PASSWORD="admin" \
--volume /container/service/slapd/assets/config/bootstrap/ldif/custom \
--volume /data/slapd/database:/var/lib/ldap \
--volume /data/slapd/config:/etc/ldap/slapd.d \
--detach osixia/openldap:1.5.0
--copy-service

其中:
sudo xxx 这行是 docker 命令, run 表示运行一个新的容器, -p 用来设置映射端口, –name 用来设置容器名称
–env:
有三个, 分别用来设置公司名称, 域名, 以及登录数据库的密码, 那么, 登录数据库的用户名是什么呢? 在 LDAP 中, 登录的时候一般不使用用户名, 而是使用 dn 来登录, 上面这个命令, 登录的 dn 是:
cn=xwan,dc=xwanlion,dc=github,dc=io

volume /container/service/slapd/assets/config/bootstrap/ldif/custom
由于容器镜像启动时可以加载 LDAP 的脚本文件, 用于作出初始化配置, 这一行就是用来设置脚本文件存放在容器中的哪个目录

–volume /data/slapd/database:/var/lib/ldap \
为了防止数据丢失, 所以把数据库地址映射到容器的宿主机器中

–volume /data/slapd/config:/etc/ldap/slapd.d
为了防止数据丢失, 所以把配置文件的地址映射到容器的宿主机器中, 这里的配置文件就是上面我们提到的模型数据

–copy-service 启动的时候, 加载的脚本文件不要覆盖原有的配置。

  详细的说明, 请看制作该镜像的官方主页: github.com/osixia/docker-openldap
  
4.7.2 如何添加删除修改节点数据?

请注意, 这里所说的修改和删除, 是指修改和删除节点数据, 而不是修改和删除模型数据, 也就是如何对数据库中的数据如何进行操作, 不是对配置文件中的模型数据进行操作。

关于对数据库的操作, 方式有:
A) LDAP 命令
B) 先把操作的命令写在文件中, 然后再用 LDAP 命令指明需要执行的执行文件
C) 使用客户端来进行操作, 博主本人使用过 ApacheDS, 因此后面提到客户端, 一般是指 ApacheDS。
D) 使用代码来操作, 博主本人使用 ldapjs 来操作过, 后面会讲。

在上面这些方式中, 其他方式都是在 LDAP 命令的基础上扩展出来的, 因此我们先说说如何使用 LDAP 命令, 但通过执行文件来加载比较合适, 因此后面我们更倾向于使用方式 B, 其他的方式, 我们会根据情况在后面的章节中说明。

4.7.2.1 增加命令 ldapadd
常用的选项有:
-x 进行简单认证
-D 用来绑定服务器的DN
-H 目录服务器的地址
-w 绑定DN的密码
-f 通过ldif文件来添加对象数据(Entry)

举例子:
ldapadd -x -H ldapi:/// -D “cn=xwan,dc=xwanlion,dc=github,dc=io” -w admin -f /xwanlion/openldap/user1.ldif

在这个例子中,
-x 表明需要登录验证,
-H 指明登录的地址, 由于地址是在本地, 因此只需要写 ldapi:/// 即可, 完整的形式应该是 ldapi://127.0.0.1:666/
-D 指明登录名是什么, 也就是 dn 是什么,
-w 指明登录的密码是什么,
-f 指明执行文件在哪里

4.7.2.2 查询命令 ldapsearch
常用的选项有以下几个:
-x:进行简单认证。
-D:用来绑定服务器的dn。
-w:绑定dn的密码。
-b:指定要查询的根节点。
-H:指明目录服务器的地址

举例子:
ldapsearch -x -b “dc=xwanlion,dc=github,dc=io”

4.7.2.3 修改命令 ldapmodify
修改命令的选项跟添加命令差不多, 这里不在列举. 修改的方式有两种:
A) 把修改操作定义在文件中, 然后加载文件执行命令。
B) 输入一个命令, 然后根据 LDAP 的提示输入数据, 直到命令执行完毕, 这种成为交互式。

下面我们采用交互式进行说明:
目的:
对节点数据 “cn=张三,dc=xwanlion,dc=github,dc=io” 进行修改, 修改为:
a) 将其属性值 sn 从”张三”更改为 “我是张三”

步骤:
a) 在命令行输入: ldapmodify -x -H ldapi:/// -D “cn=xwan,dc=xwanlion,dc=github,dc=io” -W admin
b) 上面的命令执行后, 命令行会进入等待状态, 等待我们的输入, 输入以下内容:
dn: cn=张三,dc=xwanlion,dc=github,dc=io
changetype: modify
replace: sn
sn: 我是张三

  说明:
  dn 表示需要对哪个节点数据进行修改
  changetype 表示做什么操作? 选择项有: add, modify
  replace 表示要对哪个属性进行修改
  sn 表示该属性的值最终修改成什么样子
  

如果采用文件进行修改, 请将上面的步骤 b) 写在文件中, 然后加载命令为:
ldapmodify -x -H ldapi:/// -D “cn=xwan,dc=xwanlion,dc=github,dc=io” -W admin -f /xwanlion/openldap/edit_user.ldif

4.7.2.4 删除命令 ldapdelete
修改命令的选项跟添加命令差不多, 这里不在列举。
举例子:
ldapdelete -x -H ldapi:/// -D “cn=xwan,dc=xwanlion,dc=github,dc=io” -W admin “cn=张三,dc=xwanlion,dc=github,dc=io”

注意:
a) 只能删除叶子节点, 无法删除还有子节点的父节点

4.7.2.5 对节点数据进行操作的一些说明
A) 如果 OpenLdap 安装在 docker 容器中, 应该进入容器后在进行操作, 具体请查看 docker 命令, 后面有参考.
B) 命令的选项, 可以参考这里: openldap.org/doc/admin24/runningslapd.html

4.7.3 如何自定义 ObjectClass, 如何自定义属性?

4.7.3.1 什么情况下需要自定义 ObjectClass 和属性?
当我们需要自定义 ObjectClass 和属性 的时候, 这意味着 LDAP 中不存在我们需要的对象模型和属性模型, 所以需要自定义。因此, 我们应该优先检查以下, LDAP 中是否存在我们需要的 ObjectClass 和属性。
检查之后还是觉得应该需要自定义, 才考虑下一步.

4.7.3.2 如何知道 LDAP 中是否存在我们需要的 ObjectClass 和属性?
这个问题可以转换成下面两个问题:
A) LDAP 有哪些对象我怎么知道?
B) 某个对象有哪些属性我怎么知道?

方法是:
A) 安装 ApacheDS 客户端,
B) ApacheDS 客户端连接到 OpenLdap 服务器
C) ApacheDS 客户端主页面中的目录树, 选中我们设定的跟节点。
D) 在菜单中查询, 找到 LDAP ==> open schema browser

在这里, 你就可以看到 LDAP 中列出的所有 ObjectClass, 以及对应的属性有哪些, 如果怀疑不是最新的话, 需要更新, 步骤如下:
A) ApacheDS 客户端主页面中的目录树, 选中我们设定的跟节点。
B) 点击右键弹出菜单
C) 添加新的节点(Entry)
D) 在弹出的页面中点击下一步, 客户端会要求我们选择 ObjectClass.
E) 在 aviable object Class 中有搜索按钮, 搜索按钮的旁边有刷新按钮, 点击他就可以了

当然, 如果你觉得上面的方法太麻烦, 也可以使用下面的命令来查找:
ldapsearch -s base -b “cn=subschema” -D cn=xwan,dc=xwanlion,dc=github,dc=io -w admin -x -h 127.0.0.1 objectclass=subschema objectclasses attributetypes

4.7.3.3) 如何自定义 objectClass? 如何自定义属性?
需要记住的是, 我们不能对现有的 ObjectClass 添加属性, 我们只能把自定义属性添加到新的 ObjectClass 中, 一般情况下, 我们要定义新的 ObjectClass, 就需要添加新的 Schema, 一旦有了自己的 Schema, 后续添加新的 ObjectClass, 就不用再添加新的 schema 了, 直接在自己的 Schema 中添加 ObjectClass 就可以了.

如果我们需要对系统中已经存在的对象添加新的属性, 怎么办?
答案是, 添加新的 ObjectClass, 该 ObjectClass 继承自系统中的对象, 然后再添加属性.

以同时添加新的 ObjectClass 和属性为例, 下面是具体的步骤:
A) 指明 Schema 的名称是什么
B) 定义属性列表
C) 定义 ObjectClass, 同时指明必填属性和可选属性有哪些

上面这些步骤, 只需要在文件中定义就可以了, 文件内容如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
dn: cn=xwanlion,cn=schema,cn=config
changetype: modify
add: olcAttributeTypes
olcAttributeTypes: ( xxx.xxx.xxx.1
NAME 'wechat'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: ( xxx.xxx.xxx.2
NAME 'email'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )
-
add: olcObjectClasses
olcObjectClasses: ( xxx.xxx.yyy.3
NAME 'xwanPerson'
DESC 'xwanPerson'
SUP Person
STRUCTURAL
MAY (wechat $ email))

说明:
dn: 表示我们定义的 Schema 的 dn
changetype 表示我们需要对 schema 进行什么样的操作, 我们要修改, 所以使用 modify.
add: olcAttributeTypes 表示我们要添加自定义属性
olcAttributeTypes 表示我们要添加的属性列表, 后面的 xxx.xxx.xxx.1 表示 id, 不可用跟现有的 id 重复
NAME 表示属性名称或者 ObjectClass 的名称
EQUALITY 匹配方式
SYNTAX 匹配方式对应的 id.

add: olcObjectClasses 表示我们要添加 ObjectClass, xxx.xxx.yyy.3 是 id, 不可重复
DESC 对类进行说明
SUP 父类是哪个, 必须是已经存在的 ObjectClass
STRUCTURAL ObjectClass 的类型, 结构型
MAY 可选属性有哪些, 每个属性之间使用 $ 分隔, 如果有必填属性, 可以使用 MUST, MAY 和 MUST 可以同时存在

注意:
A) 每一行末尾不可以有空格
B) 下一行开始部分如果有空格, 表示延续上一行, 否则表示新的一行开始
C) 不可用有空行, 否则 LDAP 会认为执行命令到此结束。
D) 使用命令来加载文件, 使命令生效: ldapmodify -H ldapi:/// -Y EXTERNAL -f xxxx.ldif
D) 关于id和匹配方式, 进一步的说明请参考这里: openldap.org/doc/admin24/schema.html

4.7.3.4) objectClass 和 olcObjectClasses 有什么区别?
在比较老旧的版本中, OpenLdap 通过配置文件来添加 ObjectClass, 并且需要重启 OpenLdap 才能使配置生效, 这种方式称之为静态配置, 但是这种方式根本就无法适应开发的要求, 开发中往往需要动态配置, 要求能够及时添加 ObjectClass, 并且不用重启服务也能够生效, 因此就产生了 olcObjectClasses, 同样的道理, 原先使用的 AttributeTypes 也更改为 olcAttributeTypes。

4.8 如何通过 acl 来进行权限控制?

执行步骤:
A) 在文件中写好操作命令
B) 使用下面的命令来文件中的命令, 使配置生效: ldapadd -H ldapi:/// -Y EXTERNAL -f /xwanlion/openldap/acl.ldif

下面是文件内容:

1
2
3
4
5
6
7
8
9
dn: olcDatabase={1}mdb,cn=config
changetype: modify
# 只有自己可以修改密码, 不允许匿名访问, 允许g-admin组修改
add: olcAccess
olcAccess: {0}to attrs=userPassword by self write by anonymous auth by group.exact="cn=g-admin,dc=xwanlion,dc=github,dc=io" write by * none
-
# 自己可以修改自己的信息,g-admin组可以修改任何信息
add: olcAccess
olcAccess: {1}to * by self write by group.exact="cn=g-admin,dc=xwanlion,dc=github,dc=io" write by * none
  说明:
    A) dn 中的数字不是随便填写的, 查看方法是进入下面的目录, 查看该目录下的文件:        
      /etc/ldap/slapd.d/cn=config 
      你会发现有一个类似于olcDatabase={1}mdb.ldif的文件, 但不会完全相同, 不相同的有可能是:
      1) 数字不相同
      2) 有可能不是 mdb, 有可能是 hdb.
      
    B) olcAccess 是指对节点进行权限控制, 大括号中的数字, 博主本人还高不清楚是什么具体意思, 只知道需要这么填写.
    C) 关于 acl 的详细说明, 请参考这里: openldap.org/doc/admin24/access-control.html

4.9 ldapjs 操作

4.9.1 如何使用 ldapjs 连接 openldap 服务器?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let ldapClient = function() {
console.log("aaa");

client = ldap.createClient({
url: ['ldap://127.0.0.1:389']
});

client.bind('cn=admin,dc=xwanlion,dc=github,dc=io', 'admin', (err) => {
console.log(err);
});

client.on('error', (err) => {
console.log("err");
});

client.on("connect", (res) => {
console.log("successd");
});
}
4.9.2 如何通过 ldapjs 添加分组?
1
2
3
4
5
6
7
8
9
10
let addOu = function() {
const entry = {
ou: 'dev',
objectclass: 'organizationalUnit'
};

client.add('ou=dev,dc=xwanlion,dc=github,dc=io', entry, (err) => {
if (err == null) console.log("added!");
});
}
4.9.3 如何通过 ldapjs 添加节点?
1
2
3
4
5
6
7
8
9
10
11
12
13
let addEntry = function() {
const entry = {
cn: 'foo',
sn: 'bar',
mail: ['foo@bar.com', 'foo1@bar.com'],
objectclass: 'InetOrgPerson'
};

client.add('cn=foo,ou=coders,dc=xwanlion,dc=github,dc=io', entry, (err) => {
console.log("add entry", err);
if (err == null) console.log("added!", err);
});
}
4.9.4 如何通过 ldapjs 修改节点的属性?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let updateEntry = function() {
const entry = {
sn: 'bar',
mail: ['foo@bar1.com'],
objectclass: 'InetOrgPerson'
};


let change = {
operation: 'replace',
modification: entry
};

client.modify("cn=foo,ou=coders,dc=xwanlion,dc=github,dc=io", change, function(err) {
if (err != null) console.log(err);
});

}
4.9.5 如何通过 ldapjs 修改节点的 dn ?
1
2
3
4
5
let modifyDn = function () {
client.modifyDN('ou=dev,dc=xwanlion,dc=github,dc=io', 'ou=coders', (err) => {
if (err != null) console.log(err);
});
}
说明

A) 修改节点的时候, dn 和属性不能同时修改, 只能分开来修改。

5 如何设计

5.1 分析需求

回到老大交给博主的任务, 要求能够自动化, 不能手动配置, 而且还能够灵活, 所谓灵活是指:
不管公司是小的公司, 还是中型的公司, 甚至是大集团公司, 在权限控制上, 都能够应付, 这意味着下面这些结构都能够应付:
A) 小公司: 老板 ==> 部门 ==> 职员
B) 中型规模的公司: 老板 ==> 部门 ==> 小组 ==> 职员
C) 集团公司: 老板 ==> 分公司(子公司) ==> 部门 ==> 小组 ==> 职员

这些结构层级结构不同, 各类公司的权限分配方式也不同

另外还有, 公司内部的人员, 比如开发人员和特殊权限的管理人员, 要求能够对这些公司进行操作, 这意味着:
A) 他们可以对某些节点的数据拥有查看权限, 但是不能添加,修改, 也不能删除
B) 他们可以对某些节点的数据拥有查看, 添加的权限, 也可以修改, 但是不能删除
C) 他们可以对某些节点的数据拥有查看, 添加, 修改, 甚至拥有删除的权限.

5.2 结构树设计

为此, 我们设计了这样的结构树:

xwanlion.github.io
|–admin
|–g-admin
|–人事部
|–|–张三
|–开发部
|–|–李四
|–客服
|–|–王五
|–系统服务中心
|–|–users
|–|–|–老六
|–|–宇宙公司
|–|–|–事业部
|–|–|–|–田七

说明:
A) admin 是 openldap 的超级帐号, 安装时会按照提示提供
B) g-admin 是管理组, 加入该组的人拥有超级权限, 具体权限待定
C) 人事部, 开发部, 客服也会分配给特殊权限, 具体权限待定
D) 系统服务中心, 对外提供的接口, 所产生的所有节点数据, 都在此节点下面
E) users 节点, 当用户注册后, 但是还没有归属于某个公司的情况下, 在此节点下面, 用户归属于某家公司后拉到该公司的节点下面
F) 宇宙公司是假想出来的公司名称

5.3 权限控制分析

5.3.1 控制方案
为了方便, 我们把直接操作 OpenLDAP 的系统称之为用户管理系统, 其他系统称之为业务系统

5.3.1.1 全部使用 acl 来控制
这个又可以分为两种方式:
A) 业务系统提供授权对象的节点和被控制对象的节点, 由用户管理系统来生成 acl 语句并且自动加载生效, 同时把授权对象和被控制对象的节点以及关系保存下来, 用户登录的时候, 把这些把授权对象和被控制对象返回给给业务系统

B) 业务系统填写 acl 语句, 有用户管理系统来检查语句是否有效, 然后再决定是否加载生效。 当用户登录的时候,把 acl 语句一起反馈给业务系统.

5.3.1.2 结合 mysql 数据表来进行
业务系统提供授权对象的节点和被控制对象的节点, 用户管理系统接收后保存在 mysql 数据表中, 用户登录的时候, 直接将授权对象和被控制对象的节点以及关系返回给业务系统.
那么, 没有 acl 控制的情况下, 如何判断节点之间的父子关系, 以及该节点是否在被控制的范围之内? 做法是: 给每个节点一个 Id, 根据给定的两个节点ID, 要能够判断得出来他们是否是父子或者是祖孙关系.

5.3.2 验证以及控制的逻辑

验证的时候分析哪些是需要用户管理系统拦截, 如果:
A) 限制行为在管理系统这边, 验证不通过的情况下直接拦截掉, 让用户无法访问业务系统。
B) 限制行为不在用户管理系统这边, 验证不通过的情况下, 放行给业务系统执行决定
C) 限制行为在管理系统这边, 验证通过的情况下直接拦放行.
D) 限制行为不在管理系统这边, 验证通过的情况下直接拦放行.

6 额外操作说明

6.1 docker 如何查询容器?

查看正在运行的镜像有哪些: sudo docker ps
查看所有安装的镜像有哪些: sudo docker ps -a

6.2 docker 如何进入容器?

A) 先查看容器,获取镜像ID
B) sudo docker exec -it ID bash
记得把Id修改成真实的ID.

6.3 如何安装 docker?

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

详细参考: docs.docker.com/engine/install/ubuntu/

6.4 如何启动 docker 容器?

sudo docker start Id

6.5 如何停掉 docker 容器?

sudo docker stop Id

6.6 如何查询 docker 容器的映射地址?

sudo docker inspect ID

6.7 OpenLdap 如何导入导出数据库

导入: ldapadd -x -D “cn=xwan,dc=xwanlion,dc=github,dc=io” -W admin -f ldapbackup.ldif
备份: slapcat -v -l ldapbackup.ldif

6.8 如何配置日志

博主本人还没有操作过, 所以给出别人的地址: ilanni.com/?p=13775


分享到:


  如果您觉得这篇文章对您的学习很有帮助, 请您也分享它, 让它能再次帮助到更多的需要学习的人. 您的支持将鼓励我继续创作 !
本文基于署名4.0国际许可协议发布,转载请保留本文署名和文章链接。 如您有任何授权方面的协商,请邮件联系我。

Contents

  1. 1 背景介绍
  2. 2 目的
  3. 3 难点
  4. 4 基础知识介绍
    1. 4.1 ldap 是什么?
    2. 4.2 什么是 RBAC? 他跟 LDAP 有什么不同?
    3. 4.3 LDAP 跟 mysql 有什么不同?
    4. 4.4 实现 LDAP 规范的数据库系统有哪些?
    5. 4.5 为什么需要 LDAP?
    6. 4.6 LDAP 的底层是如何实现的?
      1. 4.6.1 LDAP 是如何识别一个对象的?
      2. 4.6.2 LDAP 的底层是如何知道, 有哪些数据类型可以使用?
      3. 4.6.3 LDAP 如何识别对象类型? 我们可不可以添加新的对象类型?
      4. 4.6.4 LDAP 如何进行权限控制?
      5. 4.6.5 如何理解, 模型数据和节点对象的不同?
  • 4.7 如何使用 LDAP ?
    1. 4.7.1 如何安装 OpenLdap?
    2. 4.7.2 如何添加删除修改节点数据?
    3. 4.7.3 如何自定义 ObjectClass, 如何自定义属性?
  • 4.8 如何通过 acl 来进行权限控制?
  • 4.9 ldapjs 操作
    1. 4.9.1 如何使用 ldapjs 连接 openldap 服务器?
    2. 4.9.2 如何通过 ldapjs 添加分组?
    3. 4.9.3 如何通过 ldapjs 添加节点?
    4. 4.9.4 如何通过 ldapjs 修改节点的属性?
    5. 4.9.5 如何通过 ldapjs 修改节点的 dn ?
    6. 说明
  • 5 如何设计
    1. 5.1 分析需求
    2. 5.2 结构树设计
    3. 5.3 权限控制分析
      1. 5.3.1 控制方案
      2. 5.3.2 验证以及控制的逻辑
  • 6 额外操作说明
    1. 6.1 docker 如何查询容器?
    2. 6.2 docker 如何进入容器?
    3. 6.3 如何安装 docker?
    4. 6.4 如何启动 docker 容器?
    5. 6.5 如何停掉 docker 容器?
    6. 6.6 如何查询 docker 容器的映射地址?
    7. 6.7 OpenLdap 如何导入导出数据库
    8. 6.8 如何配置日志