Files
DinQuant/backend_api_python/app/utils/auth.py
T

239 lines
7.0 KiB
Python
Raw Normal View History

"""
Authentication Utilities
2025-12-29 03:06:49 +08:00
JWT token generation, verification, and middleware decorators.
Supports multi-user authentication with role-based access control.
"""
2025-12-29 03:06:49 +08:00
import jwt
import datetime
import os
2025-12-29 03:06:49 +08:00
from functools import wraps
from flask import request, jsonify, g
from app.config.settings import Config
from app.utils.logger import get_logger
logger = get_logger(__name__)
2026-01-31 02:59:49 +08:00
def generate_token(user_id: int, username: str, role: str = 'user', token_version: int = 1) -> str:
"""
Generate JWT token with user information.
Args:
user_id: User ID
username: Username
role: User role (admin/manager/user/viewer)
2026-01-31 02:59:49 +08:00
token_version: Token version for single-client enforcement
Returns:
JWT token string
"""
2025-12-29 03:06:49 +08:00
try:
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7),
'iat': datetime.datetime.utcnow(),
'sub': username,
'user_id': user_id,
'role': role,
2026-01-31 02:59:49 +08:00
'token_version': token_version, # 用于单一客户端登录控制
2025-12-29 03:06:49 +08:00
}
return jwt.encode(
payload,
Config.SECRET_KEY,
algorithm='HS256'
)
except Exception as e:
logger.error(f"Token generation failed: {e}")
return None
def verify_token(token: str) -> dict:
"""
Verify JWT token and return payload.
Args:
token: JWT token string
Returns:
Token payload dict or None if invalid
"""
2025-12-29 03:06:49 +08:00
try:
payload = jwt.decode(token, Config.SECRET_KEY, algorithms=['HS256'])
2026-01-31 02:59:49 +08:00
# 验证 token_version(单一客户端登录控制)
user_id = payload.get('user_id')
token_version = payload.get('token_version')
if user_id and token_version is not None:
# 检查数据库中的 token_version 是否匹配
if not _verify_token_version(user_id, token_version):
logger.debug(f"Token version mismatch for user {user_id}: expected current, got {token_version}")
return None
return payload
2025-12-29 03:06:49 +08:00
except jwt.ExpiredSignatureError:
logger.debug("Token expired")
2025-12-29 03:06:49 +08:00
return None
except jwt.InvalidTokenError as e:
logger.debug(f"Invalid token: {e}")
2025-12-29 03:06:49 +08:00
return None
2026-01-31 02:59:49 +08:00
def _verify_token_version(user_id: int, token_version: int) -> bool:
"""
验证 token 版本是否与数据库中存储的版本匹配。
用于实现单一客户端登录(踢出重复登录)。
Args:
user_id: 用户ID
token_version: Token中的版本号
Returns:
True if version matches, False otherwise
"""
try:
from app.utils.db import get_db_connection
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"SELECT token_version FROM qd_users WHERE id = ?",
(user_id,)
)
row = cur.fetchone()
cur.close()
if not row:
return False
db_token_version = row.get('token_version') or 1
return int(token_version) == int(db_token_version)
except Exception as e:
logger.error(f"_verify_token_version failed: {e}")
# 如果验证失败,为了安全起见,返回 False
return False
def get_current_user_id() -> int:
"""Get current user ID from flask.g context"""
return getattr(g, 'user_id', None)
def get_current_user_role() -> str:
"""Get current user role from flask.g context"""
return getattr(g, 'user_role', 'user')
2025-12-29 03:06:49 +08:00
def login_required(f):
"""
Decorator that enforces Bearer token auth.
Sets g.user, g.user_id, g.user_role on successful auth.
"""
2025-12-29 03:06:49 +08:00
@wraps(f)
def decorated(*args, **kwargs):
token = None
# Read token from Authorization: Bearer <token>
auth_header = request.headers.get('Authorization')
if auth_header:
parts = auth_header.split()
if len(parts) == 2 and parts[0].lower() == 'bearer':
token = parts[1]
if not token:
return jsonify({'code': 401, 'msg': 'Token missing', 'data': None}), 401
payload = verify_token(token)
if not payload:
2025-12-29 03:06:49 +08:00
return jsonify({'code': 401, 'msg': 'Token invalid or expired', 'data': None}), 401
# Store user info in flask.g
g.user = payload.get('sub')
g.user_id = payload.get('user_id')
g.user_role = payload.get('role', 'user')
2025-12-29 03:06:49 +08:00
return f(*args, **kwargs)
return decorated
def admin_required(f):
"""
Decorator that requires admin role.
Must be used after @login_required.
"""
@wraps(f)
def decorated(*args, **kwargs):
role = getattr(g, 'user_role', None)
if role != 'admin':
return jsonify({'code': 403, 'msg': 'Admin access required', 'data': None}), 403
return f(*args, **kwargs)
return decorated
def manager_required(f):
"""
Decorator that requires manager or admin role.
Must be used after @login_required.
"""
@wraps(f)
def decorated(*args, **kwargs):
role = getattr(g, 'user_role', None)
if role not in ('admin', 'manager'):
return jsonify({'code': 403, 'msg': 'Manager access required', 'data': None}), 403
return f(*args, **kwargs)
return decorated
def permission_required(permission: str):
"""
Decorator factory that checks for a specific permission.
Must be used after @login_required.
Usage:
@login_required
@permission_required('strategy')
def my_endpoint():
...
"""
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
role = getattr(g, 'user_role', 'user')
# Import here to avoid circular import
from app.services.user_service import get_user_service
permissions = get_user_service().get_user_permissions(role)
if permission not in permissions:
return jsonify({
'code': 403,
'msg': f'Permission denied: {permission}',
'data': None
}), 403
return f(*args, **kwargs)
return decorated
return decorator
# Legacy compatibility: single-user mode fallback
def _is_single_user_mode() -> bool:
"""Check if system is in single-user (legacy) mode"""
return os.getenv('SINGLE_USER_MODE', 'false').lower() == 'true'
def authenticate_legacy(username: str, password: str) -> dict:
"""
Legacy single-user authentication (for backward compatibility).
Uses ADMIN_USER and ADMIN_PASSWORD from environment.
"""
if username == Config.ADMIN_USER and password == Config.ADMIN_PASSWORD:
return {
'user_id': 1,
'username': username,
'role': 'admin',
'nickname': 'Admin',
}
return None