This commit is contained in:
zlwu committed 2026-05-12 22:07:53 +08:00
1 parent c87732fcd5
commit 058811ae77
107 files changed
+16983 -12758

No files matched your search

+48 -2
View File
@@ -6,7 +6,7 @@ import json
import logging
import time
from typing import Any, Optional, Tuple
from urllib.parse import urlencode
from urllib.parse import parse_qsl, urlencode
import redis.asyncio as redis
from jose import jwt
@@ -102,10 +102,56 @@ def _json_error_response(code: int, message: str) -> Response:
return Response(content=json.dumps(body, ensure_ascii=False), media_type="application/json", status_code=200)
async def _coerce_post_method_override_to_get(request: Request, call_next) -> Response:
"""前端 Axios 在开启加密时会把 GET 改为 POST 并带 X-HTTP-Method-Override: GET。
当后端未启用 API_ENCRYPT_ENABLED 时,原逻辑直接放行,路由仍按 POST 匹配,导致仅注册了 GET 的接口返回 405。
此处在不解密的情况下,将此类请求还原为 GET(空或明文 JSON 体);若仍为 enc=1 密文则提示配置不一致。
"""
if request.method != "POST":
return await call_next(request)
override = (request.headers.get("x-http-method-override") or "").upper()
if override != "GET":
return await call_next(request)
body_bytes = await request.body()
req_json: dict = {}
if body_bytes:
try:
raw = json.loads(body_bytes.decode("utf-8"))
req_json = raw if isinstance(raw, dict) else {}
except Exception:
req_json = {}
if int(req_json.get("enc", 0) or 0) == 1:
return _json_error_response(
400,
"客户端已开启接口加密(enc=1),但服务端未启用 API_ENCRYPT_ENABLED;"
"请在后端开启加密并配置 API_ENCRYPT_FIXED_SECRET,或将前端 VITE_API_ENCRYPT_ENABLED 设为 false。",
)
scope = dict(request.scope)
scope["method"] = "GET"
payload = req_json if isinstance(req_json, dict) else {}
q0 = (request.scope.get("query_string") or b"").decode("utf-8")
pairs = dict(parse_qsl(q0, keep_blank_values=True))
for k, v in payload.items():
if isinstance(v, (dict, list)):
continue
pairs[k] = "" if v is None else str(v)
scope["query_string"] = urlencode(pairs, doseq=True).encode("utf-8") if pairs else b""
async def receive():
return {"type": "http.request", "body": b"", "more_body": False}
new_request = Request(scope, receive)
return await call_next(new_request)
class ApiCryptoMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if not settings.API_ENCRYPT_ENABLED:
return await call_next(request)
return await _coerce_post_method_override_to_get(request, call_next)
if not settings.API_ENCRYPT_FIXED_SECRET:
if settings.API_ENCRYPT_FAIL_OPEN:
logger.warning("api crypto bypassed: API_ENCRYPT_FIXED_SECRET is empty")
+17 -11
View File
@@ -12,10 +12,9 @@ from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.database import get_db
from app.core.security import verify_password
from app.models.models import SysUser
from app.crud.user import user_crud
from app.core.utils import generate_uuid
from app.services.web_auth_roles import resolve_web_role_for_user
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/public/login")
@@ -197,27 +196,34 @@ async def get_current_user_by_token(
)
user = user_crud.get(db, user_id)
if not user or user.is_del == 1:
if not user or getattr(user, "del_flag", 0) != 0:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户不存在或已删除",
)
if not user.is_active:
if getattr(user, "status", "1") != "1":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户已被禁用",
)
web_role = resolve_web_role_for_user(db, user.id)
phone = (getattr(user, "phone_no", None) or "").strip() or None
email = (getattr(user, "email", None) or "").strip() or None
avatar = getattr(user, "avatar", None)
return {
"user_id": user.id,
"username": user.username,
"real_name": user.real_name,
"role": user.role,
"region_id": user.region_id,
"phone": getattr(user, "phone", None),
"avatar_url": getattr(user, "avatar_url", None),
"is_active": getattr(user, "is_active", True),
"username": user.user_no,
"real_name": user.nick_name,
"role": web_role,
"region_id": user.dept_id,
"phone": phone,
"email": email,
"avatar_url": avatar,
"is_active": user.status == "1",
}
+7 -2
View File
@@ -8,12 +8,12 @@ class Settings(BaseSettings):
"""应用配置"""
# 应用基础配置
APP_NAME: str = "中科兴皖一体化门户"
APP_NAME: str = "zkxw 管理骨架"
DEBUG: bool = True
API_V1_PREFIX: str = "/api"
# 数据库配置
DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/zkxw"
DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/sanqiang"
# JWT配置
SECRET_KEY: str = "your-secret-key-change-in-production-please-use-random-string"
@@ -100,6 +100,11 @@ class Settings(BaseSettings):
WX_TEMPLATE_REFUND_RESULT: str = "" # 退款结果通知模板ID
WX_TEMPLATE_DELIVERY_WARNING: str = "" # 配送时效预警模板ID
# 与 docs/sql/zkxw-requirements-v0.3-schema.sql 一致:登录按租户 + sys_user.user_no
DEFAULT_TENANT_NO: str = "zkxw"
# 用户未绑定 sys_user_role 时,Web 管理端默认角色(建议 super_admin 便于开发全菜单)
WEB_AUTH_DEFAULT_ROLE: str = "super_admin"
# 登录 RSA:私钥 PEM(多实例/负载均衡须配置同一私钥;留空则进程启动时临时生成,仅适合单机开发)
LOGIN_RSA_PRIVATE_KEY_PEM: str = ""
# 是否允许明文 username/password 登录(仅建议本地排障临时开启;生产务必 false)
+4 -14
View File
@@ -1,4 +1,4 @@
"""菜单 code 权限守卫"""
"""菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms)"""
from typing import Callable, Set
from fastapi import Depends, HTTPException
@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
from app.core.auth import get_current_user
from app.core.database import get_db
from app.models.models import SysUserMenuScope
from app.services.web_auth_roles import iter_user_menu_perm_codes
ADMIN_ROLES = {"admin", "super_admin"}
SUPER_ADMIN_ROLE = "super_admin"
@@ -21,18 +21,8 @@ def require_admin_or_super_admin(current_user: dict = Depends(get_current_user))
def get_user_menu_codes(db: Session, user_id: str) -> Set[str]:
"""读取用户授权的菜单 code 集合。"""
if not user_id:
return set()
rows = (
db.query(SysUserMenuScope.menu_code)
.filter(
SysUserMenuScope.user_id == user_id,
SysUserMenuScope.is_del == 0,
)
.all()
)
return {str(row[0]).strip() for row in rows if row and row[0]}
"""读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。"""
return iter_user_menu_perm_codes(db, user_id or "")
def require_menu_code(menu_code: str) -> Callable: