feat: 添加 OAuth 认证支持及相关改进

- 新增 OAuth 模块,支持 LinuxDo/GitHub/Google 等第三方登录
- 用户邮箱改为可选字段,支持无邮箱注册
- 新增模块配置验证状态 (config_validated/config_error)
- 系统设置界面改为分块独立保存
- 用户设置新增 OAuth 绑定管理和首次密码设置
- 登录界面支持 OAuth 按钮展示
- 邮箱验证设置移至邮件设置页面
This commit is contained in:
fawney19
2026-01-19 03:19:17 +08:00
parent e2e14fd09c
commit 3d88dfd98a
61 changed files with 4548 additions and 950 deletions

View File

@@ -59,16 +59,12 @@ async def get_current_user(
raise ForbiddenException("无效的Token")
user_id = payload.get("user_id")
token_email = payload.get("email")
token_created_at = payload.get("created_at")
if not user_id:
logger.error(f"Token缺少user_id字段: payload={payload}")
logger.error("Token缺少user_id字段: payload={}", payload)
raise ForbiddenException("无效的认证凭据")
if not token_email:
logger.error(f"Token缺少email字段: payload={payload}")
raise ForbiddenException("无效的认证凭据")
# 兼容旧 tokenemail 字段可能存在;新 token 不再包含 email支持无邮箱用户
# 仅在DEBUG模式下记录详细信息
token_fp = hashlib.sha256(token.encode()).hexdigest()[:12]
@@ -76,7 +72,7 @@ async def get_current_user(
# 确保user_id是字符串格式UUID
if not isinstance(user_id, str):
logger.error(f"Token中user_id格式错误: {type(user_id)} - {user_id}")
logger.error("Token中user_id格式错误: {} - {}", type(user_id), user_id)
raise ForbiddenException("认证信息格式错误,请重新登录")
# 使用新的数据库会话获取用户,避免会话状态问题
@@ -85,46 +81,35 @@ async def get_current_user(
user = UserService.get_user(db, user_id)
except Exception as db_error:
logger.error(f"数据库查询失败: user_id={user_id}, error={db_error}")
logger.error("数据库查询失败: user_id={}, error={}", user_id, db_error)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="数据库查询失败,请稍后重试",
)
if not user:
logger.error(f"用户不存在: user_id={user_id}")
logger.error("用户不存在: user_id={}", user_id)
raise ForbiddenException("用户不存在或已禁用")
if not user.is_active:
logger.error(f"用户已禁用: user_id={user_id}")
logger.error("用户已禁用: user_id={}", user_id)
raise ForbiddenException("用户不存在或已禁用")
# 验证邮箱是否匹配防止用户ID重用导致的身份混淆
if user.email != token_email:
logger.error(f"Token邮箱不匹配: Token中的邮箱={token_email}, 数据库中的邮箱={user.email}")
if user.is_deleted:
logger.error("用户已删除: user_id={}", user_id)
raise ForbiddenException("用户不存在或已禁用")
if not AuthService.token_identity_matches_user(payload, user):
logger.error("Token身份校验失败: user_id={}, token_fp={}", user_id, token_fp)
raise ForbiddenException("身份验证失败")
# 验证用户创建时间是否匹配防止ID重用
if token_created_at and user.created_at:
try:
from datetime import datetime
token_created = datetime.fromisoformat(token_created_at.replace("Z", "+00:00"))
# 允许1秒的时间差异考虑到时间精度问题
time_diff = abs((user.created_at - token_created).total_seconds())
if time_diff > 1:
logger.error(f"Token创建时间不匹配: Token时间={token_created_at}, 用户创建时间={user.created_at}")
raise ForbiddenException("身份验证失败")
except ValueError as e:
logger.warning(f"Token时间格式解析失败: {e}")
logger.debug(f"成功获取用户: user_id={user_id}, email={user.email}")
logger.debug("成功获取用户: user_id={}, email={}", user_id, user.email)
return user
except HTTPException:
raise
except Exception as e:
logger.error(f"认证失败,未预期的错误: {e}")
logger.error("认证失败,未预期的错误: {}", e)
# 返回500而不是401避免触发前端的退出逻辑
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="认证服务暂时不可用"
@@ -166,6 +151,12 @@ async def get_current_user_from_header(
if not user.is_active:
raise ForbiddenException("用户已被禁用")
if user.is_deleted:
raise ForbiddenException("用户不存在或已禁用")
if not AuthService.token_identity_matches_user(payload, user):
raise ForbiddenException("身份验证失败")
return user
except HTTPException:
# 保持原始的HTTPException (包括401)