feat: implement JWT auth and optimize token validation

- Separate JWT login (21d) from check-in token
- Unify check-in token validation with verify_checkin_authorization()
- Update API docs for dual-token architecture
This commit is contained in:
2026-01-05 23:02:50 +08:00
parent b32b53853a
commit a9b141fc69
13 changed files with 464 additions and 336 deletions
+128 -40
View File
@@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
from backend.models import User
from backend.workers.token_refresher import get_token_headless, get_session_data
from backend.config import settings
from backend.utils.jwt import JWTManager
logger = logging.getLogger(__name__)
@@ -208,10 +209,13 @@ class AuthService:
logger.info(f"更新老用户 {user.alias} 的 Token")
# 生成 JWT access token(用于网站登录)
access_token = JWTManager.create_access_token(user.id, user.alias)
return {
"status": "success",
"message": "登录成功",
"token": pure_token, # 返回清理后的 token
"token": access_token, # 返回 JWT token(用于网站登录)
"user": {
"id": user.id,
"alias": user.alias,
@@ -270,10 +274,13 @@ class AuthService:
except Exception as e:
logger.error(f"发送注册通知邮件失败: {e}")
# 生成 JWT access token(用于网站登录)
access_token = JWTManager.create_access_token(new_user.id, new_user.alias)
return {
"status": "success",
"message": "注册成功,请等待管理员审批(24小时内)",
"token": pure_token, # 返回清理后的 token
"token": access_token, # 返回 JWT token(用于网站登录)
"user": {
"id": new_user.id,
"alias": new_user.alias,
@@ -299,58 +306,134 @@ class AuthService:
@staticmethod
def verify_token(authorization: str, db: Session) -> Dict[str, Any]:
"""
验证 Token 有效性
验证 JWT Token 有效性
Args:
authorization: Token
authorization: JWT Token(可带或不带 "Bearer " 前缀)
db: 数据库会话
Returns:
包含验证结果的字典
"""
from backend.utils.jwt import JWTManager
# 移除 "Bearer " 前缀
token = authorization.replace("Bearer ", "") if authorization.startswith("Bearer ") else authorization
# 从数据库查询用户
user = db.query(User).filter(User.authorization == token).first()
try:
# 验证 JWT token
payload = JWTManager.verify_token(token)
user_id = payload.get("user_id")
if not user:
if not user_id:
return {
"is_valid": False,
"message": "Token 格式错误"
}
# 从数据库获取用户
user = db.query(User).filter(User.id == user_id).first()
if not user:
return {
"is_valid": False,
"message": "用户不存在"
}
return {
"is_valid": True,
"message": "Token 有效",
"user_id": user.id,
"alias": user.alias,
"role": user.role,
"is_approved": user.is_approved
}
except pyjwt.ExpiredSignatureError:
return {
"is_valid": False,
"message": "Token 无效"
"message": "JWT Token 已过期"
}
except pyjwt.InvalidTokenError:
return {
"is_valid": False,
"message": "JWT Token 无效"
}
except Exception as e:
logger.error(f"验证 JWT Token 失败: {str(e)}")
return {
"is_valid": False,
"message": "Token 验证失败"
}
@staticmethod
def verify_checkin_authorization(user: User) -> Dict[str, Any]:
"""
验证打卡业务 authorization token 的有效性
注意:这与 JWT token 验证不同
- JWT token 用于网站登录认证
- authorization token 用于打卡业务操作(存储在 User.authorization
Args:
user: 用户对象
Returns:
包含打卡 token 验证结果的字典
"""
# 检查是否有 authorization token
if not user.authorization or user.authorization == "":
return {
"is_valid": False,
"message": "未设置打卡凭证",
"reason": "no_token"
}
# 检查 Token 是否过期
if user.jwt_exp and user.jwt_exp != "0":
try:
exp_timestamp = int(user.jwt_exp)
current_timestamp = int(datetime.now().timestamp())
if not user.jwt_exp or user.jwt_exp == "0":
return {
"is_valid": False,
"message": "打卡凭证无效",
"reason": "invalid_expiry"
}
if current_timestamp > exp_timestamp:
return {
"is_valid": False,
"message": "Token 已过期",
"user_id": user.id
}
# 计算剩余天数
days_until_expiry = (exp_timestamp - current_timestamp) // 86400
try:
exp_timestamp = int(user.jwt_exp)
current_timestamp = int(datetime.now().timestamp())
if current_timestamp > exp_timestamp:
days_expired = (current_timestamp - exp_timestamp) // 86400
return {
"is_valid": True,
"message": "Token 有效",
"user_id": user.id,
"days_until_expiry": days_until_expiry
"is_valid": False,
"message": f"打卡凭证已过期 {days_expired}",
"reason": "expired",
"days_expired": days_expired
}
except ValueError:
logger.error(f"用户 {user.id} 的 jwt_exp 格式不正确: {user.jwt_exp}")
# 计算剩余时间
seconds_remaining = exp_timestamp - current_timestamp
days_remaining = seconds_remaining // 86400
minutes_remaining = seconds_remaining // 60
return {
"is_valid": True,
"message": "Token 有效",
"user_id": user.id
}
# 判断是否即将过期(30分钟内)
expiring_soon = minutes_remaining <= 30
return {
"is_valid": True,
"message": "打卡凭证有效",
"days_remaining": days_remaining,
"minutes_remaining": minutes_remaining,
"expiring_soon": expiring_soon,
"expires_at": exp_timestamp
}
except ValueError:
logger.error(f"用户 {user.id} 的 jwt_exp 格式不正确: {user.jwt_exp}")
return {
"is_valid": False,
"message": "打卡凭证格式错误",
"reason": "invalid_format"
}
@staticmethod
def alias_login(alias: str, password: str, db: Session) -> Dict[str, Any]:
@@ -422,23 +505,28 @@ class AuthService:
# 登录成功
logger.info(f"✅ 用户 {alias} (ID: {user.id}) 别名登录成功")
# 生成 JWT access token(用于网站登录)
access_token = JWTManager.create_access_token(user.id, user.alias)
result = {
"success": True,
"message": "登录成功",
"user_id": user.id,
"authorization": user.authorization,
"alias": user.alias,
"role": user.role,
"is_approved": user.is_approved
"token": access_token, # 返回 JWT token(用于网站登录)
"user": {
"id": user.id,
"alias": user.alias,
"role": user.role,
"is_approved": user.is_approved
}
}
# 如果 Token 有问题,添加警告信息
# 如果打卡 Token 有问题,添加警告信息(不影响网站使用)
if token_warning:
result["token_warning"] = token_warning
if token_warning == "token_invalid":
result["warning_message"] = "登录成功,但检测到登录凭证无效,部分功能可能受限,建议扫码更新"
result["warning_message"] = "登录成功,但检测到打卡凭证无效,无法自动打卡,建议扫码更新"
elif token_warning == "token_expired":
result["warning_message"] = "登录成功,但检测到登录凭证已过期,部分功能可能受限,建议扫码更新"
result["warning_message"] = "登录成功,但检测到打卡凭证已过期,无法自动打卡,建议扫码更新"
return result
+51 -60
View File
@@ -122,10 +122,10 @@ class CheckInService:
"""
logger.info(f"🚀 启动异步打卡 - 任务: {task.name or f'Task-{task.id}'} (ID: {task.id})")
# 获取用户的 Token
# 获取用户的打卡 Token
user = task.user
if not user or not user.authorization:
error_msg = f"用户没有有效的 Token"
error_msg = f"用户没有有效的打卡 Token"
logger.error(f"{error_msg} - Task ID: {task.id}")
# 创建失败记录
@@ -147,35 +147,31 @@ class CheckInService:
"message": error_msg
}
# 检查 Token 是否过期
if user.jwt_exp and user.jwt_exp != "0":
try:
exp_timestamp = int(user.jwt_exp)
current_timestamp = int(datetime.now().timestamp())
if current_timestamp > exp_timestamp:
error_msg = f"Token 已过期"
logger.warning(f"{error_msg} - Task ID: {task.id}")
# 使用统一的打卡 Token 验证方法
from backend.services.auth_service import AuthService
token_result = AuthService.verify_checkin_authorization(user)
record = CheckInRecord(
task_id=task.id,
status="failure",
response_text="",
error_message=f"{error_msg},请重新扫码登录",
location="{}",
trigger_type=trigger_type
)
db.add(record)
db.commit()
db.refresh(record)
if not token_result["is_valid"]:
error_msg = token_result["message"]
logger.warning(f"{error_msg} - Task ID: {task.id}")
return {
"record_id": record.id,
"status": "failure",
"message": f"{error_msg},请重新扫码登录"
}
except ValueError as e:
# jwt_exp 格式不正确,记录警告后跳过 Token 过期验证
logger.warning(f"任务 {task.id} 的用户 jwt_exp 格式不正确: {user.jwt_exp}, 错误: {e}")
record = CheckInRecord(
task_id=task.id,
status="failure",
response_text="",
error_message=f"{error_msg},请重新扫码登录",
location="{}",
trigger_type=trigger_type
)
db.add(record)
db.commit()
db.refresh(record)
return {
"record_id": record.id,
"status": "failure",
"message": f"{error_msg},请重新扫码登录"
}
# 创建待处理记录
record_id = CheckInService.create_pending_check_in_record(task, trigger_type, db)
@@ -212,10 +208,10 @@ class CheckInService:
"""
logger.info(f"🎯 开始打卡 - 任务: {task.name or f'Task-{task.id}'} (ID: {task.id}), 触发: {trigger_type}")
# 获取用户的 Token
# 获取用户的打卡 Token
user = task.user
if not user or not user.authorization:
error_msg = f"用户没有有效的 Token"
error_msg = f"用户没有有效的打卡 Token"
logger.error(f"{error_msg} - Task ID: {task.id}, User ID: {user.id if user else 'None'}")
# 记录失败
@@ -237,37 +233,32 @@ class CheckInService:
"record_id": record.id
}
# 检查 Token 是否过期
if user.jwt_exp and user.jwt_exp != "0":
try:
exp_timestamp = int(user.jwt_exp)
current_timestamp = int(datetime.now().timestamp())
if current_timestamp > exp_timestamp:
error_msg = f"Token 已过期"
expires_at = datetime.fromtimestamp(exp_timestamp)
logger.warning(f"{error_msg} - 过期时间: {expires_at}, 用户: {user.alias}, Task ID: {task.id}")
# 使用统一的打卡 Token 验证方法
from backend.services.auth_service import AuthService
token_result = AuthService.verify_checkin_authorization(user)
# 记录失败
record = CheckInRecord(
task_id=task.id,
status="failure",
response_text="",
error_message=error_msg,
location="{}",
trigger_type=trigger_type
)
db.add(record)
db.commit()
db.refresh(record)
if not token_result["is_valid"]:
error_msg = token_result["message"]
logger.warning(f"{error_msg} - 用户: {user.alias}, Task ID: {task.id}")
return {
"success": False,
"message": f"{error_msg},请重新扫码登录",
"record_id": record.id
}
except ValueError as e:
# jwt_exp 格式不正确,记录警告后跳过 Token 过期验证
logger.warning(f"任务 {task.id} 的用户 jwt_exp 格式不正确: {user.jwt_exp}, 错误: {e}")
# 记录失败
record = CheckInRecord(
task_id=task.id,
status="failure",
response_text="",
error_message=error_msg,
location="{}",
trigger_type=trigger_type
)
db.add(record)
db.commit()
db.refresh(record)
return {
"success": False,
"message": f"{error_msg},请重新扫码登录",
"record_id": record.id
}
# 执行打卡(传递 task 对象和用户 token)
logger.info(f"🤖 调用 Selenium Worker 执行打卡...")
+53 -54
View File
@@ -149,12 +149,12 @@ def cleanup_expired_pending_users():
def check_token_expiration():
"""
检查 Token 是否即将过期,并发送邮件提醒
检查打卡 Token 是否即将过期,并发送邮件提醒
检查所有用户的 Token,如果在 30 分钟内过期,发送提醒邮件
注意:现在需要检查用户的任务,因为邮箱地址在任务中
检查所有用户的打卡 authorization token,如果在 30 分钟内过期,发送提醒邮件
注意:检查的是打卡业务 token,不是网站登录 JWT token
"""
logger.info("Scheduler: 正在执行 Token 过期检查...")
logger.info("Scheduler: 正在执行打卡 Token 过期检查...")
try:
# 创建数据库会话
@@ -162,70 +162,69 @@ def check_token_expiration():
try:
# 获取所有用户
from backend.services.auth_service import AuthService
users = db.query(User).all()
current_timestamp = int(datetime.now().timestamp())
notified_count = 0
for user in users:
if not user.jwt_exp or user.jwt_exp == "0":
# 使用统一的验证方法
result = AuthService.verify_checkin_authorization(user)
# 获取过期时间戳和剩余时间
exp_timestamp = result.get("expires_at")
if not exp_timestamp:
continue
try:
exp_timestamp = int(user.jwt_exp)
time_until_expiry = exp_timestamp - current_timestamp
# 检查 Token 状态并发送对应的提醒
time_until_expiry = exp_timestamp - current_timestamp
# 情况1:Token 即将过期(过期前 30 分钟内,且还未过期)
if 0 < time_until_expiry < 1800: # 30分钟 = 1800秒
if user.email and not user.token_expiring_notified:
logger.info(f"用户 {user.alias} 的打卡 Token 即将过期,发送邮件提醒到 {user.email}...")
from backend.services.email_service import EmailService
jwt_exp_value = user.jwt_exp
jwt_exp_str = str(jwt_exp_value) if jwt_exp_value is not None else "0"
# 情况1:Token 即将过期(过期前 30 分钟内,且还未过期)
if 0 < time_until_expiry < 1800: # 30分钟 = 1800秒
if user.email and not user.token_expiring_notified:
logger.info(f"用户 {user.alias} 的 Token 即将过期,发送邮件提醒到 {user.email}...")
from backend.services.email_service import EmailService
jwt_exp_value = user.jwt_exp
jwt_exp_str = str(jwt_exp_value) if jwt_exp_value is not None else "0"
# 发送"即将过期"邮件
success = EmailService.notify_token_expiring(user, jwt_exp_str)
# 发送"即将过期"邮件
success = EmailService.notify_token_expiring(user, jwt_exp_str)
if success:
user.token_expiring_notified = True
db.commit()
notified_count += 1
logger.info(f"用户 {user.alias} 的 Token 即将过期邮件已发送并标记")
else:
logger.warning(f"用户 {user.alias} 的 Token 即将过期邮件发送失败")
# 情况2:Token 已过期(过期后 30 分钟内)
elif -1800 < time_until_expiry <= 0: # 过期后 30 分钟内
if user.email and not user.token_expired_notified:
logger.info(f"用户 {user.alias} 的 Token 已过期,发送邮件提醒到 {user.email}...")
from backend.services.email_service import EmailService
# 发送"已过期"邮件(可以使用不同的邮件模板或内容)
success = EmailService.notify_token_expired(user)
if success:
user.token_expired_notified = True
db.commit()
notified_count += 1
logger.info(f"用户 {user.alias} 的 Token 已过期邮件已发送并标记")
else:
logger.warning(f"用户 {user.alias} 的 Token 已过期邮件发送失败")
# 情况3Token 正常(剩余时间 > 30 分钟),重置提醒标志
elif time_until_expiry >= 1800:
if user.token_expiring_notified or user.token_expired_notified:
user.token_expiring_notified = False
user.token_expired_notified = False
if success:
user.token_expiring_notified = True
db.commit()
logger.info(f"用户 {user.alias} 的 Token 已刷新,重置所有提醒标志")
notified_count += 1
logger.info(f"用户 {user.alias} 的打卡 Token 即将过期邮件已发送并标记")
else:
logger.warning(f"用户 {user.alias} 的打卡 Token 即将过期邮件发送失败")
except ValueError:
logger.warning(f"用户 {user.alias} 的 jwt_exp 格式不正确: {user.jwt_exp}")
continue
# 情况2:Token 已过期(过期后 30 分钟内)
elif -1800 < time_until_expiry <= 0: # 过期后 30 分钟内
if user.email and not user.token_expired_notified:
logger.info(f"用户 {user.alias} 的打卡 Token 已过期,发送邮件提醒到 {user.email}...")
from backend.services.email_service import EmailService
logger.info(f"Scheduler: Token 过期检查完成,共发送 {notified_count} 封提醒邮件")
# 发送"已过期"邮件
success = EmailService.notify_token_expired(user)
if success:
user.token_expired_notified = True
db.commit()
notified_count += 1
logger.info(f"用户 {user.alias} 的打卡 Token 已过期邮件已发送并标记")
else:
logger.warning(f"用户 {user.alias} 的打卡 Token 已过期邮件发送失败")
# 情况3Token 正常(剩余时间 > 30 分钟),重置提醒标志
elif time_until_expiry >= 1800:
if user.token_expiring_notified or user.token_expired_notified:
user.token_expiring_notified = False
user.token_expired_notified = False
db.commit()
logger.info(f"用户 {user.alias} 的打卡 Token 已刷新,重置所有提醒标志")
logger.info(f"Scheduler: 打卡 Token 过期检查完成,共发送 {notified_count} 封提醒邮件")
finally:
db.close()