LDAP简介

LDAP是轻量目录访问协议,英文全称是LIGHTWEIGHT DIRECTORY ACCESS PROTOCOL,一般都简称为LDAP。

LDAP是一个数据库,但是又不是一个数据库。说他是数据库,因为他是一个数据存储的东西。但是说他不是数据库,是因为他的作用没有数据库这么强大,而是一个目录

从概念上说,LDAP分成了DN, OU等。OU就是一个树,DN就可以理解为是叶子,叶子还可以有更小的叶子。但是LDAP最大的分层按照IBM的文档是4层

LDAP的目的不是为了写,主要是为了查找,但并不是说LDAP不能写,只是说强项不是写。

LDAP作为一个统一认证的解决方案,主要的优点就在能够快速响应用户的查找需求。比如用户的认证,这可能会有大量的并发。如果用数据库来实现,由于数据库结构分成了各个表,要满足认证这个非常简单的需求,每次都需要去搜索数据库,合成过滤,效率慢也没有好处。虽然可以有Cache,但是还是有点浪费。LDAP就是一张表,只需要用户名和口令,加上一些其他的东西,非常简单。从效率和结构上都可以满足认证的需求。这就是为什么LDAP成为现在很人们的统一认证的解决方案的优势所在。

LDAP的优势

  • 读写效率高:LDAP也是对读操作进行优化的一种数据库,在读写比例大于7比1的情况下,LDAP会体现出极高的性能。这个特性正适合了身份认证的需要
  • 开放的标准协议:不同于SQL数据库,LDAP的客户端是跨平台的,并且对几乎所有的程序语言都有标准的API接口。即使是改变了LDAP数据库产品的提供厂商,开发人员也不用担心需要修改程序才能适应新的数据库产品。这个优势是使用SQL语言进行查询的关系型数据库难以达到的。
  • 强认证方式 :可以达到很高的安全级别。在国际化方面,LDAP使用了UTF-8编码来存储各种语言的字符。
  • OpenLDAP开源实现:OpenLDAP还包含了很多有创造性的新功能,能满足大多数使用者的要求。笔者曾使用过许多商用LDAP产品,OpenLDAP是其中最轻便且消耗系统资源最少的一个。OpenLDAP是开源软件,近年国内很多公司开发的LDAP产品都是基于OpenLDAP开发的。
  • 灵活添加数据类型:LDAP是根据schema的内容定义各种属性之间的从属关系及匹配模式的。例如在关系型数据库中如果要为用户增加一个属性,就要在用户表中增加一个字段,在拥有庞大数量用户的情况下是十分困难的,需要改变表结构。但LDAP只需要在schema中加入新的属性,不会由于用户的属性增多而影响查询性能
  • 数据存储是树结构:整棵树的任何一个分支都可以单独放在一个服务器中进行分布式管理,不仅有利于做服务器的负载均衡,还方便了跨地域的服务器部署。这个优势在查询负载大或企业在不同地域都设有分公司的时候体现尤为明显

LDAP的特点

  1. LDAP 是一种网络协议而不是数据库,而且LDAP的目录不是关系型的,没有RDBMS那么复杂
  2. LDAP不支持数据库的Transaction机制,纯粹的无状态、请求-响应的工作模式。
  3. LDAP不能存储BLOB,LDAP的读写操作是非对称的,读非常方便,写比较麻烦,
  4. LDAP支持复杂的查询过滤器(filter),可以完成很多类似数据库的查询功能。
  5. LDAP使用树状结构,接近于公司组织结构、文件目录结构、域名结构等我们耳熟能详的东
  6. LDAP使用简单、接口标准,并支持SSL访问。

LDAP的主要应用场景

  1. 网络服务:DNS服务
  2. 统一认证服务:
  3. Linux PAM (ssh, login, cvs. . . )
  4. Apache访问控制
  5. 各种服务登录(ftpd, php based, perl based, python based. . . )
  6. 个人信息类,如地址簿
  7. 服务器信息,如帐号管理、邮件服务等

LDAP使用

几个常见的属性

