基于钉钉 + Virtual-LDAP + KeyCloak 的内网统一认证系统

0. 架构

基于钉钉的内网统一认证

1. 背景

最近公司内网的各种系统部署得越来越多,每个系统都有自己的认证方式和账号体系,这导致大家在每个地方都要去注册一个账号,并且不利于公司统一管理密码安全策略,例如密码到期策略、密码复杂度策略以及强制二次验证等。

以及对于一部分短平快的内网应用来说,我们并没有时间去为它开发一套用户体系,这个时候还是希望能有一个统一的前端反向代理来处理用户认证这个流程。

为此,我就去寻找了一些解决方案,并且为了解决开源系统并不能对接外部用户系统的问题,开发了一个 Node.JS Package(Virtual-LDAP)来提供 LDAP 能力。

2. 问题

虽然总的需求是一个很简单的功能,但是这其中还是有很多细节的问题需要考虑。

认证方式

对于有一些开源系统,它本身是支持 OAuth 来进行用户认证的,这个时候只需要去选择一个支持 OAuth 的用户管理系统就可以了,甚至基于开源库自己去开发一个也并不困难。但是对于某一些开源系统来说,它并没有提供 OAuth 认证接入的支持,只提供了 LDAP 接入。

例如最近我们引入了 Metabase 作为面向运营的快速 BI 分析工具,但是它除了可以接入 Google 账号作为认证方式,就只能接受 LDAP 作为认证方式了。这个时候就得去寻找一个认证系统,同时能支持 OAuth 以及 LDAP。

用户体系

引入用户管理系统之后,还有另外一个问题需要考虑,就是现在员工的用户体系需要去管理。对于一个小公司来说,目前并没有一个统一的工具去同时管理员工的各种信息以及状态。

但是目前公司已经引入钉钉作为公司的交流沟通工具,以及作为各种流程的审批处理系统,HR 也会在钉钉上去管理所有员工的状态,以及员工的组织架构。

因此,这个用户管理系统最好需要能支持同步钉钉中的用户体系,这样就不需要额外的人力去维护用户管理系统,以及在有员工状态变更的时候,能及时同步,避免出现授权外的访问。

数据同步

有了 OAuth,有了 LDAP,还有了钉钉这个数据源,因此就需要处理好各个 Provider 之间的数据同步问题,避免人工去维护各个系统里面的用户数据,做到以钉钉的数据为基础,用户管理系统提供能力,做到各个系统各个认证方式得到的用户数据都是一致的。

以及,对于 LDAP 来说,它是有组织架构的概念的,这个可以在对接到它的应用系统中,快速映射到应用中的用户组,实现用户的权限自动分配和管理,避免每次有新员工加入,都需要单独去配置一次各个业务系统的权限。因此,这也要求用户管理系统能够做到同步钉钉的组织架构,无需额外管理。

3. 解决方案

KeyCloak

KeyCloak Admin Console

KeyCloak 是一个开源软件产品,旨在为现代的应用程序和服务,提供包含身份管理和访问管理功能的单点登录工具。

KeyCloak 提供了丰富的功能用于公司内部单点登录上,包括:

  • 内置的用户账号管理界面
  • OpenID Connect 以及 OAuth 2.0 支持
  • LDAP 同步支持
  • 支持自定义主题

虽然之前也找到了 FreeIPA 这个开源系统,功能也同样强大,但是在部署过程中碰到了很多问题,最后还是选择了 KeyCloak。

Pomerium

Pomerium

对于大多数开源系统来说,可能都已经内置了对于 OAuth 或者 LDAP 的支持,对于这些系统,只需要按它的需求,去配置 OAuth 或者 LDAP 认证即可接入 KeyCloak。

但是对于一些静态页面,或者是自行开发的内部应用,可能并没有时间去额外添加用户认证支持,而这个需要就需要一个带用户认证支持的反向代理了。

Pomerium 就是这样一个反向代理应用,它作为一个 Identity-Aware Proxy,用于给内部应用增加安全访问的能力。

Pomerium 的主要功能有:

  • 支持接入 OAuth 认证
  • 支持配置内部应用允许哪些域,或者哪些用户可以访问
  • 支持 WebSocket 转发
  • 支持转发时自定义 HTTP Header
  • 支持 JWT 或 HTTP Header 传递用户 Session 信息到后端内部应用
  • 提供一个隐式 domain/.pomerium 路径来显示当前用户信息
  • 支持使用 Wildcard SSL 证书给后端服务统一增加 SSL 支持

