go示例:LDAP简介

go示例:LDAP简介

1. LDAP简介

LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)是实现提供被称为目录服务的信息服务.目录服务是一种特殊的数据库系统,其专门针对读取,浏览和搜索操作进行了特定的优化.目录一般用来包含描述性的,基于属性的信息并支持精细复杂的过滤能力.目录一般不支持通用数据库针对大量更新操作操作需要的复杂的事务管理或回卷策略.而目录服务的更新则一般都非常简单.这种目录可以存储包括个人信息,web链结,jpeg图像等各种信息.为了访问存储在目录中的信息,就需要使用运行在TCP/IP 之上的访问协议—LDAP.

LDAP目录中的信息是是按照树型结构组织,具体信息存储在条目(entry)的数据结构中.条目相当于关系数据库中表的记录;条目是具有区别名DN (Distinguished Name)的属性(Attribute),DN是用来引用条目的,DN相当于关系数据库表中的关键字(Primary Key).属性由类型(Type)和一个或多个值(Values)组成,相当于关系数据库中的字段(Field)由字段名和数据类型组成,只是为了方便检索的需要,LDAP中的Type可以有多个Value,而不是关系数据库中为降低数据的冗余性要求实现的各个域必须是不相关的.LDAP中条目的组织一般按照地理位置和组织关系进行组织,非常的直观.LDAP把数据存放在文件中,为提高效率可以使用基于索引的文件数据库,而不是关系数据库.类型的一个例子就是mail,其值将是一个电子邮件地址.

LDAP的信息是以树型结构存储的,在树根一般定义国家(c=CN)或域名(dc=com),在其下则往往定义一个或多个组织 (organization)(o=Acme)或组织单元(organizational units) (ou=People).一个组织单元可能包含诸如所有雇员,大楼内的所有打印机等信息.此外,LDAP支持对条目能够和必须支持哪些属性进行控制,这是有一个特殊的称为对象类别(objectClass)的属性来实现的.该属性的值决定了该条目必须遵循的一些规则,其规定了该条目能够及至少应该包含哪些属性.例如:inetorgPerson对象类需要支持sn(surname)和cn(common name)属性,但也可以包含可选的如邮件,电话号码等属性.

2. LDAP简称对应

  • o– organization(组织-公司)
  • ou – organization unit(组织单元-部门)
  • c - countryName(国家)
  • dc - domainComponent(域名)
  • sn – suer name(真实名称)
  • cn - common name(常用名称)

3. 目录设计

设计目录结构是LDAP最重要的方面之一.下面我们将通过一个简单的例子来说明如何设计合理的目录结构.该例子将通过Netscape地址薄来访文.假设有一个位于美国US(c=US)而且跨越多个州的名为Acme(o=Acme)的公司.Acme希望为所有的雇员实现一个小型的地址薄服务器.

我们从一个简单的组织DN开始:  dn: o=Acme, c=US

Acme所有的组织分类和属性将存储在该DN之下,这个DN在该存储在该服务器的目录是唯一的.Acme希望将其雇员的信息分为两类:管理者(ou= Managers)和普通雇员(ou=Employees),这种分类产生的相对区别名(RDN,relative distinguished names.表示相对于顶点DN)就shi :

  • dn: ou=Managers, o=Acme, c=US
  • dn: ou=Employees, o=Acme, c=US

在下面我们将会看到分层结构的组成:顶点是US的Acme,下面是管理者组织单元和雇员组织单元.因此包括Managers和Employees的DN组成为:

  • dn: cn=Jason H. Smith, ou=Managers, o=Acme, c=US
  • dn: cn=Ray D. Jones, ou=Employees, o=Acme, c=US
  • dn: cn=Eric S. Woods, ou=Employees, o=Acme, c=US

为了引用Jason H. Smith的通用名(common name )条目,LDAP将采用cn=Jason H. Smith的RDN.然后将前面的父条目结合在一起就形成如下的树型结构:

  cn=Jason H. Smith

        + ou=Managers

            + o=Acme

                + c=US

                               -> dn: cn=Jason H. Smith,ou=Managers,o=Acme,c=US

   现在已经定义好了目录结构,下一步就需要导入目录信息数据.目录信息数据将被存放在LDIF文件中,其是导入目录信息数据的默认存放文件.用户可以方便的编写Perl脚本来从例如/etc/passwd,NIS等系统文件中自动创建LDIF文件.

