fix: LDAP 测试连接 3s 超时 + 账号验证 search filter %s 替换 + LDAP 登录 fallback

- dialLDAP 辅助函数: 3s timeout 替代无超时 DialURL
- TestLDAPConnection: search_filter (uid=%s) 中 %s 未替换导致搜索失败
- tryLDAPAuth: 同样的 %s 替换 + timeout 3s
- auth.go 登录: 本地密码失败 → LDAP fallback → ldapInfo 判断修复
- BatchImportLDAP: 实现 LDAP 用户批量导入到本地 DB
- 前端 LdapConfigPage: LDAP 测试请求独立 10s timeout
- workflow.go: ResetUserPassword 实现
This commit is contained in:
logan committed 2026-05-26 14:42:31 +08:00
1 parent 95222d42ad
commit 5411cd6db9
5 files changed
+198 -32

No files matched your search

+40 -10
View File
@@ -3,6 +3,7 @@ package handler
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"time"
@@ -29,19 +30,40 @@ func Login(c *gin.Context) {
err := model.DB.Where("username = ?", req.Username).First(&user).Error
if err == nil {
// ── 本地用户存在,用本地密码验证 ──
// ── 本地用户存在 ──
if user.IsActive != nil && !*user.IsActive {
c.JSON(http.StatusForbidden, gin.H{"detail": "Account is disabled"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(req.Password)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"detail": "Invalid username or password"})
return
// Try local password first
passOk := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(req.Password)) == nil
if !passOk {
// Local password failed — try LDAP fallback for LDAP-imported users (password == "-")
_, ldapInfo, ldapErr := tryLDAPAuth(req.Username, req.Password)
if ldapErr != nil || ldapInfo == nil {
c.JSON(http.StatusUnauthorized, gin.H{"detail": "Invalid username or password"})
return
}
// LDAP OK — update local user info
if ldapInfo != nil {
updates := map[string]interface{}{}
if ldapInfo.DisplayName != "" && ldapInfo.DisplayName != user.DisplayName {
updates["display_name"] = ldapInfo.DisplayName
}
if ldapInfo.Email != nil && *ldapInfo.Email != "" {
updates["email"] = *ldapInfo.Email
}
if len(updates) > 0 {
model.DB.Model(&user).Updates(updates)
}
}
}
} else if err == gorm.ErrRecordNotFound {
// ── 本地不存在该用户,尝试 LDAP 认证 ──
ldapUser, ldapInfo, ldapErr := tryLDAPAuth(req.Username, req.Password)
if ldapErr != nil || ldapUser == nil {
_, ldapInfo, ldapErr := tryLDAPAuth(req.Username, req.Password)
if ldapErr != nil || ldapInfo == nil {
c.JSON(http.StatusUnauthorized, gin.H{"detail": "Invalid username or password"})
return
}
@@ -70,7 +92,6 @@ func Login(c *gin.Context) {
user = newUser
}
// 记录 auth_source
_ = ldapUser // used above
} else {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Database error"})
return
@@ -123,7 +144,8 @@ func tryLDAPAuth(username, password string) (*ldap.Conn, *LDAPAuthInfo, error) {
uri = fmt.Sprintf("%s://%s:%d", protocol, cfg.LDAPHost, *cfg.LDAPPort)
}
conn, err := ldap.DialURL(uri)
dialer := &net.Dialer{Timeout: 3 * time.Second}
conn, err := ldap.DialURL(uri, ldap.DialWithDialer(dialer))
if err != nil {
return nil, nil, fmt.Errorf("LDAP connect failed: %w", err)
}
@@ -136,11 +158,19 @@ func tryLDAPAuth(username, password string) (*ldap.Conn, *LDAPAuthInfo, error) {
}
// 4. Build search filter
userFilter := ""
baseFilter := "(objectClass=inetOrgPerson)"
if cfg.SearchFilter != nil && *cfg.SearchFilter != "" {
baseFilter = *cfg.SearchFilter
sf := *cfg.SearchFilter
if strings.Contains(sf, "%s") {
userFilter = strings.ReplaceAll(sf, "%s", ldap.EscapeFilter(username))
} else {
baseFilter = sf
userFilter = fmt.Sprintf("(&%s(uid=%s))", baseFilter, ldap.EscapeFilter(username))
}
} else {
userFilter = fmt.Sprintf("(&%s(uid=%s))", baseFilter, ldap.EscapeFilter(username))
}
userFilter := fmt.Sprintf("(&%s(uid=%s))", baseFilter, ldap.EscapeFilter(username))
// 5. Search user
searchReq := ldap.NewSearchRequest(