feat: add ldap login

This commit is contained in:
RWDai
2026-01-02 16:17:24 +08:00
parent cddc22d2b3
commit 9bfb295238
18 changed files with 1007 additions and 18 deletions

157
src/services/auth/ldap.py Normal file
View File

@@ -0,0 +1,157 @@
"""LDAP 认证服务"""
from typing import Optional, Tuple
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.models.database import LDAPConfig
class LDAPService:
"""LDAP 认证服务"""
@staticmethod
def get_config(db: Session) -> Optional[LDAPConfig]:
"""获取 LDAP 配置"""
return db.query(LDAPConfig).first()
@staticmethod
def is_ldap_enabled(db: Session) -> bool:
"""检查 LDAP 是否启用"""
config = LDAPService.get_config(db)
return config.is_enabled if config else False
@staticmethod
def is_ldap_exclusive(db: Session) -> bool:
"""检查是否仅允许 LDAP 登录"""
config = LDAPService.get_config(db)
return config.is_exclusive if config and config.is_enabled else False
@staticmethod
def authenticate(db: Session, username: str, password: str) -> Optional[dict]:
"""
LDAP bind 验证
Args:
db: 数据库会话
username: 用户名
password: 密码
Returns:
用户属性 dict {username, email, display_name} 或 None
"""
try:
import ldap3
from ldap3 import Server, Connection, SUBTREE
from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError
except ImportError:
logger.error("ldap3 库未安装")
return None
config = LDAPService.get_config(db)
if not config or not config.is_enabled:
logger.warning("LDAP 未配置或未启用")
return None
try:
# 创建服务器连接
use_ssl = config.server_url.startswith("ldaps://")
server = Server(config.server_url, use_ssl=use_ssl, get_info=ldap3.ALL)
# 使用管理员账号连接
bind_password = config.get_bind_password()
admin_conn = Connection(server, user=config.bind_dn, password=bind_password)
if config.use_starttls and not use_ssl:
admin_conn.start_tls()
if not admin_conn.bind():
logger.error(f"LDAP 管理员绑定失败: {admin_conn.result}")
return None
# 搜索用户
search_filter = config.user_search_filter.replace("{username}", username)
admin_conn.search(
search_base=config.base_dn,
search_filter=search_filter,
search_scope=SUBTREE,
attributes=[config.username_attr, config.email_attr, config.display_name_attr],
)
if not admin_conn.entries:
logger.warning(f"LDAP 用户未找到: {username}")
admin_conn.unbind()
return None
user_entry = admin_conn.entries[0]
user_dn = user_entry.entry_dn
admin_conn.unbind()
# 用户密码验证
user_conn = Connection(server, user=user_dn, password=password)
if config.use_starttls and not use_ssl:
user_conn.start_tls()
if not user_conn.bind():
logger.warning(f"LDAP 密码验证失败: {username}")
return None
user_conn.unbind()
# 提取用户属性
email = str(getattr(user_entry, config.email_attr, "")) or f"{username}@ldap.local"
display_name = str(getattr(user_entry, config.display_name_attr, "")) or username
logger.info(f"LDAP 认证成功: {username}")
return {
"username": username,
"email": email,
"display_name": display_name,
}
except LDAPSocketOpenError as e:
logger.error(f"LDAP 服务器连接失败: {e}")
return None
except LDAPBindError as e:
logger.error(f"LDAP 绑定失败: {e}")
return None
except Exception as e:
logger.error(f"LDAP 认证异常: {e}")
return None
@staticmethod
def test_connection(db: Session) -> Tuple[bool, str]:
"""
测试 LDAP 连接
Returns:
(success, message)
"""
try:
import ldap3
from ldap3 import Server, Connection
except ImportError:
return False, "ldap3 库未安装"
config = LDAPService.get_config(db)
if not config:
return False, "LDAP 配置不存在"
try:
use_ssl = config.server_url.startswith("ldaps://")
server = Server(config.server_url, use_ssl=use_ssl, get_info=ldap3.ALL)
bind_password = config.get_bind_password()
conn = Connection(server, user=config.bind_dn, password=bind_password)
if config.use_starttls and not use_ssl:
conn.start_tls()
if not conn.bind():
return False, f"绑定失败: {conn.result}"
conn.unbind()
return True, "连接成功"
except Exception as e:
return False, f"连接失败: {str(e)}"

