You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于FastAPI与ldap3的AD认证接口连接池设计与实现咨询

基于FastAPI与ldap3的AD认证接口连接池设计与实现咨询

Hey there! Let's break down how to implement a persistent LDAP connection pool for your FastAPI app step by step—since you're looking to move away from creating a new connection every time the /auth/submit_credentials endpoint is called, this will help boost performance and reduce overhead with your AD servers.

First, Let's Fix the Core Problem: Where to Put the Connection Pool

Right now, you're creating a new ServerPool and Connection every time ldap_auth runs. That defeats the purpose of a connection pool! Instead, we need to:

  • Initialize connection pools once (either when the app starts, or on first use of a domain)
  • Store them in a global, thread-safe location so all request handlers can reuse them
  • Reuse connections from the pool instead of spinning up new ones for every request

Step 1: Refactor Your LDAP Auth Module

Let's rewrite /app/dependencies/ldapauth.py to use persistent connection pools. We'll use a global dictionary to track pools per domain, and add an initialization function to set them up:

from ldap3 import ServerPool, Server, Connection, FIRST
import logging

logger = logging.getLogger(__name__)

# Global storage for domain-specific LDAP connection pools
ldap_pools = {}

# Preconfigure your domain-to-server mappings (move this to a config file later if you want!)
LDAP_SERVER_CONFIGS = [
    {"domain": "domain1", "addresses": ("dc1.domain1.internal", "dc2.domain1.internal")},
    {"domain": "domain2.internal", "addresses": ("dc1.domain2.internal", "dc2.domain2.internal")}
]

def init_ldap_pools():
    """Initialize connection pools for all configured domains when the app starts"""
    for config in LDAP_SERVER_CONFIGS:
        domain = config["domain"]
        addresses = config["addresses"]
        
        # Create server pool for the domain
        server_pool = ServerPool(None, strategy=FIRST, active=True, exhaust=True)
        for addr in addresses:
            server = Server(addr, use_ssl=True, get_info="DSA")
            server_pool.add(server)
        
        # Create a pooled connection setup (adjust pool_size based on your expected concurrency)
        # We'll use pool parameters to manage persistent connections
        ldap_pools[domain] = {
            "server_pool": server_pool,
            "pool_settings": {
                "pool_size": 10,  # Number of persistent connections to keep
                "pool_lifetime": 3600  # Max time a connection stays in the pool (seconds)
            }
        }
    logger.info("All LDAP connection pools initialized successfully")

def get_domain_from_username(username):
    """Helper to extract domain from UPN-format username"""
    try:
        return username.split('@')[1]
    except IndexError:
        logger.error(f"Invalid username format: {username}")
        raise ValueError("Username must be in UPN format (user@domain)")

def ldap_auth(username, password):
    try:
        response = {}
        domain = get_domain_from_username(username)
        
        # Check if we have a pool for this domain
        if domain not in ldap_pools:
            logger.error(f"No LDAP pool configured for domain: {domain}")
            return {"outcome": "User_LDAP_Error", "error": "Unsupported domain"}
        
        pool_config = ldap_pools[domain]
        
        # Get a connection from the pool (ldap3 handles connection reuse automatically)
        conn = Connection(
            pool_config["server_pool"],
            user=username,
            password=password,
            auto_bind=True,
            raise_exceptions=True,
            **pool_config["pool_settings"]
        )
        
        try:
            # Get the default naming context from the server info (cleaner than accessing __dict__)
            default_naming_context = conn.server.info.other['defaultNamingContext'][0]
            search_filter = f"(UserPrincipalName={username})"
            
            # Run the group search
            conn.search(
                search_base=default_naming_context,
                search_filter=search_filter,
                search_scope="SUBTREE",
                attributes=["memberOf"]
            )
            
            if not conn.entries:
                logger.warning(f"No AD entry found for user: {username}")
                return {"outcome": "User_Invalid", "error": "User not found in Active Directory"}
            
            groups = conn.entries[0].memberOf.values
            response = {
                "outcome": "User_Valid",
                "username": username,
                "groups": groups
            }
        finally:
            # Return the connection to the pool (don't close it!)
            conn.unbind()
            logger.debug(f"Connection returned to pool for domain: {domain}")
        
        return response
    
    except ValueError as ve:
        logger.error(f"LDAP auth validation error: {ve}")
        return {"outcome": "User_LDAP_Error", "error": str(ve)}
    except Exception as ldaperror:
        logger.error(f"LDAP auth error: {ldaperror}")
        # Don't expose raw exception __dict__ (risk of leaking sensitive info!)
        return {"outcome": "User_LDAP_Error", "error": str(ldaperror)}

Step 2: Initialize Pools on App Startup

Update /app/main.py to run the pool initialization when your FastAPI app starts up:

from fastapi import FastAPI, Depends
from routers.useractions import useractions
from dependencies.ldapauth import init_ldap_pools
import logging

logger = logging.getLogger(__name__)

app = FastAPI(
    dependencies=[Depends(get_api_key)]  # Keep your existing API key dependency
)

# Initialize LDAP pools when the app starts
@app.on_event("startup")
async def startup_event():
    logger.info("Starting up: Initializing LDAP connection pools...")
    init_ldap_pools()

app.include_router(useractions)

@app.get("/")
async def root():
    logger.info("Root API endpoint called")
    return {"message": "please read the docs at endpoint /docs"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, log_config=None, host="0.0.0.0", port=8000)

Key Design & Technical Notes

  1. Thread Safety: ldap3's connection pool implementation is thread-safe, so you don't have to worry about race conditions when multiple FastAPI request threads access the pool.

  2. Pool Tuning: Adjust pool_size based on your expected concurrent requests—if you typically have 15-20 simultaneous auth requests, set pool_size=20 to avoid waiting for connections. pool_lifetime ensures stale connections are replaced after an hour (AD servers often drop idle connections after this time).

  3. Security Best Practices:

    • Never return raw exception __dict__ in responses—it can leak sensitive LDAP server details or partial credentials.
    • If you want to use a dedicated service account for group searches (instead of the user's own credentials), create a separate connection pool bound to that service account. You'd first validate the user's credentials with a temporary connection, then use the service pool to fetch groups.
  4. Lazy Loading Alternative: If you don't want to initialize all pools at startup (e.g., for rarely used domains), you can modify ldap_auth to create the pool on the first request for that domain instead.

Final Thoughts

This setup will keep persistent connections open to your AD servers, reuse them across requests, and eliminate the overhead of creating new connections every time. It's clean, scalable, and aligns with FastAPI's async/threaded model.

备注:内容来源于stack exchange,提问作者dada216

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.20 11:33:00