| 属性 | 描述 | | --------------: | ------------------------------------------------------------ | | dn | 唯一标识名类似于linux文件系统中的绝对路径,每个对象都有唯一标识名:uid=dpgdy,ou=people,dc=gdy,dc=com | | rdn | 通常指相对标识名,类似于linux系统中的相对路径,例如uid=dpgdy | | uid | 通常指一个用户的登录名称,例如uid=dpgdy,与系统中的uid不是一个概念 | | sn | 通常指一个人的姓氏,例如:sn:Guo | | giceName | 通常指一个人的名字,例如,giveName:Guodayyong,但是不能指姓氏 | | I | 通常指一个地方的地名,例如 I:shanghai | | objectClass | 特殊属性,包括数据存储的方式及相关属性信息 | | dc | 通常指一个域名:例如dc=example、dc=com | | ou | 通常指一个组织单元的名称。 例如ou=people,dcexample,dc=com | | cn | 通常指一个对象的名称,如果是人,则是全名 | | mail | 通常指登录账号的邮箱地址,例如 mail:dayong@126.com | | telephoneNumber | 通常指登录账号的手机号码,例如 telephoneNumber:XXXXXXXX | | c | 通常指一个而为国家的名称,比如CN,US等国家代号,比如c:CN |

LDIF文件及格式语法

LDIF为轻量级目录访问协议数据交换格式,是存储LDAP配置信息及目录内容的标准文本文件格式。

LDIF文件存取OpenLDAP条目标准格式:

# 注释,用于对条目进行解释  
dn:条目名称  
objectClass(对象类): 属性值  
objectClass(对象类): 属性值  
……

LDIF格式范例:

dn: uid=Guodayong,ou=people,dc=gdy,dc=com  //DN描述项,在整个目录树上为***的  
objectClass: top  
objectClass: posixAccount  
objectClass: shadowAccount  
objectClass: person  
objectClass: inetOrgPerson  
objectClass: hostObject  
sn: wang  
cn: wangxiaomei  
telephoneNumber:157****8900  
mail: wangxiaomei@126.com

go语言中使用http://gopkg.in/ldap.v3连接ldap服务器

准备工作

导入包

go get gopkg.in/ldap.v3

获取Ldap服务器的主机名(或者IP)和端口号

LdapPort = "636"                    //对应代码中的config.Ldap().Port
LdapHost = "localhost"  //对应config.Ldap().Host

获取Ldap目录结构

需要获取到ldap服务器的目录结构。记录下连接ldap数据库所用的用户DN和密码


//基础DN,要用到的基础子树层级关系。
LdapBaseDn       = "ou=staff,dc=wang,dc=com"   //对应config.Ldap().BaseDn

获取Ldap数据结构(键值关系)

项目中需要的人员信息来源于staff子树的部门子树下。

在项目中需要获取的ldap数据字段包括:uid(用户id) 、deportment(部门),displayName(姓名)、mail(邮箱)

LdapUsernameKey   = "uid"         //对应config.Ldap().Attributes.UNameKey
LdapNameKey       = "displayName" //对应config.Ldap().Attributes.NameKey
LdapEmailKey      = "mail"        //对应config.Ldap().Attributes.EmailKey
LdapDepartmentKey = "department"  //对应config.Ldap().Attributes.DepartmentKey

在项目中进行连接

在项目中单独使用一个函数来验证用户名和密码,验证成功则返回一个符合本项目数据库要求的user实体,如中途出现err或者没有拿到数据等都会返回err或者空数据。

//创建与ldap服务器的链接
conn, err := ldap.DialTLS("tcp", config.Ldap().Host+":"+config.Ldap().Port, &tls.Config{InsecureSkipVerify: true})
//设置超时时间
conn.SetTimeout(5 * time.Second)
defer conn.Close()

查询需要用到的数据

//这里的username为用户点击ldap登录时输入的用户名。
filter = fmt.Sprintf("(%s=%s)", config.Ldap().Attributes.UNameKey, username)
attributes = []string{config.Ldap().Attributes.UNameKey, config.Ldap().Attributes.DepartmentKey, config.Ldap().Attributes.EmailKey, config.Ldap().Attributes.NameKey,}
sql := ldap.NewSearchRequest(
    //config.Ldap().LoginDn: ou=staff,dc=ebupt,dc=com
   config.Ldap().LoginDn,   
    //scope:  查询的范围 
   ldap.ScopeWholeSubtree,
    //DerefAiases: 在搜索中别名(cn, ou)是否废弃
   ldap.NeverDerefAliases, 
    //SizeLimit: 大小设置,一般设置为0
   0,           
    //TimeLimit: 时间设置,一般设置为0
   0,    
    //TypesOnly:  设置false(返回的值要多一点)
   false,    
    //Filter 是过滤条件
   filter, 
    //Attributes 需要返回的属性值
   attributes,     
    //Controls:  控制
   nil)

解析查询结果

entry := cur.Entries[0]
user = datamodels.User{
   Id:         utils.GetMillisecond(),
   Password:   config.DefaultPwd,
   RoleName:   config.OrdinaryRole,
   RoleId:     config.OrdinaryRoleId,
   LastLogin:  utils.JsonTime(time.Now()),
   HaveLdap:   config.One,
   Name:       entry.GetAttributeValue(config.Ldap().Attributes.NameKey),
   Username:   entry.GetAttributeValue(config.Ldap().Attributes.UNameKey),
   Email:      entry.GetAttributeValue(config.Ldap().Attributes.EmailKey),
   Department: entry.GetAttributeValue(config.Ldap().Attributes.DepartmentKey),
}