通过 Pomerium,就可以将内部不带用户认证的应用直接加上用户认证的能力,避免非授权访问,并且可以直接通过 JWT 传递过来的用户信息,进行额外的用户权限管理。

Virtual-LDAP

Virtual-LDAP 是我开发的一个 Node.JS 程序包,用于使用自定义数据源来提供一个 LDAP 服务,可以将非 LDAP 用户系统(目前支持钉钉)对接到只支持 LDAP 认证的系统当中。并且支持保存用户密码到数据库中,以使用与非 LDAP 用户系统不一样的密码数据。

Virtual-LDAP 主要功能有:

  • 定时同步钉钉组织架构以及员工信息
  • 提供基本 LDAP 功能,包括 bind、search、modify 等操作
  • 通过配置文件配置管理账号
  • 独立保存用户密码,以 SHA256+Salt 存储
  • 支持自定义分组能力,在钉钉组织架构之外扩展用户分组
  • 支持自定义用户数据 Provider,可以自行开发接入钉钉以外的用户系统

Virtual-LDAP 主要解决的问题是市面上的 LDAP 服务系统并不支持接入钉钉用户系统,钉钉也并没有提供一个 LDAP 方式的数据源供企业内部使用,因此需要额外的系统去对接钉钉的用户系统,以 LDAP 方式提供用户数据,供 KeyCloak 及其他内部系统使用。

当然,Virtual-LDAP 并不是一个全功能的 LDAP 服务器,它仅支持有限的 LDAP 操作,但是在对接到 KeyCloak 作为 User Federation 已经足够用了,以下功能都可以正常使用:

  • User Synchronize
  • Group Synchronize
  • User 和 Group 的映射关系,以及完整的组织架构
  • 在认证时,使用 LDAP bind 进行认证
  • 修改密码时,使用 LDAP modify 同步保存密码

通过 Virtual-LDAP,就可以直接由 HR 去管理公司的员工以及组织架构,并且不需要额外再去另外一个系统中同步维护相关信息,Virtual-LDAP 会使用钉钉 OpenAPI 自动从钉钉获取最新的员工列表以及组织架构信息。

4. 部署

对于 KeyCloak 和 Pomerium 来说,官方均已提供 Docker 镜像,因此直接通过 Docker 即可以快速部署使用。

  • KeyCloak Docker:https://hub.docker.com/r/jboss/keycloak
  • Pomerium Docker:https://hub.docker.com/r/pomerium/pomerium

对于 Virtual-LDAP 来说,可以直接从源代码运行,也可以自己编写一个 Dockerfile 来使用 Docker 部署。

一个典型的 Virtual-LDAP Dockerfile 可以像下面这样:

Dockerfile

FROM node:13.7.0-alpine

WORKDIR /app
COPY package.json /app/
RUN npm install

COPY index.js /app/
COPY config.js /app/

CMD [ "node","index.js" ]

index.js

const server = require('virtual-ldap');

server.setupVirtualLDAPServer(require("./config"));
server.runVirtualLDAPServer();

package.json

{
"dependencies": {
"virtual-ldap": "^0.1.1"
}
}

当然,必需的配置文件 config.js 也是不可少的,具体如何配置可以参考 config.sample.js

5. 小结

总的来说,如果有额外的人力去维护两份员工信息以及组织架构,只需要 KeyCloak 就可以解决以上很多问题。但是对于小公司来说,使用额外的人力总归不是很高效的方式,并且钉钉拥有更完整的用户系统管理功能,对于 HR 来说使用上可能也更为友好。

希望 Virtual-LDAP 能给同样希望在公司内部部署统一认证系统,以及使用钉钉作为企业交流沟通的朋友们有所帮助。

6. 参考

—EOF—

发表评论?