面的实例保存目录信息数据为testdate.ldif文件,该文件的格式说明将可以在man ldif中得到.

在添加任何组织单元以前,必须首先定义Acme DN: 

  • dn: o=Acme, c=US
  • objectClass: organization

这里o属性是必须的

  • o: Acme

下面是管理组单元的DN,在添加任何管理者信息以前,必须先定义该条目.

  • dn: ou=Managers, o=Acme, c=US
  • objectClass: organizationalUnit

这里ou属性是必须的.


ou: Managers

  第一个管理者DN

    dn: cn=Jason H. Smith, ou=Managers, o=Acme, c=US

    objectClass: inetOrgPerson

  cn和sn都是必须的属性

    cn: Jason H. Smith

    sn: Smith

  但是还可以定义一些可选的属性

    telephoneNumber: 111-222-9999

    mail: headhauncho@acme.com

    localityName: Houston

 

  可以定义另外一个组织单元

    dn: ou=Employees, o=Acme, c=US

    objectClass: organizationalUnit

    ou: Employees

 

  并添加雇员信息如下

    dn: cn=Ray D. Jones, ou=Employees, o=Acme, c=US

    objectClass: inetOrgPerson

    cn: Ray D. Jones

    sn: Jones

    telephoneNumber: 444-555-6767

    mail: jonesrd@acme.com

    localityName: Houston

    dn: cn=Eric S. Woods, ou=Employees, o=Acme, c=US

    objectClass: inetOrgPerson

    cn: Eric S. Woods

    sn: Woods

    telephoneNumber: 444-555-6768

    mail: woodses@acme.com

    localityName: Houston

4. 用 Go 操作 LDAP

我们可以用 https://github.com/go-ldap/ldap 这个库来操作 LDAP 他的 example 给的非常的详细,基本看一遍就可以开始抄了… 我们拿其中 userAuthentication 的 example 来举个例子,下为 example 中的示例代码,我增加了若干注释说明

func Example_userAuthentication() {
    // The username and password we want to check
    // 用来认证的用户名和密码
    username := "someuser"
    password := "userpassword"

    // 用来获取查询权限的 bind 用户.如果 ldap 禁止了匿名查询,那我们就需要先用这个帐户 bind 以下才能开始查询
    // bind 的账号通常要使用完整的 DN 信息.例如 cn=manager,dc=example,dc=org
    // 在 AD 上,则可以用诸如 mananger@example.org 的方式来 bind
    bindusername := "readonly"
    bindpassword := "password"

    l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389))
    if err != nil {
        log.Fatal(err)
    }
    defer l.Close()

    // Reconnect with TLS
    // 建立 StartTLS 连接,这是建立纯文本上的 TLS 协议,允许您将非加密的通讯升级为 TLS 加密而不需要另外使用一个新的端口.
    // 邮件的 POP3 ,IMAP 也有支持类似的 StartTLS,这些都是有 RFC 的
    err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
    if err != nil {
        log.Fatal(err)
    }

    // First bind with a read only user
    // 先用我们的 bind 账号给 bind 上去
    err = l.Bind(bindusername, bindpassword)
    if err != nil {
        log.Fatal(err)
    }

    // Search for the given username
    // 这样我们就有查询权限了,可以构造查询请求了
    searchRequest := ldap.NewSearchRequest(
        // 这里是 basedn,我们将从这个节点开始搜索
        "dc=example,dc=com",
        // 这里几个参数分别是 scope, derefAliases, sizeLimit, timeLimit,  typesOnly
        // 详情可以参考 RFC4511 中的定义,文末有链接
        ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, 
        // 这里是 LDAP 查询的 Filter.这个例子例子,我们通过查询 uid=username 且 objectClass=organizationalPerson.
        // username 即我们需要认证的用户名
        fmt.Sprintf("(&(objectClass=organizationalPerson)(uid=%s))", username),
        // 这里是查询返回的属性,以数组形式提供.如果为空则会返回所有的属性
        []string{"dn"},
        nil,
    )
    // 好了现在可以搜索了,返回的是一个数组
    sr, err := l.Search(searchRequest)
    if err != nil {
        log.Fatal(err)
    }

    // 如果没有数据返回或者超过1条数据返回,这对于用户认证而言都是不允许的.
    // 前这意味着没有查到用户,后者意味着存在重复数据
    if len(sr.Entries) != 1 {
        log.Fatal("User does not exist or too many entries returned")
    }

    // 如果没有意外,那么我们就可以获取用户的实际 DN 了
    userdn := sr.Entries[0].DN

    // Bind as the user to verify their password
    // 拿这个 dn 和他的密码去做 bind 验证
    err = l.Bind(userdn, password)
    if err != nil {
        log.Fatal(err)
    }

    // Rebind as the read only user for any further queries
    // 如果后续还需要做其他操作,那么使用最初的 bind 账号重新 bind 回来.恢复初始权限.
    err = l.Bind(bindusername, bindpassword)
    if err != nil {
        log.Fatal(err)
    }
}

