mirror of https://github.com/Mai-with-u/MaiBot.git
184 lines
5.8 KiB
Python
184 lines
5.8 KiB
Python
"""
|
||
WebUI 认证模块
|
||
提供统一的认证依赖,支持 Cookie 和 Header 两种方式
|
||
"""
|
||
|
||
from typing import Optional
|
||
from fastapi import HTTPException, Cookie, Header, Response, Request
|
||
from src.common.logger import get_logger
|
||
from src.config.config import global_config
|
||
from .token_manager import get_token_manager
|
||
|
||
logger = get_logger("webui.auth")
|
||
|
||
# Cookie 配置
|
||
COOKIE_NAME = "maibot_session"
|
||
COOKIE_MAX_AGE = 7 * 24 * 60 * 60 # 7天
|
||
|
||
|
||
def _is_secure_environment() -> bool:
|
||
"""
|
||
检测是否应该启用安全 Cookie(HTTPS)
|
||
|
||
Returns:
|
||
bool: 如果应该使用 secure cookie 则返回 True
|
||
"""
|
||
# 从配置读取
|
||
if global_config.webui.secure_cookie:
|
||
logger.info("配置中启用了 secure_cookie")
|
||
return True
|
||
|
||
# 检查是否是生产环境
|
||
if global_config.webui.mode == "production":
|
||
logger.info("WebUI运行在生产模式,启用 secure cookie")
|
||
return True
|
||
|
||
# 默认:开发环境不启用(因为通常是 HTTP)
|
||
logger.debug("WebUI运行在开发模式,禁用 secure cookie")
|
||
return False
|
||
|
||
|
||
def get_current_token(
|
||
request: Request,
|
||
maibot_session: Optional[str] = Cookie(None),
|
||
authorization: Optional[str] = Header(None),
|
||
) -> str:
|
||
"""
|
||
获取当前请求的 token,优先从 Cookie 获取,其次从 Header 获取
|
||
|
||
Args:
|
||
request: FastAPI Request 对象
|
||
maibot_session: Cookie 中的 token
|
||
authorization: Authorization Header (Bearer token)
|
||
|
||
Returns:
|
||
验证通过的 token
|
||
|
||
Raises:
|
||
HTTPException: 认证失败时抛出 401 错误
|
||
"""
|
||
token = None
|
||
|
||
# 优先从 Cookie 获取
|
||
if maibot_session:
|
||
token = maibot_session
|
||
# 其次从 Header 获取(兼容旧版本)
|
||
elif authorization and authorization.startswith("Bearer "):
|
||
token = authorization.replace("Bearer ", "")
|
||
|
||
if not token:
|
||
raise HTTPException(status_code=401, detail="未提供有效的认证信息")
|
||
|
||
# 验证 token
|
||
token_manager = get_token_manager()
|
||
if not token_manager.verify_token(token):
|
||
raise HTTPException(status_code=401, detail="Token 无效或已过期")
|
||
|
||
return token
|
||
|
||
|
||
def set_auth_cookie(response: Response, token: str, request: Optional[Request] = None) -> None:
|
||
"""
|
||
设置认证 Cookie
|
||
|
||
Args:
|
||
response: FastAPI Response 对象
|
||
token: 要设置的 token
|
||
request: FastAPI Request 对象(可选,用于检测协议)
|
||
"""
|
||
# 根据环境和实际请求协议决定安全设置
|
||
is_secure = _is_secure_environment()
|
||
|
||
# 如果提供了 request,检测实际使用的协议
|
||
if request:
|
||
# 检查 X-Forwarded-Proto header(代理/负载均衡器)
|
||
forwarded_proto = request.headers.get("x-forwarded-proto", "").lower()
|
||
if forwarded_proto:
|
||
is_https = forwarded_proto == "https"
|
||
logger.debug(f"检测到 X-Forwarded-Proto: {forwarded_proto}, is_https={is_https}")
|
||
else:
|
||
# 检查 request.url.scheme
|
||
is_https = request.url.scheme == "https"
|
||
logger.debug(f"检测到 scheme: {request.url.scheme}, is_https={is_https}")
|
||
|
||
# 如果是 HTTP 连接,强制禁用 secure 标志
|
||
if not is_https and is_secure:
|
||
logger.warning("=" * 80)
|
||
logger.warning("检测到 HTTP 连接但环境配置要求 HTTPS (secure cookie)")
|
||
logger.warning("已自动禁用 secure 标志以允许登录,但建议修改配置:")
|
||
logger.warning("1. 在配置文件中设置: webui.secure_cookie = false")
|
||
logger.warning("2. 如果使用反向代理,请确保正确配置 X-Forwarded-Proto 头")
|
||
logger.warning("=" * 80)
|
||
is_secure = False
|
||
|
||
# 设置 Cookie
|
||
response.set_cookie(
|
||
key=COOKIE_NAME,
|
||
value=token,
|
||
max_age=COOKIE_MAX_AGE,
|
||
httponly=True, # 防止 JS 读取,阻止 XSS 窃取
|
||
samesite="lax", # 使用 lax 以兼容更多场景(开发和生产)
|
||
secure=is_secure, # 根据实际协议决定
|
||
path="/", # 确保 Cookie 在所有路径下可用
|
||
)
|
||
|
||
logger.info(f"已设置认证 Cookie: {token[:8]}... (secure={is_secure}, samesite=lax, httponly=True, path=/, max_age={COOKIE_MAX_AGE})")
|
||
logger.debug(f"完整 token 前缀: {token[:20]}...")
|
||
|
||
|
||
def clear_auth_cookie(response: Response) -> None:
|
||
"""
|
||
清除认证 Cookie
|
||
|
||
Args:
|
||
response: FastAPI Response 对象
|
||
"""
|
||
# 保持与 set_auth_cookie 相同的安全设置
|
||
is_secure = _is_secure_environment()
|
||
|
||
response.delete_cookie(
|
||
key=COOKIE_NAME,
|
||
httponly=True,
|
||
samesite="strict" if is_secure else "lax",
|
||
secure=is_secure,
|
||
path="/",
|
||
)
|
||
logger.debug("已清除认证 Cookie")
|
||
|
||
|
||
def verify_auth_token_from_cookie_or_header(
|
||
maibot_session: Optional[str] = None,
|
||
authorization: Optional[str] = None,
|
||
) -> bool:
|
||
"""
|
||
验证认证 Token,支持从 Cookie 或 Header 获取
|
||
|
||
Args:
|
||
maibot_session: Cookie 中的 token
|
||
authorization: Authorization header (Bearer token)
|
||
|
||
Returns:
|
||
验证成功返回 True
|
||
|
||
Raises:
|
||
HTTPException: 认证失败时抛出 401 错误
|
||
"""
|
||
token = None
|
||
|
||
# 优先从 Cookie 获取
|
||
if maibot_session:
|
||
token = maibot_session
|
||
# 其次从 Header 获取(兼容旧版本)
|
||
elif authorization and authorization.startswith("Bearer "):
|
||
token = authorization.replace("Bearer ", "")
|
||
|
||
if not token:
|
||
raise HTTPException(status_code=401, detail="未提供有效的认证信息")
|
||
|
||
# 验证 token
|
||
token_manager = get_token_manager()
|
||
if not token_manager.verify_token(token):
|
||
raise HTTPException(status_code=401, detail="Token 无效或已过期")
|
||
|
||
return True
|