155 lines
4.8 KiB
Python
155 lines
4.8 KiB
Python
"""
|
||
用户登录记录服务
|
||
"""
|
||
|
||
import asyncio
|
||
|
||
from app.database.user_login_log import UserLoginLog
|
||
from app.dependencies.geoip import get_client_ip, get_geoip_helper, normalize_ip
|
||
from app.log import logger
|
||
from app.utils import utcnow
|
||
|
||
from fastapi import Request
|
||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||
|
||
|
||
class LoginLogService:
|
||
"""用户登录记录服务"""
|
||
|
||
@staticmethod
|
||
async def record_login(
|
||
db: AsyncSession,
|
||
user_id: int,
|
||
request: Request,
|
||
user_agent: str | None = None,
|
||
login_success: bool = True,
|
||
login_method: str = "password",
|
||
notes: str | None = None,
|
||
) -> UserLoginLog:
|
||
"""
|
||
记录用户登录信息
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
user_id: 用户ID
|
||
request: HTTP请求对象
|
||
login_success: 登录是否成功
|
||
login_method: 登录方式
|
||
notes: 备注信息
|
||
|
||
Returns:
|
||
UserLoginLog: 登录记录对象
|
||
"""
|
||
# 获取客户端IP并标准化格式
|
||
raw_ip = get_client_ip(request)
|
||
ip_address = normalize_ip(raw_ip)
|
||
|
||
# 创建基本的登录记录
|
||
login_log = UserLoginLog(
|
||
user_id=user_id,
|
||
ip_address=ip_address,
|
||
user_agent=user_agent,
|
||
login_time=utcnow(),
|
||
login_success=login_success,
|
||
login_method=login_method,
|
||
notes=notes,
|
||
)
|
||
|
||
# 异步获取GeoIP信息
|
||
try:
|
||
geoip = get_geoip_helper()
|
||
|
||
# 在后台线程中运行GeoIP查询(避免阻塞)
|
||
loop = asyncio.get_event_loop()
|
||
geo_info = await loop.run_in_executor(None, lambda: geoip.lookup(ip_address))
|
||
|
||
if geo_info:
|
||
login_log.country_code = geo_info.get("country_iso", "")
|
||
login_log.country_name = geo_info.get("country_name", "")
|
||
login_log.city_name = geo_info.get("city_name", "")
|
||
login_log.latitude = geo_info.get("latitude", "")
|
||
login_log.longitude = geo_info.get("longitude", "")
|
||
login_log.time_zone = geo_info.get("time_zone", "")
|
||
|
||
# 处理 ASN(可能是字符串,需要转换为整数)
|
||
asn_value = geo_info.get("asn")
|
||
if asn_value is not None:
|
||
try:
|
||
login_log.asn = int(asn_value)
|
||
except (ValueError, TypeError):
|
||
login_log.asn = None
|
||
|
||
login_log.organization = geo_info.get("organization", "")
|
||
|
||
logger.debug(f"GeoIP lookup for {ip_address}: {geo_info.get('country_name', 'Unknown')}")
|
||
else:
|
||
logger.warning(f"GeoIP lookup failed for {ip_address}")
|
||
|
||
except Exception as e:
|
||
logger.warning(f"GeoIP lookup error for {ip_address}: {e}")
|
||
|
||
# 保存到数据库
|
||
db.add(login_log)
|
||
await db.commit()
|
||
await db.refresh(login_log)
|
||
|
||
logger.info(f"Login recorded for user {user_id} from {ip_address} ({login_method})")
|
||
return login_log
|
||
|
||
@staticmethod
|
||
async def record_failed_login(
|
||
db: AsyncSession,
|
||
request: Request,
|
||
attempted_username: str | None = None,
|
||
login_method: str = "password",
|
||
notes: str | None = None,
|
||
user_agent: str | None = None,
|
||
) -> UserLoginLog:
|
||
"""
|
||
记录失败的登录尝试
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
request: HTTP请求对象
|
||
attempted_username: 尝试登录的用户名
|
||
login_method: 登录方式
|
||
notes: 备注信息
|
||
|
||
Returns:
|
||
UserLoginLog: 登录记录对象
|
||
"""
|
||
# 对于失败的登录,使用user_id=0表示未知用户
|
||
return await LoginLogService.record_login(
|
||
db=db,
|
||
user_id=0, # 0表示未知/失败的登录
|
||
request=request,
|
||
login_success=False,
|
||
login_method=login_method,
|
||
user_agent=user_agent,
|
||
notes=(
|
||
f"Failed login attempt on user {attempted_username}: {notes}"
|
||
if attempted_username
|
||
else "Failed login attempt"
|
||
),
|
||
)
|
||
|
||
|
||
def get_request_info(request: Request) -> dict:
|
||
"""
|
||
提取请求的详细信息
|
||
|
||
Args:
|
||
request: HTTP请求对象
|
||
|
||
Returns:
|
||
dict: 包含请求信息的字典
|
||
"""
|
||
return {
|
||
"ip": get_client_ip(request),
|
||
"user_agent": request.headers.get("User-Agent", ""),
|
||
"referer": request.headers.get("Referer", ""),
|
||
"accept_language": request.headers.get("Accept-Language", ""),
|
||
"x_forwarded_for": request.headers.get("X-Forwarded-For", ""),
|
||
"x_real_ip": request.headers.get("X-Real-IP", ""),
|
||
}
|