总结:

建立连接

  1. 使用 bind 用户先 bind 以获取权限
    • 根据用户名对应的属性写 searchfilter,结合 basedn 进行查询
    • 如果需要认证,用查到的 dn 进行 bind 验证
    • 如果还要继续查询/认证,rebind 回初始的 bind 用户上
    • 关闭连接

命令行

作为一个 cli 工具,命令行部分的设计是很重要的.考虑我们所需要实现的功能

  • 用户查询
  • 用户认证
  • 用特定的 filter 查询
  • 批量认证
  • 批量查询

比如可以按这个方式进行罗列

Go 由一个非常好的 cli 库 cobra,我们就用它来做轮子.

cobra 用起来容易上手,我同样贴一段他的 example 代码来加以注释来说明

package main

import (
  "fmt"
  "strings"

  "github.com/spf13/cobra"
)

func main() {
  // 给后面的 Flags 用的
  var echoTimes int

  // cobra 以层次的方式组织命令.从 rootCmd 开始,每一个命令都通过一个 struct 来配置命令的相关信息
  // 这一行本来在 example 的最下面,我给挪上来了
  var rootCmd = &cobra.Command{Use: "app"}

  // 不同于 rootCmd,我们开始给出比较详细的配置了
  var cmdPrint = &cobra.Command{
  // 命令的名称,同时 [string to print] 等会在 help 时作为 usage 的内容输出
    Use:   "print [string to print]",
  // help 时作为 Available Commands 中,cmd 后的短描述
    Short: "Print anything to the screen",
  // help 时作为 cmd 的长描述
    Long: `print is for printing anything back to the screen.
For many years people have printed back to the screen.`,
  // 限制命令最小参数输入为1,还有其他的参数限制,详见 github 上的说明
    Args: cobra.MinimumNArgs(1),
  // 命令执行的函数,把命令要干的事情放在这里就好了
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Print: " + strings.Join(args, " "))
    },
  }

  var cmdEcho = &cobra.Command{
    Use:   "echo [string to echo]",
    Short: "Echo anything to the screen",
    Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Print: " + strings.Join(args, " "))
    },
  }

  var cmdTimes = &cobra.Command{
    Use:   "times [# times] [string to echo]",
    Short: "Echo anything to the screen more times",
    Long: `echo things multiple times back to the user by providing
a count and a string.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      for i := 0; i < echoTimes; i++ {
        fmt.Println("Echo: " + strings.Join(args, " "))
      }
    },
  }

  // 这里为 cmdTimes 对应命令设置了一个 Flag 参数
  // 类型为 Int,输入方式为 `--times` 或者 `-t`,默认值时 1,绑定到最开始声明的 `echoTimes` 上.
  cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")

  // rootCmd 后面 Add 了 cmdPrint, cmdEcho
  // 也就是说初始的两个命令是 `print` 和 `echo`
  rootCmd.AddCommand(cmdPrint, cmdEcho)
  // cmdEcho 后面 Add 了 cmdTimes
  // 所以 `echo` 后面还有一个命令时 `times`
  cmdEcho.AddCommand(cmdTimes)
  rootCmd.Execute()
}

实际生产环境中,我们可以每个命令的相关代码单独放在一个 .go 文件中,这样看起来会比较清晰一些.像这样

├── cmd
   ├── auth.go
   ├── http.go
   ├── root.go
   ├── search.go
   ├── utils.go
   └── version.go
├── main.go

API

API 可以用著名的 beego 框架来搞. beego 的文档 非常详细,就不再赘述了. 基于 beego ,我们提供以下 API,把命令行支持的功能都搬过来.

GET /api/v1/ldap/health
ldap 健康状态监测.请求的时候就去尝试连接一下 ldap,用 bind 账号 bind 测试下.成功的话就返回 ok,否则给个错. 

GET /api/v1/ldap/search/filter/:filter
根据 ldap filter 来做查询

GET /api/v1/ldap/search/user/:username
根据用户名来查询

POST /api/v1/ldap/search/multi
根据用户名同时查询多个用户,以 application/json 方式发送请求数据,例:
["user1","user2","user3"]

POST /api/v1/ldap/auth/single
单个用户的认证测试,以 application/json 方式发送请求数据,例:
{
    "username": "user",
    "password": "123456"
}

POST /api/v1/ldap/auth/multi
单个用户的认证测试,以 application/json 方式发送请求数据,例:
[{
    "username": "user1",
    "password": "123456"
}, {
    "username": "user2",
    "password": "654321"
}]

轮子

那么这个轮子已经造好了.ldao-test-tool 代码结构

# tree
.
├── cfg.json.example
├── cmd
   ├── auth.go
   ├── http.go
   ├── root.go
   ├── search.go
   ├── utils.go
   └── version.go
├── g
   ├── cfg.go
   └── const.go
├── http
   ├── controllers
      ├── authMulti.go
      ├── authSingle.go
      ├── default.go
      ├── health.go
      ├── searchFilter.go
      ├── searchMulti.go
      └── searchUser.go
   ├── http.go
   └── router.go
├── LICENSE
├── main.go
├── models
   ├── funcs.go
   ├── ldap.go
   └── ldap_test.go
└── README.MD

编译

go get ./...
go build

Release

可以直接下载编译好的 release 版本 提供 win64 和 linux64 两个平台的可执行文件 https://github.com/shanghai-edu/ldap-test-tool/releases/ 配置文件 默认配置文件为目录下的 cfg.json,也可以使用 -c 或 –config 来加载自定义的配置文件.

openldap 配置示例

{
    "ldap": {
        "addr": "ldap.example.org:389",
        "baseDn": "dc=example,dc=org",
        "bindDn": "cn=manager,dc=example,dc=org",
        "bindPass": "password",
        "authFilter": "(&(uid=%s))",
        "attributes": ["uid", "cn", "mail"],
        "tls":        false,
        "startTLS":   false
    },
    "http": {
        "listen": "0.0.0.0:8888"
    }
}

AD 配置示例

{
    "ldap": {
        "addr": "ad.example.org:389",
        "baseDn": "dc=example,dc=org",
        "bindDn": "manager@example.org",
        "bindPass": "password",
        "authFilter": "(&(sAMAccountName=%s))",
        "attributes": ["sAMAccountName", "displayName", "mail"],
        "tls":        false,
        "startTLS":   false
    },
    "http": {
        "listen": "0.0.0.0:8888"
    }
}

命令体系

命令行部分使用 cobra 框架,可以使用 help 命令查看命令的使用方式

# ./ldap-test-tool help
ldap-test-tool is a simple tool for ldap test
build by shanghai-edu.
Complete documentation is available at github.com/shanghai-edu/ldap-test-tool

Usage:
  ldap-test-tool [flags]
  ldap-test-tool [command]

Available Commands:
  auth        Auth Test
  help        Help about any command
  http        Enable a http server for ldap-test-tool
  search      Search Test
  version     Print the version number of ldap-test-tool

Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")
  -h, --help            help for ldap-test-tool

Use "ldap-test-tool [command] --help" for more information about a command.

认证
./ldap-test-tool auth -h
Auth Test

Usage:
  ldap-test-tool auth [flags]
  ldap-test-tool auth [command]

Available Commands:
  multi       Multi Auth Test
  single      Single Auth Test

Flags:
  -h, --help   help for auth

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

Use "ldap-test-tool auth [command] --help" for more information about a command.
目录