2026-02-04 13:30:07 +08:00
|
|
|
|
# app/utils/decorators.py
|
|
|
|
|
|
from functools import wraps
|
2026-03-10 12:15:26 +08:00
|
|
|
|
from flask_jwt_extended import get_jwt, verify_jwt_in_request, get_jwt_identity
|
2026-04-20 15:10:12 +08:00
|
|
|
|
from flask import jsonify, g, request, current_app, has_request_context
|
2026-02-27 10:16:43 +08:00
|
|
|
|
import logging
|
2026-03-10 12:15:26 +08:00
|
|
|
|
import json
|
2026-02-04 13:30:07 +08:00
|
|
|
|
|
2026-03-20 09:11:54 +08:00
|
|
|
|
def _verify_token_in_redis():
|
|
|
|
|
|
"""
|
|
|
|
|
|
验证当前 Token 是否与 Redis 中存储的 Token 一致(单设备登录互踢)
|
|
|
|
|
|
"""
|
|
|
|
|
|
from app.extensions import redis_client
|
|
|
|
|
|
from flask import current_app
|
|
|
|
|
|
|
|
|
|
|
|
if redis_client is None:
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
auth_header = request.headers.get('Authorization', '')
|
|
|
|
|
|
if not auth_header.startswith('Bearer '):
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
2026-04-20 16:04:01 +08:00
|
|
|
|
request_token = auth_header[7:]
|
2026-03-20 09:11:54 +08:00
|
|
|
|
claims = get_jwt()
|
|
|
|
|
|
user_id = claims.get('sub')
|
|
|
|
|
|
if user_id is None:
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
stored_token = redis_client.get(f"user_token_{user_id}")
|
|
|
|
|
|
if stored_token is None:
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
if request_token != stored_token:
|
|
|
|
|
|
current_app.logger.warning(f"Token mismatch for user {user_id}: request token != stored token")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
current_app.logger.error(f"Redis token verification error: {e}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def _raise_token_mismatch_error():
|
2026-04-20 16:04:01 +08:00
|
|
|
|
"""抛出 Token 不一致的错误"""
|
2026-03-20 09:11:54 +08:00
|
|
|
|
return jsonify({
|
|
|
|
|
|
'msg': '您的账号已在其他设备登录,请重新登录',
|
|
|
|
|
|
'code': 401,
|
|
|
|
|
|
'reason': 'token_mismatch'
|
|
|
|
|
|
}), 401
|
|
|
|
|
|
|
2026-02-04 13:30:07 +08:00
|
|
|
|
def role_required(*roles):
|
2026-04-20 16:04:01 +08:00
|
|
|
|
"""自定义装饰器:检查用户角色"""
|
2026-02-04 13:30:07 +08:00
|
|
|
|
def wrapper(fn):
|
|
|
|
|
|
@wraps(fn)
|
|
|
|
|
|
def decorator(*args, **kwargs):
|
|
|
|
|
|
claims = get_jwt()
|
|
|
|
|
|
user_role = claims.get('role')
|
2026-02-27 17:07:45 +08:00
|
|
|
|
user_role_upper = user_role.upper() if user_role else None
|
2026-02-04 13:30:07 +08:00
|
|
|
|
|
2026-02-27 17:07:45 +08:00
|
|
|
|
if user_role_upper == 'SUPER_ADMIN':
|
2026-02-04 13:30:07 +08:00
|
|
|
|
return fn(*args, **kwargs)
|
|
|
|
|
|
|
2026-02-27 17:07:45 +08:00
|
|
|
|
if user_role_upper not in [r.upper() for r in roles]:
|
2026-02-04 13:30:07 +08:00
|
|
|
|
return jsonify(msg='权限不足:您没有访问此资源的权限'), 403
|
|
|
|
|
|
|
|
|
|
|
|
return fn(*args, **kwargs)
|
|
|
|
|
|
return decorator
|
2026-02-27 10:16:43 +08:00
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
def login_required(fn):
|
2026-04-20 16:04:01 +08:00
|
|
|
|
"""验证 JWT 令牌是否存在且有效"""
|
2026-02-27 10:16:43 +08:00
|
|
|
|
@wraps(fn)
|
|
|
|
|
|
def decorator(*args, **kwargs):
|
|
|
|
|
|
try:
|
|
|
|
|
|
verify_jwt_in_request()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logging.warning(f"JWT verification failed: {e}")
|
|
|
|
|
|
return jsonify(msg='登录已过期,请重新登录'), 401
|
2026-03-20 09:11:54 +08:00
|
|
|
|
|
|
|
|
|
|
if not _verify_token_in_redis():
|
|
|
|
|
|
return _raise_token_mismatch_error()
|
|
|
|
|
|
|
2026-02-27 10:16:43 +08:00
|
|
|
|
return fn(*args, **kwargs)
|
|
|
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
|
|
|
def permission_required(permission_code):
|
2026-04-20 16:04:01 +08:00
|
|
|
|
"""检查当前用户是否拥有指定权限码"""
|
2026-02-27 10:16:43 +08:00
|
|
|
|
def wrapper(fn):
|
|
|
|
|
|
@wraps(fn)
|
|
|
|
|
|
def decorator(*args, **kwargs):
|
|
|
|
|
|
try:
|
|
|
|
|
|
verify_jwt_in_request()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logging.warning(f"JWT verification failed: {e}")
|
|
|
|
|
|
return jsonify(msg='登录已过期,请重新登录'), 401
|
|
|
|
|
|
|
2026-03-20 09:11:54 +08:00
|
|
|
|
if not _verify_token_in_redis():
|
|
|
|
|
|
return _raise_token_mismatch_error()
|
|
|
|
|
|
|
2026-02-27 10:16:43 +08:00
|
|
|
|
claims = get_jwt()
|
|
|
|
|
|
user_role = claims.get('role')
|
2026-02-27 17:04:22 +08:00
|
|
|
|
if user_role and user_role.upper() == 'SUPER_ADMIN':
|
2026-02-27 10:16:43 +08:00
|
|
|
|
return fn(*args, **kwargs)
|
|
|
|
|
|
|
2026-02-27 10:20:09 +08:00
|
|
|
|
try:
|
|
|
|
|
|
from app.services.auth_service import AuthService
|
|
|
|
|
|
perm_dict = AuthService.get_user_permissions(user_role)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logging.warning(f"Failed to fetch permissions for role {user_role}: {e}")
|
|
|
|
|
|
return jsonify(msg='权限查询失败'), 403
|
2026-02-27 10:16:43 +08:00
|
|
|
|
|
2026-02-27 10:20:09 +08:00
|
|
|
|
all_perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
|
|
|
|
|
if permission_code not in all_perms:
|
2026-04-20 16:04:01 +08:00
|
|
|
|
logging.warning(f"权限检查失败: 角色={user_role}, 所需权限={permission_code}")
|
2026-02-27 10:20:09 +08:00
|
|
|
|
return jsonify(msg='权限不足:您没有访问此资源的权限'), 403
|
|
|
|
|
|
return fn(*args, **kwargs)
|
2026-02-27 10:16:43 +08:00
|
|
|
|
return decorator
|
|
|
|
|
|
return wrapper
|
2026-03-10 12:15:26 +08:00
|
|
|
|
|
2026-04-20 16:04:01 +08:00
|
|
|
|
def audit_log(module: str = None, action: str = None, get_target_id_fn=None, get_target_name_fn=None, get_details_fn=None):
|
2026-03-10 12:15:26 +08:00
|
|
|
|
"""
|
2026-04-20 16:04:01 +08:00
|
|
|
|
已废弃!
|
|
|
|
|
|
由 SQLAlchemy 底层监听器(app/core/audit_listener.py)全面接管审计日志入库。
|
|
|
|
|
|
此装饰器保留空壳以防项目中其他文件 import 引用时报错。
|
2026-03-10 12:15:26 +08:00
|
|
|
|
"""
|
|
|
|
|
|
def wrapper(fn):
|
2026-04-20 16:04:01 +08:00
|
|
|
|
from functools import wraps
|
2026-03-10 12:15:26 +08:00
|
|
|
|
@wraps(fn)
|
2026-04-20 16:04:01 +08:00
|
|
|
|
def decorator(*inner_args, **inner_kwargs):
|
|
|
|
|
|
return fn(*inner_args, **inner_kwargs)
|
2026-03-10 12:15:26 +08:00
|
|
|
|
return decorator
|
2026-04-20 16:04:01 +08:00
|
|
|
|
return wrapper
|