GO对接ldap认证

基本流程

  1. 连接到LDAP服务器并绑定到LDAP服务器;(一般以管理员用户绑定,权限更大)
  2. 在LDAP服务器上执行所需的任何操作;
  3. 释放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("------")
		}
	}
}
posted @ 2025-03-10 11:21  厚礼蝎  阅读(339)  评论(0)    收藏  举报