进行用户名密码校验

//这里的password为用户点击ldap登录时输入的用户名。
err = conn.Bind(entry.DN, password)

链接Ldap进行用户校验的完整代码

func (ldapService *ldapService) Login(username string, password string) (*datamodels.User, error) {
   var (
      filter     string
      attributes []string
      conn       *ldap.Conn
      err        error
      cur        *ldap.SearchResult
      user       datamodels.User
   )

   filter = fmt.Sprintf("(%s=%s)", config.Ldap().Attributes.UNameKey, username)
   attributes = []string{config.Ldap().Attributes.UNameKey, config.Ldap().Attributes.DepartmentKey, config.Ldap().Attributes.EmailKey, config.Ldap().Attributes.NameKey,}
   if config.Ldap().Port == "636" {
      conn, err = ldap.DialTLS("tcp", config.Ldap().Host+":"+config.Ldap().Port, &tls.Config{InsecureSkipVerify: true})
   } else {
      conn, err = ldap.Dial("tcp", config.Ldap().Host+":"+config.Ldap().Port)
   }

   if err != nil {
      return nil, err
   }
   conn.SetTimeout(5 * time.Second)
   defer conn.Close()
   sql := ldap.NewSearchRequest(
      config.Ldap().BaseDn,   
      ldap.ScopeWholeSubtree, 
      ldap.NeverDerefAliases, 
      0,                      
      0,                      
      false,                  
      filter,                 
      attributes,             
      nil) 
   if cur, err = conn.Search(sql); err != nil {
      return nil, err
   }
   if len(cur.Entries) == 0 {
      return nil, nil
   }
   entry := cur.Entries[0]
   user = datamodels.User{
      Id:         utils.GetMillisecond(),
      Password:   config.DefaultPwd,
      RoleName:   config.OrdinaryRole,
      RoleId:     config.OrdinaryRoleId,
      LastLogin:  utils.JsonTime(time.Now()),
      HaveLdap:   config.One,
      Name:       entry.GetAttributeValue(config.Ldap().Attributes.NameKey),
      Username:   entry.GetAttributeValue(config.Ldap().Attributes.UNameKey),
      Email:      entry.GetAttributeValue(config.Ldap().Attributes.EmailKey),
      Department: entry.GetAttributeValue(config.Ldap().Attributes.DepartmentKey),
   }
   err = conn.Bind(entry.DN, password)
   if err != nil {
      fmt.Print(err)
      return nil, err
   }

   return &user, nil
}

通用性设置和注意事项

  • 连接ldap服务时由于使用的是636端口需要进行tls跳过的配置,如果使用389端口则使用Dial()函数,Dial()无tls配置
  • 本项目与Ldap使用了匿名绑定,如果更换其他Ldap服务器需要在ldap服务器中设置允许匿名绑定。否则连接后无法查询数据
  • 代码中将需要的字段都统一抽出来在系统运行时使用初始化函数进行初始化,初始化时从环境变量中读取对应值,方便在更换Ldap数据库时不需要进行代码的更改。本文中示例的相关数据为项目代码中的默认数据。
  • 只需要在代码中加入下面的 环境变量对应关系,在项目运行时设置环境变量就可以完成连接不同ldap服务器的重用:
/*
ldap配置
*/
type ldapConfig struct {
    Host       string         //ip或者主机名
    Port       string         //端口
    BaseDn     string         //基础DN
    Attributes ldapAttributes //结果集
}
type ldapAttributes struct {
    UNameKey      string //ldap中用户名的key
    NameKey       string //ldap中姓名的key
    EmailKey      string //ldap中email的key
    DepartmentKey string //ldap中部门的key
}
//从ldap环境变量初始化数据
LdapCon = ldapConfig{
    Host:     os.Getenv("LDAP_HOST"),
    Port:     os.Getenv("LDAP_PORT"),
    BaseDn:   os.Getenv("LDAP_BASE_DN"),
    Attributes: ldapAttributes{
        UNameKey:      os.Getenv("LDAP_USERNAME_KEY"),
        NameKey:       os.Getenv("LDAP_NAME_KEY"),
        EmailKey:      os.Getenv("LDAP_EMAIL_KEY"),
        DepartmentKey: os.Getenv("LDAP_DEPARTMENT_KEY"),
    },
}
func Ldap() *ldapConfig {
    return &LdapCon
}