105 lines
3.5 KiB
Python
105 lines
3.5 KiB
Python
"""权限检查中间件(数据库驱动 + Redis 缓存)"""
|
||
import json
|
||
import logging
|
||
from fastapi import HTTPException, Depends, Header
|
||
from sqlalchemy.orm import Session
|
||
from sqlalchemy import text
|
||
|
||
logger = logging.getLogger(__name__)
|
||
import redis
|
||
|
||
from app.core.database import get_db
|
||
from app.core.security import verify_token
|
||
from app.core.config import settings
|
||
|
||
_redis_client = None
|
||
|
||
|
||
def get_redis() -> redis.Redis:
|
||
global _redis_client
|
||
if _redis_client is None:
|
||
_redis_client = redis.from_url(settings.REDIS_URL, decode_responses=True)
|
||
return _redis_client
|
||
|
||
|
||
def get_role_permissions(role: str, db: Session) -> list:
|
||
"""从数据库加载角色权限,结果缓存到 Redis(TTL 5分钟)"""
|
||
if role == 'admin':
|
||
return ['*']
|
||
|
||
r = get_redis()
|
||
cache_key = f"permissions:role:{role}"
|
||
cached = r.get(cache_key)
|
||
if cached:
|
||
return json.loads(cached)
|
||
|
||
rows = db.execute(
|
||
text("""
|
||
SELECT p.code FROM permissions p
|
||
JOIN role_permissions rp ON rp.permission_id = p.id
|
||
WHERE rp.role = :role
|
||
"""),
|
||
{"role": role}
|
||
).fetchall()
|
||
perms = [row[0] for row in rows]
|
||
|
||
r.setex(cache_key, 300, json.dumps(perms))
|
||
return perms
|
||
|
||
|
||
def invalidate_role_cache(role: str) -> None:
|
||
"""修改角色权限后清除缓存"""
|
||
get_redis().delete(f"permissions:role:{role}")
|
||
|
||
|
||
def require_permission(permission: str):
|
||
"""FastAPI Depends 工厂,检查 Bearer token 中的角色是否拥有指定权限"""
|
||
def dependency(
|
||
authorization: str = Header(None, alias="Authorization"),
|
||
db: Session = Depends(get_db),
|
||
) -> dict:
|
||
if not authorization:
|
||
logger.warning("auth rejected: 缺少 Authorization 头 (permission=%s)", permission)
|
||
raise HTTPException(status_code=401, detail="未授权")
|
||
if not authorization.startswith("Bearer "):
|
||
logger.warning("auth rejected: Authorization 格式错误 (permission=%s): %.50s", permission, authorization)
|
||
raise HTTPException(status_code=401, detail="未授权")
|
||
token = authorization[7:]
|
||
payload = verify_token(token)
|
||
if not payload:
|
||
logger.warning("auth rejected: token 验证失败 (permission=%s)", permission)
|
||
raise HTTPException(status_code=401, detail="令牌无效或已过期")
|
||
|
||
role = payload.get('role', 'user')
|
||
perms = get_role_permissions(role, db)
|
||
|
||
if '*' not in perms and permission not in perms:
|
||
raise HTTPException(status_code=403, detail="权限不足")
|
||
|
||
# 附加用户的区域/学校分配信息,供数据范围过滤使用
|
||
user_id = payload.get('sub')
|
||
if user_id and role in ('area_admin', 'school_admin'):
|
||
from app.models.user import User
|
||
user = db.query(User).filter(User.id == int(user_id)).first()
|
||
if user:
|
||
payload['assigned_area'] = user.assigned_area
|
||
payload['assigned_school'] = user.assigned_school
|
||
|
||
return payload
|
||
|
||
return dependency
|
||
|
||
|
||
def get_current_user(
|
||
authorization: str = Header(None, alias="Authorization"),
|
||
db: Session = Depends(get_db),
|
||
) -> dict:
|
||
"""仅验证登录状态,不检查具体权限"""
|
||
if not authorization or not authorization.startswith("Bearer "):
|
||
raise HTTPException(status_code=401, detail="未授权")
|
||
token = authorization[7:]
|
||
payload = verify_token(token)
|
||
if not payload:
|
||
raise HTTPException(status_code=401, detail="令牌无效或已过期")
|
||
return payload
|