View File

@@ -15,8 +15,10 @@ from sqlalchemy.orm import Session, joinedload
from src.config import config
from src.core.crypto import crypto_service
from src.core.logger import logger
from src.core.enums import AuthSource
from src.models.database import ApiKey, User, UserRole
from src.services.auth.jwt_blacklist import JWTBlacklistService
from src.services.auth.ldap import LDAPService
from src.services.cache.user_cache import UserCacheService
from src.services.user.apikey import ApiKeyService
@@ -92,8 +94,36 @@ class AuthService:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的Token")
@staticmethod
async def authenticate_user(db: Session, email: str, password: str) -> Optional[User]:
"""用户登录认证"""
async def authenticate_user(
db: Session, email: str, password: str, auth_type: str = "local"
) -> Optional[User]:
"""用户登录认证
Args:
db: 数据库会话
email: 邮箱/用户名
password: 密码
auth_type: 认证类型 ("local""ldap")
"""
if auth_type == "ldap":
# LDAP 认证
if not LDAPService.is_ldap_enabled(db):
logger.warning("登录失败 - LDAP 认证未启用")
return None
ldap_user = LDAPService.authenticate(db, email, password)
if not ldap_user:
return None
# 获取或创建本地用户
user = await AuthService._get_or_create_ldap_user(db, ldap_user)
return user
# 本地认证
if LDAPService.is_ldap_exclusive(db):
logger.warning("登录失败 - 仅允许 LDAP 登录")
return None
# 登录校验必须读取密码哈希,不能使用不包含 password_hash 的缓存对象
user = db.query(User).filter(User.email == email).first()
@@ -101,6 +131,11 @@ class AuthService:
logger.warning(f"登录失败 - 用户不存在: {email}")
return None
# 检查用户认证来源
if user.auth_source == AuthSource.LDAP:
logger.warning(f"登录失败 - 该用户使用 LDAP 认证: {email}")
return None
if not user.verify_password(password):
logger.warning(f"登录失败 - 密码错误: {email}")
return None
@@ -118,6 +153,42 @@ class AuthService:
logger.info(f"用户登录成功: {email} (ID: {user.id})")
return user
@staticmethod
async def _get_or_create_ldap_user(db: Session, ldap_user: dict) -> User:
"""获取或创建 LDAP 用户
Args:
ldap_user: LDAP 用户信息 {username, email, display_name}
"""
# 先按 email 查找
user = db.query(User).filter(User.email == ldap_user["email"]).first()
if user:
# 更新 auth_source如果是首次 LDAP 登录)
if user.auth_source != AuthSource.LDAP:
user.auth_source = AuthSource.LDAP
user.last_login_at = datetime.now(timezone.utc)
db.commit()
await UserCacheService.invalidate_user_cache(user.id, user.email)
logger.info(f"LDAP 用户登录成功: {ldap_user['email']} (ID: {user.id})")
return user
# 创建新用户
user = User(
email=ldap_user["email"],
username=ldap_user["username"],
password_hash="", # LDAP 用户无本地密码
auth_source=AuthSource.LDAP,
role=UserRole.USER,
is_active=True,
last_login_at=datetime.now(timezone.utc),
)
db.add(user)
db.commit()
db.refresh(user)
logger.info(f"LDAP 用户创建成功: {ldap_user['email']} (ID: {user.id})")
return user
@staticmethod
def authenticate_api_key(db: Session, api_key: str) -> Optional[tuple[User, ApiKey]]:
"""API密钥认证"""