22 条评论。

  1. 很棒,优雅的解决办法。
    这个领域看似简单,做起来其实真的太麻烦了

  2. 哈哈,握爪,跟着Metabase看LDAP,看钉钉看到了博主的文章 :mrgreen:

  3. 安装好了。可是keycloak配置的时候不知道各个参数怎么填。能不能也出个教程啊。

  4. 运行npm start的时候报错,日志如下:

    2020-05-25T18:02:29.368Z i provider Setting up provider ‘dingtalk’
    2020-05-25T18:02:30.035Z i dingtalk-provider Got 31 ‘departments’
    (node:9668) UnhandledPromiseRejectionWarning: TypeError: Cannot read property ‘name’ of undefined
    at allDeps.forEach.d (/tmp/node_modules/virtual-ldap/lib/providers/dingtalk.js:115:25)
    at Array.forEach ()
    at fetchAllDepartments (/tmp/node_modules/virtual-ldap/lib/providers/dingtalk.js:110:11)
    at process._tickCallback (internal/process/next_tick.js:68:7)
    (node:9668) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
    (node:9668) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

  5. 运行npm start的时候报错,日志如下:
    (node:9668) UnhandledPromiseRejectionWarning: TypeError: Cannot read property ‘name’ of undefined
    at allDeps.forEach.d (/tmp/node_modules/virtual-ldap/lib/providers/dingtalk.js:115:25)
    at Array.forEach ()
    at fetchAllDepartments (/tmp/node_modules/virtual-ldap/lib/providers/dingtalk.js:110:11)
    at process._tickCallback (internal/process/next_tick.js:68:7)
    (node:9668) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
    (node:9668) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

    • 需要给相应的 API Key 开启联系人权限,需要读取手机号、邮箱。
      如果组织内联系人没有邮箱属性,那就没法用。

      • (node:8331) UnhandledPromiseRejectionWarning: TypeError: Cannot read property ‘f ilter’ of null
        at reloadFromDingtalkServer (/home/tian263/virtual-ldap/lib/providers/dingta lk.js:229:27)
        at processTicksAndRejections (internal/process/task_queues.js:97:5)
        at async Object.setupProvider (/home/tian263/virtual-ldap/lib/providers/ding talk.js:190:3)
        at async createProvider (/home/tian263/virtual-ldap/lib/utilities/provider.j s:13:5)
        at async /home/tian263/virtual-ldap/lib/server.js:234:5
        (Use `node –trace-warnings …` to show where the warning was created)
        (node:8331) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To termina te the node process on unhandled promise rejection, use the CLI flag `–unhandle d-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejectio ns_mode). (rejection id: 1)
        (node:8331) [DEP0018] DeprecationWarning: Unhandled promise rejections are depre cated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
        这个报错是什么意思呢?

  6. User 和 Group 的映射关系,以及完整的组织架构,组织架构映射时,如果想保留树结构,有同名的组织就会报错。怎样解决或设置才能通过deptid来决定唯一性呢

  7. 钉钉同步到gitlab里面的时候 钉钉没有密码 这个怎么处理呢?

    • virtual-ldap 并不提供同步钉钉密码的功能,第一次使用时用户需要在 KeyCloak 中选择找回密码,这样可以将密码保存到 virtual-ldap 的数据库里面

      • 相当于 virtual-ldap 保存用户信息和密码 然后keycloak可以让用户登录修改密码和人员组织架构?
        其他系统要接入ldap 应该对接 virtual-ldap 吧?比如gitlab接入ldap应该接入virtual-ldap?

  8. 应用的登录和用户管理托管给了 keycloak,如何实现钉钉内免密登录应用?

  9. Virtual-LDAP代码简单,使用方便,但有一点不足:在钉钉上要的权限过多:又要电话号码权限(非必须),又要邮箱权限(必须要有)。
    特别是邮件权限,如果某些员工没有信箱(或没有企业邮箱)的话,就无法导入到V-LDAP来(我司大部分员工都没有录入邮件或分配企业邮箱),另外,V-LDAP中的电话和邮件没有加密,在导入的同时,相当于暴露了整个公司通讯录,所以,我还是小改了一下代码,使用工号+密码登录,修改如下:

    virtual-ldap/lib/providers/dingtalk.js
    代码255行:

    return false;
    ==>
    u.email=u.jobnumber;

    钉钉的应用中,只需要通讯录的只读权限就好了,电话和邮件都不需要(注意:这行代码须配合不分配给电话和邮件权限一起使用)

    • 这些权限主要是为了配合其他各个系统使用,例如邮箱用来供其他系统接入 OAuth 验证,电话可以用来接入 OnCall 系统。
      的确暴露全公司的通讯录是一个隐私问题,但是在控制好 LDAP 访问权限的情况下应该也还好。

发表评论


注意 - 你可以用以下 HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>