GO对接ldap认证
基本流程
- 连接到LDAP服务器并绑定到LDAP服务器;(一般以管理员用户绑定,权限更大)
- 在LDAP服务器上执行所需的任何操作;
- 释放LDAP服务器的连接;
安装库
// 安装go操作ldap库
go get "github.com/go-ldap/ldap/v3"
准备工作
准备配置结构体
package ldap_1
import (
"crypto/sha1"
"crypto/tls"
"encoding/base64"
"fmt"
"github.com/go-ldap/ldap/v3"
)
// ldap:未加密协议
// ldaps:加密协议
// 定义LDAP服务器的URL,这里使用未加密的LDAP协议,指定了服务器的IP地址和端口号
var ldapURL = "ldap://10.1.0.153:389"
// 用于生成SSHA(Salted SHA-1)哈希的盐值,盐值可以增加密码哈希的安全性
var salt = []byte("123456")
// LdapConfig 定义LDAP配置结构,用于存储连接和操作LDAP服务器所需的各种信息
type LdapConfig struct {
Addr string // LDAP服务器地址,即上面定义的ldapURL
BindUserDn string // 绑定用户的DN(Distinguished Name),用于认证管理员账户
BindUserPassword string // 绑定用户的密码,用于认证管理员账户
BaseDn string // 基础DN,是LDAP目录树的根节点,后续的搜索和操作都基于此
LoginName string // 登录名属性(如uid),用于标识用户的唯一名称
ObjectClass []string // 对象类(如inetOrgPerson),用于指定LDAP条目的类型
}
// NewLdapConfig 初始化LDAP配置,返回一个指向LdapConfig结构体的指针
func NewLdapConfig() *LdapConfig {
return &LdapConfig{
Addr: ldapURL, // 使用上面定义的LDAP服务器地址
BaseDn: "dc=hexug,dc=com", // 基础DN
BindUserDn: "cn=admin,dc=hexug,dc=com", // 绑定用户的DN
BindUserPassword: "111111", // 绑定用户的密码
LoginName: "uid", // 登录名属性
ObjectClass: []string{"inetOrgPerson"}, // 对象类
}
}
操作
登录
// LoginBind 登录绑定LDAP管理员账户,返回一个LDAP连接对象和可能的错误信息
func LoginBind(config *LdapConfig) (*ldap.Conn, error) {
// 使用DialURL函数连接到LDAP服务器,同时配置TLS以跳过证书验证(不建议在生产环境中使用)
l, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}))
if err != nil {
// 如果连接失败,触发恐慌并返回错误
panic(err)
return nil, err
}
// 使用SimpleBind方法进行简单绑定,提供绑定用户的DN和密码
_, err = l.SimpleBind(
&ldap.SimpleBindRequest{
Username: config.BindUserDn,
Password: config.BindUserPassword,
},
)
if err != nil {
// 如果绑定失败,打印错误信息并返回错误
fmt.Println("ldap password is error: ", ldap.LDAPResultInvalidCredentials)
return nil, err
}
// 绑定成功,返回LDAP连接对象
return l, nil
}
单元测试
package ldap_1_test
import (
"fmt"
"gitee.com/hexug/go-tools/format"
"go_auth_bridge/ldap_1"
"testing"
)
// TestLoginBind 测试LDAP管理员账户登录绑定功能
func TestLoginBind(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 如果绑定成功,使用t.Log输出成功信息
t.Log("ok")
}
}
增加用户
// GenerateSSHAHash 生成SSHA哈希密码,根据输入的密码和预定义的盐值生成SSHA哈希密码
func GenerateSSHAHash(password string) (string, error) {
// 创建SHA-1哈希对象
hash := sha1.New()
// 写入密码
hash.Write([]byte(password))
// 写入盐值
hash.Write(salt)
// 计算哈希值
hashSum := hash.Sum(nil)
// 将哈希值和盐值拼接
hashWithSalt := append(hashSum, salt...)
// 对拼接后的结果进行Base64编码,并添加{SSHA}前缀
return "{SSHA}" + base64.StdEncoding.EncodeToString(hashWithSalt), nil
}
// AddUser 添加用户到LDAP目录,根据用户属性添加新用户到LDAP目录,返回添加结果和可能的错误信息
func AddUser(conn *ldap.Conn, config *LdapConfig, userAttributes map[string]string) (bool, error) {
// 验证必需字段,确保用户属性中包含必需的字段
requiredFields := []string{"uid", "cn", "sn", "userPassword"}
for _, field := range requiredFields {
if _, exists := userAttributes[field]; !exists {
// 如果缺少必需字段,返回错误信息
return false, fmt.Errorf("缺少必需字段: %s", field)
}
}
// 构造用户DN
parentDN := "ou=users," + config.BaseDn
userDN := fmt.Sprintf("cn=%s,%s", userAttributes["cn"], parentDN)
// 准备用户属性条目
addRequest := ldap.NewAddRequest(userDN, nil)
// 添加对象类,包括top类
objectClasses := append(config.ObjectClass, "top")
addRequest.Attribute("objectClass", objectClasses)
addRequest.Attribute("uid", []string{userAttributes["uid"]})
addRequest.Attribute("cn", []string{userAttributes["cn"]})
addRequest.Attribute("sn", []string{userAttributes["sn"]})
// 生成SSHA哈希密码
hashedPassword, err := GenerateSSHAHash(userAttributes["userPassword"])
if err != nil {
// 如果密码哈希生成失败,返回错误信息
return false, fmt.Errorf("密码哈希生成失败: %v", err)
}
addRequest.Attribute("userPassword", []string{hashedPassword})
// 添加可选属性
if mail, ok := userAttributes["mail"]; ok {
addRequest.Attribute("mail", []string{mail})
}
if telephone, ok := userAttributes["description"]; ok {
addRequest.Attribute("description", []string{telephone})
}
// 执行添加操作
if err := conn.Add(addRequest); err != nil {
if ldapErr, ok := err.(*ldap.Error); ok {
switch ldapErr.ResultCode {
case ldap.LDAPResultEntryAlreadyExists:
// 如果用户已存在,返回错误信息
return false, fmt.Errorf("用户已存在: %s", userDN)
case ldap.LDAPResultInsufficientAccessRights:
// 如果权限不足,返回错误信息
return false, fmt.Errorf("权限不足,请检查管理员ACL")
}
}
// 如果添加失败,返回错误信息
return false, fmt.Errorf("添加用户失败: %v", err)
}
// 添加成功,返回添加结果和无错误信息
return true, nil
}
这里使用了 SSHA 对密码进行加密处理,增加安全性
单元测试
// TestAddUser 测试向LDAP目录添加用户的功能
func TestAddUser(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 构造要添加的用户属性
user := map[string]string{
"uid": "john.doe4",
"cn": "John Doe4",
"sn": "Doe4", // 姓氏必填
"userPassword": "111111",
"mail": "john@hexug.com",
"description": "+86-13800138000",
}
// 调用AddUser函数将用户添加到LDAP目录,获取添加结果和可能的错误信息
success, err := ldap_1.AddUser(conn, conf, user)
if err != nil {
// 如果添加失败,打印错误信息并返回
fmt.Printf("添加失败: %v\n", err)
return
}
if success {
// 如果添加成功,打印添加成功信息
fmt.Println("用户添加成功")
}
}
}
校验密码
// VerifyUserCredentials 验证用户凭据,根据用户名和密码验证用户是否合法,返回验证结果和可能的错误信息
func VerifyUserCredentials(conn *ldap.Conn, config *LdapConfig, username, password string) (bool, error) {
// 搜索用户DN,根据登录名属性和用户名构造过滤条件
searchRequest := ldap.NewSearchRequest(
config.BaseDn,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, 0, false,
fmt.Sprintf("(%s=%s)", config.LoginName, username),
[]string{"dn"},
nil,
)
// 执行搜索请求
searchResult, err := conn.Search(searchRequest)
if err != nil {
// 如果搜索失败,返回错误信息
return false, fmt.Errorf("用户搜索失败: %v", err)
}
if len(searchResult.Entries) == 0 {
// 如果没有找到用户,返回错误信息
return false, fmt.Errorf("用户不存在")
}
if len(searchResult.Entries) > 1 {
// 如果找到多个同名用户,返回错误信息
return false, fmt.Errorf("找到多个同名用户")
}
// 获取用户DN
userDN := searchResult.Entries[0].DN
// 创建新连接验证用户密码
userConn, err := ldap.DialURL(config.Addr, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}))
if err != nil {
// 如果新连接失败,返回错误信息
return false, fmt.Errorf("用户验证连接失败: %v", err)
}
// 确保在函数结束时关闭连接
defer userConn.Close()
// 尝试绑定用户DN和密码
if err := userConn.Bind(userDN, password); err != nil {
if ldapErr, ok := err.(*ldap.Error); ok && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials {
// 如果密码错误,返回验证失败但无错误信息
return false, nil
}
// 如果绑定失败,返回错误信息
return false, fmt.Errorf("绑定验证失败: %v", err)
}
// 验证成功,返回验证结果和无错误信息
return true, nil
}
提供了专门的校验密码的方法 Bind
单元测试
// TestVerifyUserCredentials 测试验证用户凭据的功能
func TestVerifyUserCredentials(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 调用VerifyUserCredentials函数验证用户"hxg"的密码是否为"111111",获取验证结果和可能的错误信息
isValid, err := ldap_1.VerifyUserCredentials(conn, conf, "hxg", "111111")
if err != nil {
// 如果验证过程中出现错误,使用t.Fatal输出错误信息并终止测试
t.Fatal(err)
}
if isValid {
// 如果验证成功,使用t.Log输出登录成功信息
t.Log("登录成功")
} else {
// 如果验证失败,使用t.Error输出登录失败信息
t.Error("登录失败")
}
}
}
查询用户
// FindUser 查询用户信息,根据用户名在LDAP目录中搜索用户,返回搜索结果和可能的错误信息
func FindUser(conn *ldap.Conn, config *LdapConfig, username string) (*ldap.SearchResult, error) {
// 构造过滤条件,使用用户名进行精确匹配
filter := fmt.Sprintf("(cn=%s)", ldap.EscapeFilter(username))
// 创建搜索请求,指定搜索的基础DN、搜索范围、别名处理方式、过滤条件和要返回的属性
request := ldap.NewSearchRequest(
config.BaseDn,
ldap.ScopeWholeSubtree, // 搜索整个子树
ldap.NeverDerefAliases, // 不解析别名
0, 0, false,
filter,
// 需要查询的属性
[]string{"cn", "uid", "userPassword", "mail", "description", "sn", "gidNumber", "homeDirectory", "objectClass", "uidNumber", "dn"},
nil,
)
// 执行搜索请求
searchResult, err := conn.Search(request)
if err != nil {
// 如果搜索失败,打印错误信息并返回错误
fmt.Println("search user error: ", err)
return nil, err
}
// 搜索成功,返回搜索结果
return searchResult, nil
}
[]string{"cn", "uid", "userPassword", "mail", "description", "sn", "gidNumber", "homeDirectory", "objectClass", "uidNumber", "dn"},
一般查询属性的字段不会这么多,这里主要是为了展示使用
单元测试
// TestFindUser 测试根据用户名查找用户信息的功能
func TestFindUser(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 调用FindUser函数查找名为"hxg"的用户信息,获取搜索结果和可能的错误信息
res, err := ldap_1.FindUser(conn, conf, "hxg")
if err != nil {
// 如果查找失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 如果查找成功,将搜索结果格式化为JSON并打印
fmt.Println(format.FormatToJson(res))
}
}
}
修改
这里以修改密码来测试
// ModifyUserPasswordByAdmin 管理员修改用户密码,根据用户名和新密码修改用户的密码,返回修改结果和可能的错误信息
func ModifyUserPasswordByAdmin(conn *ldap.Conn, config *LdapConfig, cn, newPassword string) (bool, error) {
// 生成哈希密码
hashedPassword, err := GenerateSSHAHash(newPassword)
if err != nil {
// 如果生成哈希密码失败,返回错误信息
return false, fmt.Errorf("生成哈希密码失败: %v", err)
}
// 构造用户DN
parentDN := "ou=users," + config.BaseDn
userDN := fmt.Sprintf("cn=%s,%s", cn, parentDN)
// 构造修改请求
modifyRequest := ldap.NewModifyRequest(userDN, nil)
modifyRequest.Replace("userPassword", []string{hashedPassword})
// 执行修改操作
if err := conn.Modify(modifyRequest); err != nil {
if ldapErr, ok := err.(*ldap.Error); ok {
switch ldapErr.ResultCode {
case ldap.LDAPResultNoSuchObject:
// 如果用户不存在,返回错误信息
return false, fmt.Errorf("用户不存在: %s", userDN)
case ldap.LDAPResultInsufficientAccessRights:
// 如果权限不足,返回错误信息
return false, fmt.Errorf("权限不足,请检查管理员ACL")
}
}
// 如果修改失败,返回错误信息
return false, fmt.Errorf("修改密码失败: %v", err)
}
// 修改成功,返回修改结果和无错误信息
return true, nil
}
修改其他的属性值,可以通过 modifyRequest.Replace("userPassword", []string{hashedPassword}) 这种来设置
单元测试
// TestModifyUserPasswordByAdmin 测试管理员修改用户密码的功能
func TestModifyUserPasswordByAdmin(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 定义新密码
newPassword := "222222"
// 调用ModifyUserPasswordByAdmin函数修改用户"john.doe4"的密码,获取修改结果和可能的错误信息
success, err := ldap_1.ModifyUserPasswordByAdmin(conn, conf, "John Doe4", newPassword)
if err != nil {
// 如果修改失败,打印错误信息
fmt.Printf("修改密码失败: %v\n", err)
} else {
// 如果修改成功,打印修改结果和成功信息
fmt.Println(success)
fmt.Println("修改密码成功")
}
}
}
删除用户
需要先查,然后再删除
// DeleteUser 删除用户,根据用户名删除LDAP目录中的用户,返回删除结果和可能的错误信息
func DeleteUser(conn *ldap.Conn, config *LdapConfig, cn string) (bool, error) {
// 构造用户DN
parentDN := "ou=users," + config.BaseDn
userDN := fmt.Sprintf("cn=%s,%s", cn, parentDN)
// 检查用户是否存在
searchRequest := ldap.NewSearchRequest(
userDN,
ldap.ScopeBaseObject,
ldap.NeverDerefAliases,
0, 0, false,
"(objectClass=*)",
[]string{"dn"},
nil,
)
_, err := conn.Search(searchRequest)
if err != nil {
if ldapErr, ok := err.(*ldap.Error); ok && ldapErr.ResultCode == ldap.LDAPResultNoSuchObject {
// 如果用户不存在,返回错误信息
return false, fmt.Errorf("用户不存在: %s", userDN)
}
// 如果搜索用户失败,返回错误信息
return false, fmt.Errorf("搜索用户失败: %v", err)
}
// 检查用户是否包含子条目
subSearchRequest := ldap.NewSearchRequest(
userDN,
ldap.ScopeSingleLevel,
ldap.NeverDerefAliases,
0, 0, false,
"(objectClass=*)",
[]string{"dn"},
nil,
)
subSearchResult, err := conn.Search(subSearchRequest)
if err != nil {
// 如果搜索子条目失败,返回错误信息
return false, fmt.Errorf("搜索子条目失败: %v", err)
}
if len(subSearchResult.Entries) > 0 {
// 如果用户条目包含子条目,返回错误信息
return false, fmt.Errorf("用户条目包含子条目,无法直接删除")
}
// 构造删除请求
delRequest := ldap.NewDelRequest(userDN, nil)
if err := conn.Del(delRequest); err != nil {
if ldapErr, ok := err.(*ldap.Error); ok {
switch ldapErr.ResultCode {
case ldap.LDAPResultNoSuchObject:
// 如果用户不存在,返回错误信息
return false, fmt.Errorf("用户不存在: %s", userDN)
case ldap.LDAPResultInsufficientAccessRights:
// 如果权限不足,返回错误信息
return false, fmt.Errorf("权限不足,请检查管理员ACL")
case ldap.LDAPResultNotAllowedOnNonLeaf:
// 如果用户条目包含子条目,返回错误信息
return false, fmt.Errorf("用户条目包含子条目,无法直接删除")
}
}
// 如果删除失败,返回错误信息
return false, fmt.Errorf("删除用户失败: %v", err)
}
// 删除成功,返回删除结果和无错误信息
return true, nil
}
单元测试
// TestDeleteUser 测试删除LDAP目录中用户的功能
func TestDeleteUser(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 调用DeleteUser函数删除用户"hexug1",获取删除结果和可能的错误信息
success, err := ldap_1.DeleteUser(conn, conf, "John Doe4")
if err != nil {
// 如果删除失败,打印错误信息
fmt.Printf("删除用户失败: %v\n", err)
} else {
// 如果删除成功,打印删除结果和成功信息
fmt.Println(success)
fmt.Println("删除用户成功")
}
}
}
获取分组中的用户列表
// GetListUsersInOU 获取指定OU下的用户列表,根据OU名称获取该OU下的所有用户信息,返回用户列表和可能的错误信息
func GetListUsersInOU(conn *ldap.Conn, config *LdapConfig, ou string) ([]map[string]string, error) {
// 构造搜索基础DN
searchBase := fmt.Sprintf("ou=%s,%s", ou, config.BaseDn)
// 构造搜索过滤条件,只搜索inetOrgPerson类型的对象
searchFilter := "(objectClass=inetOrgPerson)"
// 创建搜索请求
searchRequest := ldap.NewSearchRequest(
searchBase,
ldap.ScopeSingleLevel,
ldap.NeverDerefAliases,
0, 0, false,
searchFilter,
[]string{"cn", "uid", "userPassword", "mail", "description", "sn", "objectClass", "dn"},
nil,
)
// 执行搜索请求
searchResult, err := conn.Search(searchRequest)
if err != nil {
// 如果搜索失败,返回错误信息
return nil, fmt.Errorf("搜索用户失败: %v", err)
}
// 初始化用户列表
users := make([]map[string]string, 0)
for _, entry := range searchResult.Entries {
// 初始化用户信息
user := make(map[string]string)
user["dn"] = entry.DN
user["uid"] = entry.GetAttributeValue("uid")
user["userPassword"] = entry.GetAttributeValue("userPassword")
user["cn"] = entry.GetAttributeValue("cn")
user["sn"] = entry.GetAttributeValue("sn")
user["mail"] = entry.GetAttributeValue("mail")
user["description"] = entry.GetAttributeValue("description")
user["objectClass"] = entry.GetAttributeValue("objectClass")
// 将用户信息添加到用户列表中
users = append(users, user)
}
// 返回用户列表和无错误信息
return users, nil
}
单元测试
// TestGetListUsersInOU 测试获取指定组织单元(OU)下用户列表的功能
func TestGetListUsersInOU(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 定义要查询的组织单元名称
ou := "users"
// 调用GetListUsersInOU函数获取指定组织单元下的用户列表,获取用户列表和可能的错误信息
users, err := ldap_1.GetListUsersInOU(conn, conf, ou)
if err != nil {
// 如果获取失败,打印错误信息并返回
fmt.Printf("获取用户失败: %v\n", err)
return
}
// 遍历用户列表并打印每个用户的信息
for _, user := range users {
fmt.Printf("用户 DN: %s\n", user["dn"])
fmt.Printf("用户 UID: %s\n", user["uid"])
fmt.Printf("用户 密码: %s\n", user["userPassword"])
fmt.Printf("用户 CN: %s\n", user["cn"])
fmt.Printf("用户 SN: %s\n", user["sn"])
fmt.Printf("用户邮箱: %s\n", user["mail"])
fmt.Printf("用户description: %s\n", user["description"])
fmt.Printf("用户objectClass: %s\n", user["objectClass"])
fmt.Println("------")
}
}
}
完整代码
package ldap_1
import (
"crypto/sha1"
"crypto/tls"
"encoding/base64"
"fmt"
"github.com/go-ldap/ldap/v3"
)
// ldap:未加密协议
// ldaps:加密协议
// 定义LDAP服务器的URL,这里使用未加密的LDAP协议,指定了服务器的IP地址和端口号
var ldapURL = "ldap://10.1.0.153:389"
// 用于生成SSHA(Salted SHA-1)哈希的盐值,盐值可以增加密码哈希的安全性
var salt = []byte("123456")
// LdapConfig 定义LDAP配置结构,用于存储连接和操作LDAP服务器所需的各种信息
type LdapConfig struct {
Addr string // LDAP服务器地址,即上面定义的ldapURL
BindUserDn string // 绑定用户的DN(Distinguished Name),用于认证管理员账户
BindUserPassword string // 绑定用户的密码,用于认证管理员账户
BaseDn string // 基础DN,是LDAP目录树的根节点,后续的搜索和操作都基于此
LoginName string // 登录名属性(如uid),用于标识用户的唯一名称
ObjectClass []string // 对象类(如inetOrgPerson),用于指定LDAP条目的类型
}
// NewLdapConfig 初始化LDAP配置,返回一个指向LdapConfig结构体的指针
func NewLdapConfig() *LdapConfig {
return &LdapConfig{
Addr: ldapURL, // 使用上面定义的LDAP服务器地址
BaseDn: "dc=hexug,dc=com", // 基础DN
BindUserDn: "cn=admin,dc=hexug,dc=com", // 绑定用户的DN
BindUserPassword: "111111", // 绑定用户的密码
LoginName: "uid", // 登录名属性
ObjectClass: []string{"inetOrgPerson"}, // 对象类
}
}
// LoginBind 绑定LDAP管理员账户,返回一个LDAP连接对象和可能的错误信息
func LoginBind(config *LdapConfig) (*ldap.Conn, error) {
// 使用DialURL函数连接到LDAP服务器,同时配置TLS以跳过证书验证(不建议在生产环境中使用)
l, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}))
if err != nil {
// 如果连接失败,触发恐慌并返回错误
panic(err)
return nil, err
}
// 使用SimpleBind方法进行简单绑定,提供绑定用户的DN和密码
_, err = l.SimpleBind(
&ldap.SimpleBindRequest{
Username: config.BindUserDn,
Password: config.BindUserPassword,
},
)
if err != nil {
// 如果绑定失败,打印错误信息并返回错误
fmt.Println("ldap password is error: ", ldap.LDAPResultInvalidCredentials)
return nil, err
}
// 绑定成功,打印提示信息并返回LDAP连接对象
fmt.Println("bind success...")
return l, nil
}
// FindUser 查询用户信息,根据用户名在LDAP目录中搜索用户,返回搜索结果和可能的错误信息
func FindUser(conn *ldap.Conn, config *LdapConfig, username string) (*ldap.SearchResult, error) {
// 构造过滤条件,使用用户名进行精确匹配
filter := fmt.Sprintf("(cn=%s)", ldap.EscapeFilter(username))
// 创建搜索请求,指定搜索的基础DN、搜索范围、别名处理方式、过滤条件和要返回的属性
request := ldap.NewSearchRequest(
config.BaseDn,
ldap.ScopeWholeSubtree, // 搜索整个子树
ldap.NeverDerefAliases, // 不解析别名
0, 0, false,
filter,
// 需要查询的属性
[]string{"cn", "uid", "userPassword", "mail", "description", "sn", "gidNumber", "homeDirectory", "objectClass", "uidNumber", "dn"},
nil,
)
// 执行搜索请求
searchResult, err := conn.Search(request)
if err != nil {
// 如果搜索失败,打印错误信息并返回错误
fmt.Println("search user error: ", err)
return nil, err
}
// 搜索成功,返回搜索结果
return searchResult, nil
}
// VerifyUserCredentials 验证用户凭据,根据用户名和密码验证用户是否合法,返回验证结果和可能的错误信息
func VerifyUserCredentials(conn *ldap.Conn, config *LdapConfig, username, password string) (bool, error) {
// 搜索用户DN,根据登录名属性和用户名构造过滤条件
searchRequest := ldap.NewSearchRequest(
config.BaseDn,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, 0, false,
fmt.Sprintf("(%s=%s)", config.LoginName, username),
[]string{"dn"},
nil,
)
// 执行搜索请求
searchResult, err := conn.Search(searchRequest)
if err != nil {
// 如果搜索失败,返回错误信息
return false, fmt.Errorf("用户搜索失败: %v", err)
}
if len(searchResult.Entries) == 0 {
// 如果没有找到用户,返回错误信息
return false, fmt.Errorf("用户不存在")
}
if len(searchResult.Entries) > 1 {
// 如果找到多个同名用户,返回错误信息
return false, fmt.Errorf("找到多个同名用户")
}
// 获取用户DN
userDN := searchResult.Entries[0].DN
// 创建新连接验证用户密码
userConn, err := ldap.DialURL(config.Addr, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}))
if err != nil {
// 如果新连接失败,返回错误信息
return false, fmt.Errorf("用户验证连接失败: %v", err)
}
// 确保在函数结束时关闭连接
defer userConn.Close()
// 尝试绑定用户DN和密码
if err := userConn.Bind(userDN, password); err != nil {
if ldapErr, ok := err.(*ldap.Error); ok && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials {
// 如果密码错误,返回验证失败但无错误信息
return false, nil
}
// 如果绑定失败,返回错误信息
return false, fmt.Errorf("绑定验证失败: %v", err)
}
// 验证成功,返回验证结果和无错误信息
return true, nil
}
// GenerateSSHAHash 生成SSHA哈希密码,根据输入的密码和预定义的盐值生成SSHA哈希密码
func GenerateSSHAHash(password string) (string, error) {
// 创建SHA-1哈希对象
hash := sha1.New()
// 写入密码
hash.Write([]byte(password))
// 写入盐值
hash.Write(salt)
// 计算哈希值
hashSum := hash.Sum(nil)
// 将哈希值和盐值拼接
hashWithSalt := append(hashSum, salt...)
// 对拼接后的结果进行Base64编码,并添加{SSHA}前缀
return "{SSHA}" + base64.StdEncoding.EncodeToString(hashWithSalt), nil
}
// AddUser 添加用户到LDAP目录,根据用户属性添加新用户到LDAP目录,返回添加结果和可能的错误信息
func AddUser(conn *ldap.Conn, config *LdapConfig, userAttributes map[string]string) (bool, error) {
// 验证必需字段,确保用户属性中包含必需的字段
requiredFields := []string{"uid", "cn", "sn", "userPassword"}
for _, field := range requiredFields {
if _, exists := userAttributes[field]; !exists {
// 如果缺少必需字段,返回错误信息
return false, fmt.Errorf("缺少必需字段: %s", field)
}
}
// 构造用户DN
parentDN := "ou=users," + config.BaseDn
userDN := fmt.Sprintf("cn=%s,%s", userAttributes["cn"], parentDN)
// 准备用户属性条目
addRequest := ldap.NewAddRequest(userDN, nil)
// 添加对象类,包括top类
objectClasses := append(config.ObjectClass, "top")
addRequest.Attribute("objectClass", objectClasses)
addRequest.Attribute("uid", []string{userAttributes["uid"]})
addRequest.Attribute("cn", []string{userAttributes["cn"]})
addRequest.Attribute("sn", []string{userAttributes["sn"]})
// 生成SSHA哈希密码
hashedPassword, err := GenerateSSHAHash(userAttributes["userPassword"])
if err != nil {
// 如果密码哈希生成失败,返回错误信息
return false, fmt.Errorf("密码哈希生成失败: %v", err)
}
addRequest.Attribute("userPassword", []string{hashedPassword})
// 添加可选属性
if mail, ok := userAttributes["mail"]; ok {
addRequest.Attribute("mail", []string{mail})
}
if telephone, ok := userAttributes["description"]; ok {
addRequest.Attribute("description", []string{telephone})
}
// 执行添加操作
if err := conn.Add(addRequest); err != nil {
if ldapErr, ok := err.(*ldap.Error); ok {
switch ldapErr.ResultCode {
case ldap.LDAPResultEntryAlreadyExists:
// 如果用户已存在,返回错误信息
return false, fmt.Errorf("用户已存在: %s", userDN)
case ldap.LDAPResultInsufficientAccessRights:
// 如果权限不足,返回错误信息
return false, fmt.Errorf("权限不足,请检查管理员ACL")
}
}
// 如果添加失败,返回错误信息
return false, fmt.Errorf("添加用户失败: %v", err)
}
// 添加成功,返回添加结果和无错误信息
return true, nil
}
// ModifyUserPasswordByAdmin 管理员修改用户密码,根据用户名和新密码修改用户的密码,返回修改结果和可能的错误信息
func ModifyUserPasswordByAdmin(conn *ldap.Conn, config *LdapConfig, cn, newPassword string) (bool, error) {
// 生成哈希密码
hashedPassword, err := GenerateSSHAHash(newPassword)
if err != nil {
// 如果生成哈希密码失败,返回错误信息
return false, fmt.Errorf("生成哈希密码失败: %v", err)
}
// 构造用户DN
parentDN := "ou=users," + config.BaseDn
userDN := fmt.Sprintf("cn=%s,%s", cn, parentDN)
// 构造修改请求
modifyRequest := ldap.NewModifyRequest(userDN, nil)
modifyRequest.Replace("userPassword", []string{hashedPassword})
// 执行修改操作
if err := conn.Modify(modifyRequest); err != nil {
if ldapErr, ok := err.(*ldap.Error); ok {
switch ldapErr.ResultCode {
case ldap.LDAPResultNoSuchObject:
// 如果用户不存在,返回错误信息
return false, fmt.Errorf("用户不存在: %s", userDN)
case ldap.LDAPResultInsufficientAccessRights:
// 如果权限不足,返回错误信息
return false, fmt.Errorf("权限不足,请检查管理员ACL")
}
}
// 如果修改失败,返回错误信息
return false, fmt.Errorf("修改密码失败: %v", err)
}
// 修改成功,返回修改结果和无错误信息
return true, nil
}
// DeleteUser 删除用户,根据用户名删除LDAP目录中的用户,返回删除结果和可能的错误信息
func DeleteUser(conn *ldap.Conn, config *LdapConfig, cn string) (bool, error) {
// 构造用户DN
parentDN := "ou=users," + config.BaseDn
userDN := fmt.Sprintf("cn=%s,%s", cn, parentDN)
// 检查用户是否存在
searchRequest := ldap.NewSearchRequest(
userDN,
ldap.ScopeBaseObject,
ldap.NeverDerefAliases,
0, 0, false,
"(objectClass=*)",
[]string{"dn"},
nil,
)
_, err := conn.Search(searchRequest)
if err != nil {
if ldapErr, ok := err.(*ldap.Error); ok && ldapErr.ResultCode == ldap.LDAPResultNoSuchObject {
// 如果用户不存在,返回错误信息
return false, fmt.Errorf("用户不存在: %s", userDN)
}
// 如果搜索用户失败,返回错误信息
return false, fmt.Errorf("搜索用户失败: %v", err)
}
// 检查用户是否包含子条目
subSearchRequest := ldap.NewSearchRequest(
userDN,
ldap.ScopeSingleLevel,
ldap.NeverDerefAliases,
0, 0, false,
"(objectClass=*)",
[]string{"dn"},
nil,
)
subSearchResult, err := conn.Search(subSearchRequest)
if err != nil {
// 如果搜索子条目失败,返回错误信息
return false, fmt.Errorf("搜索子条目失败: %v", err)
}
if len(subSearchResult.Entries) > 0 {
// 如果用户条目包含子条目,返回错误信息
return false, fmt.Errorf("用户条目包含子条目,无法直接删除")
}
// 构造删除请求
delRequest := ldap.NewDelRequest(userDN, nil)
if err := conn.Del(delRequest); err != nil {
if ldapErr, ok := err.(*ldap.Error); ok {
switch ldapErr.ResultCode {
case ldap.LDAPResultNoSuchObject:
// 如果用户不存在,返回错误信息
return false, fmt.Errorf("用户不存在: %s", userDN)
case ldap.LDAPResultInsufficientAccessRights:
// 如果权限不足,返回错误信息
return false, fmt.Errorf("权限不足,请检查管理员ACL")
case ldap.LDAPResultNotAllowedOnNonLeaf:
// 如果用户条目包含子条目,返回错误信息
return false, fmt.Errorf("用户条目包含子条目,无法直接删除")
}
}
// 如果删除失败,返回错误信息
return false, fmt.Errorf("删除用户失败: %v", err)
}
// 删除成功,返回删除结果和无错误信息
return true, nil
}
// GetListUsersInOU 获取指定OU下的用户列表,根据OU名称获取该OU下的所有用户信息,返回用户列表和可能的错误信息
func GetListUsersInOU(conn *ldap.Conn, config *LdapConfig, ou string) ([]map[string]string, error) {
// 构造搜索基础DN
searchBase := fmt.Sprintf("ou=%s,%s", ou, config.BaseDn)
// 构造搜索过滤条件,只搜索inetOrgPerson类型的对象
searchFilter := "(objectClass=inetOrgPerson)"
// 创建搜索请求
searchRequest := ldap.NewSearchRequest(
searchBase,
ldap.ScopeSingleLevel,
ldap.NeverDerefAliases,
0, 0, false,
searchFilter,
[]string{"cn", "uid", "userPassword", "mail", "description", "sn", "objectClass", "dn"},
nil,
)
// 执行搜索请求
searchResult, err := conn.Search(searchRequest)
if err != nil {
// 如果搜索失败,返回错误信息
return nil, fmt.Errorf("搜索用户失败: %v", err)
}
// 初始化用户列表
users := make([]map[string]string, 0)
for _, entry := range searchResult.Entries {
// 初始化用户信息
user := make(map[string]string)
user["dn"] = entry.DN
user["uid"] = entry.GetAttributeValue("uid")
user["userPassword"] = entry.GetAttributeValue("userPassword")
user["cn"] = entry.GetAttributeValue("cn")
user["sn"] = entry.GetAttributeValue("sn")
user["mail"] = entry.GetAttributeValue("mail")
user["description"] = entry.GetAttributeValue("description")
user["objectClass"] = entry.GetAttributeValue("objectClass")
// 将用户信息添加到用户列表中
users = append(users, user)
}
// 返回用户列表和无错误信息
return users, nil
}
完整测试用例
package ldap_1_test
import (
"fmt"
"gitee.com/hexug/go-tools/format"
"go_auth_bridge/ldap_1"
"testing"
)
// TestLoginBind 测试LDAP管理员账户绑定功能
func TestLoginBind(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 如果绑定成功,使用t.Log输出成功信息
t.Log("ok")
}
}
// TestFindUser 测试根据用户名查找用户信息的功能
func TestFindUser(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 调用FindUser函数查找名为"hxg"的用户信息,获取搜索结果和可能的错误信息
res, err := ldap_1.FindUser(conn, conf, "hxg")
if err != nil {
// 如果查找失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 如果查找成功,将搜索结果格式化为JSON并打印
fmt.Println(format.FormatToJson(res))
}
}
}
// TestVerifyUserCredentials 测试验证用户凭据的功能
func TestVerifyUserCredentials(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 调用VerifyUserCredentials函数验证用户"hxg"的密码是否为"111111",获取验证结果和可能的错误信息
isValid, err := ldap_1.VerifyUserCredentials(conn, conf, "hxg", "111111")
if err != nil {
// 如果验证过程中出现错误,使用t.Fatal输出错误信息并终止测试
t.Fatal(err)
}
if isValid {
// 如果验证成功,使用t.Log输出登录成功信息
t.Log("登录成功")
} else {
// 如果验证失败,使用t.Error输出登录失败信息
t.Error("登录失败")
}
}
}
// TestAddUser 测试向LDAP目录添加用户的功能
func TestAddUser(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 构造要添加的用户属性
user := map[string]string{
"uid": "john.doe4",
"cn": "John Doe4",
"sn": "Doe4", // 姓氏必填
"userPassword": "111111",
"mail": "john@hexug.com",
"description": "+86-13800138000",
}
// 调用AddUser函数将用户添加到LDAP目录,获取添加结果和可能的错误信息
success, err := ldap_1.AddUser(conn, conf, user)
if err != nil {
// 如果添加失败,打印错误信息并返回
fmt.Printf("添加失败: %v\n", err)
return
}
if success {
// 如果添加成功,打印添加成功信息
fmt.Println("用户添加成功")
// 可选步骤:立即验证用户
isValid, verifyErr := ldap_1.VerifyUserCredentials(conn, conf, user["uid"], user["userPassword"])
if verifyErr != nil {
// 如果验证过程中出现错误,打印错误信息
fmt.Println("验证时出错:", verifyErr)
} else if isValid {
// 如果验证成功,打印验证通过信息
fmt.Println("用户验证通过")
}
}
}
}
// TestModifyUserPasswordByAdmin 测试管理员修改用户密码的功能
func TestModifyUserPasswordByAdmin(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 定义新密码
newPassword := "222222"
// 调用ModifyUserPasswordByAdmin函数修改用户"john.doe4"的密码,获取修改结果和可能的错误信息
success, err := ldap_1.ModifyUserPasswordByAdmin(conn, conf, "John Doe4", newPassword)
if err != nil {
// 如果修改失败,打印错误信息
fmt.Printf("修改密码失败: %v\n", err)
} else {
// 如果修改成功,打印修改结果和成功信息
fmt.Println(success)
fmt.Println("修改密码成功")
}
}
}
// TestDeleteUser 测试删除LDAP目录中用户的功能
func TestDeleteUser(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 调用DeleteUser函数删除用户"hexug1",获取删除结果和可能的错误信息
success, err := ldap_1.DeleteUser(conn, conf, "John Doe4")
if err != nil {
// 如果删除失败,打印错误信息
fmt.Printf("删除用户失败: %v\n", err)
} else {
// 如果删除成功,打印删除结果和成功信息
fmt.Println(success)
fmt.Println("删除用户成功")
}
}
}
// TestGetListUsersInOU 测试获取指定组织单元(OU)下用户列表的功能
func TestGetListUsersInOU(t *testing.T) {
// 初始化LDAP配置
conf := ldap_1.NewLdapConfig()
// 调用LoginBind函数进行LDAP管理员账户绑定,获取连接对象和可能的错误信息
conn, err := ldap_1.LoginBind(conf)
// 确保在函数结束时关闭连接
defer conn.Close()
if err != nil {
// 如果绑定失败,使用t.Error输出错误信息
t.Error(err)
} else {
// 定义要查询的组织单元名称
ou := "users"
// 调用GetListUsersInOU函数获取指定组织单元下的用户列表,获取用户列表和可能的错误信息
users, err := ldap_1.GetListUsersInOU(conn, conf, ou)
if err != nil {
// 如果获取失败,打印错误信息并返回
fmt.Printf("获取用户失败: %v\n", err)
return
}
// 遍历用户列表并打印每个用户的信息
for _, user := range users {
fmt.Printf("用户 DN: %s\n", user["dn"])
fmt.Printf("用户 UID: %s\n", user["uid"])
fmt.Printf("用户 密码: %s\n", user["userPassword"])
fmt.Printf("用户 CN: %s\n", user["cn"])
fmt.Printf("用户 SN: %s\n", user["sn"])
fmt.Printf("用户邮箱: %s\n", user["mail"])
fmt.Printf("用户description: %s\n", user["description"])
fmt.Printf("用户objectClass: %s\n", user["objectClass"])
fmt.Println("------")
}
}
}
本文来自博客园,作者:厚礼蝎,转载请注明原文链接:https://www.cnblogs.com/guangdelw/p/18762260

浙公网安备 33010602011771号