ldap-1-backend/public/common/ldap.go

231 lines
6.9 KiB
Go
Raw Permalink Normal View History

2025-08-26 00:43:50 +08:00
/*
Package common 提供 LDAP 连接池管理功能
LDAP 连接池是系统与 OpenLDAP 服务器交互的核心组件负责
- 管理 LDAP 连接的创建复用和销毁
- 提供线程安全的连接获取和归还机制
- 控制最大连接数避免资源耗尽
- 支持连接的健康检查和自动重连
连接池的设计目标
1. 提高 LDAP 操作的性能避免频繁建立连接
2. 控制并发连接数保护 LDAP 服务器资源
3. 提供稳定可靠的连接管理机制
4. 支持优雅的连接回收和错误处理
*/
2022-05-18 17:57:03 +08:00
package common
import (
"fmt"
2022-07-24 21:24:08 +08:00
"log"
"math/rand"
2022-05-18 17:57:03 +08:00
"net"
2022-07-24 21:24:08 +08:00
"sync"
2022-05-18 17:57:03 +08:00
"time"
2022-05-29 10:06:21 +08:00
"github.com/eryajf/go-ldap-admin/config"
2022-05-18 17:57:03 +08:00
ldap "github.com/go-ldap/ldap/v3"
)
2025-08-26 00:43:50 +08:00
// LDAP 连接池相关全局变量
var (
ldapPool *LdapConnPool // LDAP 连接池实例
ldapInit = false // LDAP 初始化标志
ldapInitOne sync.Once // 确保 LDAP 只初始化一次
)
2022-05-18 17:57:03 +08:00
2025-08-26 00:43:50 +08:00
// InitLDAP 初始化 LDAP 连接池
// 该函数在系统启动时调用,负责:
// 1. 建立与 LDAP 服务器的初始连接
// 2. 验证管理员账户凭据
// 3. 初始化连接池结构
// 4. 设置连接池参数
2022-05-18 17:57:03 +08:00
func InitLDAP() {
2025-08-26 00:43:50 +08:00
// 防止重复初始化
2022-07-24 21:24:08 +08:00
if ldapInit {
return
}
2025-08-26 00:43:50 +08:00
// 使用 sync.Once 确保只初始化一次
2022-07-24 21:24:08 +08:00
ldapInitOne.Do(func() {
ldapInit = true
})
2025-08-26 00:43:50 +08:00
// ==================== 建立初始 LDAP 连接 ====================
// 创建 LDAP 连接,设置 5 秒连接超时
2022-07-24 21:24:08 +08:00
ldapConn, err := ldap.DialURL(config.Conf.Ldap.Url, ldap.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}))
2022-05-18 17:57:03 +08:00
if err != nil {
2025-08-26 00:43:50 +08:00
Log.Panicf("初始化 LDAP 连接异常: %v", err)
panic(fmt.Errorf("初始化 LDAP 连接异常: %v", err))
2022-05-18 17:57:03 +08:00
}
2025-08-26 00:43:50 +08:00
// 使用管理员账户绑定 LDAP 连接
2022-07-24 21:24:08 +08:00
err = ldapConn.Bind(config.Conf.Ldap.AdminDN, config.Conf.Ldap.AdminPass)
2022-05-18 17:57:03 +08:00
if err != nil {
2025-08-26 00:43:50 +08:00
Log.Panicf("绑定 LDAP 管理员账号异常: %v", err)
panic(fmt.Errorf("绑定 LDAP 管理员账号异常: %v", err))
2022-05-18 17:57:03 +08:00
}
2025-08-26 00:43:50 +08:00
// ==================== 初始化连接池 ====================
// 创建连接池实例
2022-07-24 21:24:08 +08:00
ldapPool = &LdapConnPool{
2025-08-26 00:43:50 +08:00
conns: make([]*ldap.Conn, 0), // 可用连接切片
reqConns: make(map[uint64]chan *ldap.Conn), // 等待连接的请求映射
openConn: 0, // 当前打开的连接数
maxOpen: config.Conf.Ldap.MaxConn, // 最大连接数
2022-07-24 21:24:08 +08:00
}
2025-08-26 00:43:50 +08:00
// 将初始连接放入连接池
2022-07-24 21:24:08 +08:00
PutLADPConn(ldapConn)
2022-05-18 17:57:03 +08:00
2025-08-26 00:43:50 +08:00
// ==================== 输出初始化信息 ====================
// 构建显示用的 DSN隐藏密码
2022-05-18 17:57:03 +08:00
showDsn := fmt.Sprintf(
"%s:******@tcp(%s)",
config.Conf.Ldap.AdminDN,
config.Conf.Ldap.Url,
2022-05-18 17:57:03 +08:00
)
2025-08-26 00:43:50 +08:00
Log.Info("🔗 LDAP 连接池初始化完成! DSN: ", showDsn)
2022-05-18 17:57:03 +08:00
}
2022-07-24 21:24:08 +08:00
2025-08-26 00:43:50 +08:00
// GetLDAPConn 从连接池获取 LDAP 连接
// 返回一个可用的 LDAP 连接,使用完毕后需要调用 PutLADPConn 归还
2022-07-24 21:24:08 +08:00
func GetLDAPConn() (*ldap.Conn, error) {
return ldapPool.GetConnection()
}
2025-08-26 00:43:50 +08:00
// PutLADPConn 将 LDAP 连接归还到连接池
// 连接使用完毕后必须调用此方法归还,以便其他请求复用
2022-07-24 21:24:08 +08:00
func PutLADPConn(conn *ldap.Conn) {
ldapPool.PutConnection(conn)
}
2025-08-26 00:43:50 +08:00
// LdapConnPool LDAP 连接池结构体
// 实现了线程安全的连接池管理机制
2022-07-24 21:24:08 +08:00
type LdapConnPool struct {
2025-08-26 00:43:50 +08:00
mu sync.Mutex // 互斥锁,保证线程安全
conns []*ldap.Conn // 可用连接切片
reqConns map[uint64]chan *ldap.Conn // 等待连接的请求映射表
openConn int // 当前打开的连接数
maxOpen int // 最大允许的连接数
2022-07-24 21:24:08 +08:00
}
2025-08-26 00:43:50 +08:00
// GetConnection 从连接池获取一个可用的 LDAP 连接
// 该方法实现了连接池的核心逻辑:
// 1. 优先从池中获取现有连接
// 2. 检查连接健康状态
// 3. 控制最大连接数限制
// 4. 支持连接等待队列
2022-07-24 21:24:08 +08:00
func (lcp *LdapConnPool) GetConnection() (*ldap.Conn, error) {
lcp.mu.Lock()
2025-08-26 00:43:50 +08:00
// ==================== 从连接池获取现有连接 ====================
// 检查连接池中是否有可用连接
2022-07-24 21:24:08 +08:00
connNum := len(lcp.conns)
if connNum > 0 {
2025-08-26 00:43:50 +08:00
lcp.openConn++ // 增加打开连接计数
conn := lcp.conns[0] // 获取第一个连接
copy(lcp.conns, lcp.conns[1:]) // 移除已获取的连接
lcp.conns = lcp.conns[:connNum-1] // 调整切片长度
2022-07-24 21:24:08 +08:00
lcp.mu.Unlock()
2025-08-26 00:43:50 +08:00
// 检查连接是否已关闭,如果已关闭则创建新连接
2022-07-24 21:24:08 +08:00
if conn.IsClosing() {
return initLDAPConn()
}
return conn, nil
}
2025-08-26 00:43:50 +08:00
// ==================== 连接数限制处理 ====================
// 当连接池为空且已达到最大连接数限制时
if lcp.maxOpen != 0 && lcp.openConn >= lcp.maxOpen {
// 创建等待队列,当有连接归还时会通知等待的请求
2022-07-24 21:24:08 +08:00
req := make(chan *ldap.Conn, 1)
2025-08-26 00:43:50 +08:00
reqKey := lcp.nextRequestKeyLocked() // 生成唯一请求键
lcp.reqConns[reqKey] = req // 将请求加入等待队列
2022-07-24 21:24:08 +08:00
lcp.mu.Unlock()
2025-08-26 00:43:50 +08:00
// 阻塞等待连接归还
2022-07-24 21:24:08 +08:00
return <-req, nil
} else {
2025-08-26 00:43:50 +08:00
// 未达到连接数限制,创建新连接
2022-07-24 21:24:08 +08:00
lcp.openConn++
lcp.mu.Unlock()
return initLDAPConn()
}
}
2025-08-26 00:43:50 +08:00
// PutConnection 将 LDAP 连接归还到连接池
// 该方法负责连接的回收和分配:
// 1. 优先满足等待队列中的请求
// 2. 将健康的连接放回连接池
// 3. 丢弃已关闭的连接
2022-07-24 21:24:08 +08:00
func (lcp *LdapConnPool) PutConnection(conn *ldap.Conn) {
2025-08-26 00:43:50 +08:00
log.Println("🔄 归还一个 LDAP 连接到连接池")
2022-07-24 21:24:08 +08:00
lcp.mu.Lock()
defer lcp.mu.Unlock()
2025-08-26 00:43:50 +08:00
// ==================== 优先处理等待队列 ====================
// 检查是否有等待连接的请求
2022-07-24 21:24:08 +08:00
if num := len(lcp.reqConns); num > 0 {
var req chan *ldap.Conn
var reqKey uint64
2025-08-26 00:43:50 +08:00
// 获取第一个等待请求
2022-07-24 21:24:08 +08:00
for reqKey, req = range lcp.reqConns {
break
}
2025-08-26 00:43:50 +08:00
// 从等待队列中移除该请求
2022-07-24 21:24:08 +08:00
delete(lcp.reqConns, reqKey)
2025-08-26 00:43:50 +08:00
// 将连接发送给等待的请求
2022-07-24 21:24:08 +08:00
req <- conn
return
2025-08-26 00:43:50 +08:00
}
// ==================== 连接回收处理 ====================
// 没有等待请求时,将连接放回连接池
lcp.openConn-- // 减少打开连接计数
// 只有健康的连接才放回连接池
if !conn.IsClosing() {
lcp.conns = append(lcp.conns, conn)
2022-07-24 21:24:08 +08:00
}
}
2025-08-26 00:43:50 +08:00
// nextRequestKeyLocked 生成下一个唯一的请求键
// 用于标识等待队列中的请求,确保每个等待请求都有唯一标识
// 注意:调用此方法时必须已经持有互斥锁
2022-07-24 21:24:08 +08:00
func (lcp *LdapConnPool) nextRequestKeyLocked() uint64 {
for {
2025-08-26 00:43:50 +08:00
reqKey := rand.Uint64() // 生成随机数作为请求键
if _, ok := lcp.reqConns[reqKey]; !ok { // 确保键的唯一性
2022-07-24 21:24:08 +08:00
return reqKey
}
}
}
// 获取 ladp 连接
func initLDAPConn() (*ldap.Conn, error) {
ldap, err := ldap.DialURL(config.Conf.Ldap.Url, ldap.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}))
if err != nil {
return nil, err
}
err = ldap.Bind(config.Conf.Ldap.AdminDN, config.Conf.Ldap.AdminPass)
if err != nil {
return nil, err
}
return ldap, err
}