feat: 账套模式后端 — token绑定entity_id + user_entities授权表 + 解析链倒置

- auth_middleware: create_token存JSON{user_id,entity_id},旧int格式token强制下线
- models: 新增UserEntity授权表(user_id↔entity_id多对多,唯一约束)
- database: init_db自动建表+存量用户×active企业默认授权(平滑迁移)
- deps: get_entity_id解析链倒置 token优先 → Bot白名单(query/header校验entity active) → 默认1
- auth: login加entity_id+授权校验; 新增switch-entity/my-entities/登录页entities接口; register自动授权
This commit is contained in:
Hermes CI Fix
2026-08-11 11:24:52 +08:00
parent 9d0f070c14
commit b31f9b80c4
5 changed files with 336 additions and 20 deletions
+45 -11
View File
@@ -1,26 +1,60 @@
"""多租户公共依赖 — 从请求头/参数读取当前企业ID"""
from fastapi import Request, Header, Query, Depends
"""多租户公共依赖 — 账套模式:token优先,query/header降级为Bot服务白名单"""
from fastapi import Request, Header, Query, Depends, HTTPException
from typing import Optional
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth_middleware import get_token_entity_id
from app.models import Entity
def get_entity_id(
request: Request,
x_entity_id: Optional[str] = Header(None, alias="X-Entity-Id"),
entity_id: Optional[int] = Query(None, ge=1),
db: Session = Depends(get_db),
) -> int:
"""解析当前企业ID:优先query参数 → header → 默认1(酣客)
前端拦截器已统一附加 X-Entity-Id header 和 entity_id query参数
"""解析当前企业ID(账套模式):token优先 → query/headerBot白名单)→ 默认1
解析链(倒置后):
1. Authorization Bearer token → 优先返回 token.entity_id(唯一可信来源)
2. query参数 / X-Entity-Id header → 仅Bot服务通道使用(校验entity状态active
3. body中的 entity_idPOST场景)→ Bot服务通道兼容
4. 兜底默认 1(酣客)
安全说明:登录用户必须通过token绑定账套,query/header传入的entity_id
在token存在时被忽略,防止越权传参(旧漏洞:query优先且无授权校验)。
"""
# 1. token优先(账套模式唯一来源)
auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer "):
token_entity = get_token_entity_id(auth[7:])
if token_entity is not None:
return token_entity
# token存在但是旧格式/无entity → 账套模式下强制走白名单或默认(由require_auth拦截)
# 这里不抛401:公开接口可能带旧token,交给require_auth统一处理
# 2. query/header → Bot服务通道白名单(校验entity状态active
candidate = None
if entity_id is not None:
return entity_id
if x_entity_id and x_entity_id.isdigit():
return int(x_entity_id)
# 兼容 body 中的 entity_idPOST场景)
if request.method in ("POST", "PUT", "PATCH"):
candidate = entity_id
elif x_entity_id and x_entity_id.isdigit():
candidate = int(x_entity_id)
# 3. body 中的 entity_idPOST场景,Bot通道兼容)
if candidate is None and request.method in ("POST", "PUT", "PATCH"):
try:
body = request.state.body_json or {}
if body.get("entity_id"):
return int(body["entity_id"])
candidate = int(body["entity_id"])
except Exception:
pass
return 1 # 默认酣客
if candidate is not None:
ent = db.query(Entity).filter(Entity.id == candidate).first()
if ent and ent.status == "active":
return candidate
# Bot通道校验不通过:不返回默认,直接拒绝(防越权注入无效entity)
raise HTTPException(403, f"企业 entity_id={candidate} 不存在或未激活")
return 1 # 默认酣客(无token、无参数时兜底,兼容存量公开接口)