
本文介绍如何在 go web 应用中基于 active directory 实现 basic auth 认证,并在认证通过后可靠地获取并传递用户邮箱信息,避免全局连接复用和状态丢失问题。
本文介绍如何在 go web 应用中基于 active directory 实现 basic auth 认证,并在认证通过后可靠地获取并传递用户邮箱信息,避免全局连接复用和状态丢失问题。
在当前代码结构中,auth.BasicAuth 是一个中间件式包装器,它在调用 mainHandler 前完成认证,但无法将认证后的用户上下文(如邮箱)透传给后续 handler——因为 http.HandlerFunc 签名固定为 (w, r),不支持附加数据。此外,原实现中 conn 被声明为包级变量并在 ValidateAD 中重复 Dial/Close,存在并发安全隐患(defer conn.Close() 在函数返回时执行,而 conn 可能被多个请求共享)。
✅ 推荐方案:使用 http.Handler 接口 + 请求上下文传递
我们重构为自定义 authHandler 类型,实现 http.Handler 接口,使其既能完成认证、查询邮箱,又能将用户信息注入 *http.Request.Context(),供下游 handler 安全读取:
// auth/handler.go
package auth
import (
"context"
"net/http"
"strings"
)
type User struct {
Username string
Email string
}
// Key for context value
type userContextKey string
const userKey userContextKey = "auth_user"
// AuthHandler 实现 http.Handler,负责认证与用户信息注入
type AuthHandler struct {
Next http.Handler
}
func (a *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok {
a.unauthorized(w, "Missing credentials")
return
}
// 1. 验证 AD 凭据(注意:每次请求新建独立连接)
if err := ValidateAD(username, password); err != nil {
a.unauthorized(w, "AD authentication failed: "+err.Error())
return
}
// 2. 查询邮箱(复用 ValidateAD 的逻辑,但需确保连接安全)
email, err := GetLDAPEmail(username, password)
if err != nil {
http.Error(w, "Failed to fetch user email", http.StatusInternalServerError)
return
}
// 3. 将用户信息注入 request.Context
user := &User{Username: username, Email: email}
ctx := context.WithValue(r.Context(), userKey, user)
r = r.WithContext(ctx)
// 4. 继续处理
a.Next.ServeHTTP(w, r)
}
func (a *AuthHandler) unauthorized(w http.ResponseWriter, msg string) {
w.Header().Set("WWW-Authenticate", `Basic realm="Corporate AD Login"`)
http.Error(w, msg, http.StatusUnauthorized)
}
// Helper: 从 request.Context 提取用户信息
func GetUser(r *http.Request) (*User, bool) {
u, ok := r.Context().Value(userKey).(*User)
return u, ok
}
? 安全增强:修复 LDAP 连接管理
原 GetLDAPEmail 复用了 ValidateAD 中的 conn,但该连接在 ValidateAD 返回时已被 defer conn.Close() 关闭,导致后续 conn.Search() panic。正确做法是:每个请求创建独立 TLS 连接,并在查询完成后显式关闭:
// auth/ldap.go(修改 GetLDAPEmail)
func GetLDAPEmail(user, password string) (string, error) {
// 1. 先验证凭据(此处仅校验,不复用连接)
if err := ValidateAD(user, password); err != nil {
return "", err
}
// 2. 新建独立连接用于查询(关键修复)
tlsConfig := &tls.Config{InsecureSkipVerify: true}
conn, err := ldap.DialTLS("tcp", "ad.something.com:636", tlsConfig)
if err != nil {
return "", err
}
defer conn.Close() // 确保本次查询后关闭
// 3. Bind 以获得查询权限(可选:若匿名查询不可用,则需 Bind)
domainPrefix := "ad\"
if err := conn.Bind(domainPrefix+user, password); err != nil {
return "", err
}
// 4. 执行搜索(注意:searchBase 应使用更健壮的 DN 查找,而非硬编码 CN=xxx)
searchRequest := ldap.NewSearchRequest(
"DC=ad,DC=something,DC=com", // 推荐使用根域作为 base
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass=user)(sAMAccountName=%s))", ldap.EscapeFilter(user)),
[]string{"mail"},
nil,
)
sr, err := conn.Search(searchRequest)
if err != nil || len(sr.Entries) == 0 {
return "", fmt.Errorf("email not found for user %s", user)
}
email := sr.Entries[0].GetAttributeValue("mail")
return strings.ToLower(email), nil
}
? 主程序集成
// main.go
func main() {
mux := http.NewServeMux()
mux.Handle("/", &auth.AuthHandler{
Next: http.HandlerFunc(mainHandler),
})
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
func mainHandler(w http.ResponseWriter, r *http.Request) {
// 从 Context 安全提取用户信息
if user, ok := auth.GetUser(r); ok {
data := struct {
Username string
Email string
}{
Username: user.Username,
Email: user.Email,
}
tmpl, err := template.ParseFiles("templates/main.html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
http.Error(w, "User not authenticated", http.StatusForbidden)
}
⚠️ 注意事项总结
-
绝不复用 LDAP 连接:每个 HTTP 请求应创建并关闭独立
*ldap.Conn,避免并发冲突与已关闭连接误用。 -
DN 构造要安全:使用
ldap.EscapeFilter()防止 LDAP 注入;搜索 Base DN 推荐使用域根(如DC=ad,DC=something,DC=com),而非依赖用户名拼接 CN。 - Context 是传递请求级数据的唯一推荐方式:避免全局变量或闭包捕获,保证 Goroutine 安全。
-
生产环境务必禁用
InsecureSkipVerify:应配置正确的 CA 证书用于 LDAPS 验证。 -
考虑缓存邮箱信息:对高频访问用户,可结合
sync.Map或 Redis 缓存username → email映射,减少 LDAP 查询压力。
通过以上重构,你将获得一个线程安全、可扩展、符合 Go 最佳实践的 AD 集成 Web 认证方案。










