调整文档路径
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
"""配置:环境变量读取"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class Settings:
|
||||
AUTH_DB_HOST: str = os.getenv("AUTH_DB_HOST", "10.100.154.100")
|
||||
AUTH_DB_PORT: int = int(os.getenv("AUTH_DB_PORT", "5432"))
|
||||
AUTH_DB_USER: str = os.getenv("AUTH_DB_USER", "postgres")
|
||||
AUTH_DB_PASSWORD: str = os.getenv("AUTH_DB_PASSWORD", "digiwin")
|
||||
AUTH_DB_NAME: str = os.getenv("AUTH_DB_NAME", "mcp_auth")
|
||||
|
||||
# 鼎捷云 IAM 配置
|
||||
IAM_BASE_URL: str = os.getenv("IAM_BASE_URL", "https://iam.digiwincloud.com.cn")
|
||||
# 应用 apptoken(digi-middleware-auth-app)
|
||||
IAM_APP_TOKEN: str = os.getenv(
|
||||
"IAM_APP_TOKEN",
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImRhdGEtYnVzaW5lc3MtZGVtbyIsInNpZCI6MH0.Spo64LstbWxjYNefVFAbEbgfjzZoQGNcqKSGuYUOCRk",
|
||||
)
|
||||
# IAM token 校验缓存秒数
|
||||
IAM_CACHE_TTL: int = int(os.getenv("IAM_CACHE_TTL", "30"))
|
||||
|
||||
# 前端静态文件目录(Docker 构建后注入)
|
||||
STATIC_DIR: str = os.getenv("STATIC_DIR", "../frontend/dist")
|
||||
|
||||
# Redis 配置
|
||||
REDIS_HOST: str = os.getenv("REDIS_HOST", "10.100.154.100")
|
||||
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
|
||||
REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
|
||||
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "digiwin")
|
||||
|
||||
# 反向代理路径前缀(让 Swagger / OpenAPI 在带前缀的代理后正确生成 URL)
|
||||
# 生产经外层 nginx 暴露在 https://<host>/mcp-auth-api/ 下时设为 /mcp-auth-api;直连 8003 时留空
|
||||
ROOT_PATH: str = os.getenv("ROOT_PATH", "")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""鉴权库连接池"""
|
||||
|
||||
import asyncpg
|
||||
from .config import settings
|
||||
|
||||
_pool: asyncpg.Pool | None = None
|
||||
|
||||
|
||||
async def get_pool() -> asyncpg.Pool:
|
||||
global _pool
|
||||
if _pool is None:
|
||||
_pool = await asyncpg.create_pool(
|
||||
host=settings.AUTH_DB_HOST,
|
||||
port=settings.AUTH_DB_PORT,
|
||||
user=settings.AUTH_DB_USER,
|
||||
password=settings.AUTH_DB_PASSWORD,
|
||||
database=settings.AUTH_DB_NAME,
|
||||
min_size=2,
|
||||
max_size=10,
|
||||
)
|
||||
return _pool
|
||||
|
||||
|
||||
async def close_pool() -> None:
|
||||
global _pool
|
||||
if _pool is not None:
|
||||
await _pool.close()
|
||||
_pool = None
|
||||
@@ -0,0 +1,33 @@
|
||||
"""FastAPI 依赖:IAM token 校验,提取当前管理员
|
||||
|
||||
从请求头 digi-middleware-auth-user / digi-middleware-auth-app 读取凭证,
|
||||
调用 IAM /api/iam/v2/identity/token/analyze 校验并解析用户信息。
|
||||
"""
|
||||
|
||||
from fastapi import Header, HTTPException, status
|
||||
|
||||
from . import iam
|
||||
|
||||
|
||||
async def current_admin(
|
||||
user_token: str | None = Header(None, alias="digi-middleware-auth-user"),
|
||||
app_token: str | None = Header(None, alias="digi-middleware-auth-app"),
|
||||
) -> dict:
|
||||
if not user_token:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "未提供 IAM userToken")
|
||||
|
||||
info = await iam.analyze_token(user_token, app_token)
|
||||
if info is None:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "IAM token 无效或已过期")
|
||||
|
||||
# 返回统一的管理员信息(兼容原有 admin.get("username") 调用)
|
||||
return {
|
||||
"username": info.get("name") or info.get("id") or "unknown",
|
||||
"userId": info.get("id"),
|
||||
"name": info.get("name"),
|
||||
"email": info.get("email"),
|
||||
"telephone": info.get("telephone"),
|
||||
"tenantId": info.get("tenantId"),
|
||||
"tenantName": info.get("tenantName"),
|
||||
"raw": info,
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"""鼎捷云 IAM token 鉴权服务
|
||||
|
||||
通过调用 IAM `/api/iam/v2/identity/token/analyze` 校验请求头中的
|
||||
digi-middleware-auth-user / digi-middleware-auth-app,解析出用户信息。
|
||||
|
||||
进程内 LRU 缓存(userToken -> userInfo),TTL 由 IAM_CACHE_TTL 控制,
|
||||
减少对 IAM 的重复调用。
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
from .config import settings
|
||||
|
||||
# httpx 异步客户端(进程级单例,复用连接池)
|
||||
_http_client: httpx.AsyncClient | None = None
|
||||
|
||||
# 缓存:userToken -> (user_info, fetched_at)
|
||||
_cache: dict[str, tuple[dict | None, float]] = {}
|
||||
|
||||
|
||||
def _get_http_client() -> httpx.AsyncClient:
|
||||
global _http_client
|
||||
if _http_client is None:
|
||||
_http_client = httpx.AsyncClient(timeout=5.0)
|
||||
return _http_client
|
||||
|
||||
|
||||
async def close_iam_client() -> None:
|
||||
"""关闭 httpx 客户端(进程退出时调用)。"""
|
||||
global _http_client
|
||||
if _http_client is not None:
|
||||
await _http_client.aclose()
|
||||
_http_client = None
|
||||
|
||||
|
||||
async def analyze_token(user_token: str, app_token: str | None = None) -> dict | None:
|
||||
"""校验 IAM userToken,返回用户信息 dict 或 None。
|
||||
|
||||
调用 POST {IAM_BASE_URL}/api/iam/v2/identity/token/analyze,
|
||||
请求头携带 digi-middleware-auth-app 与 digi-middleware-auth-user。
|
||||
成功时返回包含 id/name/tenantId/tenantName 等字段的 dict。
|
||||
"""
|
||||
# 1. 查缓存
|
||||
now = time.time()
|
||||
cached = _cache.get(user_token)
|
||||
if cached is not None and (now - cached[1]) < settings.IAM_CACHE_TTL:
|
||||
return cached[0]
|
||||
|
||||
# 2. 调用 IAM analyze
|
||||
headers = {
|
||||
"digi-middleware-auth-user": user_token,
|
||||
"digi-middleware-auth-app": app_token or settings.IAM_APP_TOKEN,
|
||||
}
|
||||
try:
|
||||
client = _get_http_client()
|
||||
resp = await client.post(
|
||||
f"{settings.IAM_BASE_URL}/api/iam/v2/identity/token/analyze",
|
||||
headers=headers,
|
||||
)
|
||||
except Exception as ex:
|
||||
# IAM 不可达,缓存短时间避免雪崩
|
||||
_cache[user_token] = (None, now)
|
||||
print(f"[IAM] analyze 请求异常: {ex}")
|
||||
return None
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
# 兼容字段:id(用户标识)/ name(姓名)
|
||||
if data.get("id") or data.get("name"):
|
||||
_cache[user_token] = (data, now)
|
||||
return data
|
||||
# 返回体无用户标识,视为无效
|
||||
_cache[user_token] = (None, now)
|
||||
return None
|
||||
|
||||
# 非 200(token 无效/过期),缓存避免雪崩
|
||||
_cache[user_token] = (None, now)
|
||||
return None
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Redis 连接池 + 缓存读写封装
|
||||
|
||||
用于 verify-token 的服务/Token 缓存,减少数据库访问。
|
||||
缓存 Key 约定:
|
||||
mcp:svc:{api_key_hash} → 服务信息(TTL 120s)
|
||||
mcp:tok:{token_hash} → Token 信息(TTL 30s)
|
||||
mcp:tok:miss:{token_hash} → 无效标记,防穿透(TTL 30s)
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
|
||||
from .config import settings
|
||||
|
||||
_pool: aioredis.Redis | None = None
|
||||
|
||||
|
||||
async def get_redis() -> aioredis.Redis:
|
||||
"""获取 Redis 连接(进程级单例)。"""
|
||||
global _pool
|
||||
if _pool is None:
|
||||
_pool = aioredis.Redis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=settings.REDIS_PORT,
|
||||
db=settings.REDIS_DB,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
decode_responses=True,
|
||||
)
|
||||
return _pool
|
||||
|
||||
|
||||
async def close_redis() -> None:
|
||||
"""关闭 Redis 连接(进程退出时调用)。"""
|
||||
global _pool
|
||||
if _pool is not None:
|
||||
await _pool.aclose()
|
||||
_pool = None
|
||||
|
||||
|
||||
async def cache_get(key: str) -> dict | None:
|
||||
"""读取 JSON 缓存,返回 dict 或 None。Redis 不可用时返回 None(视为缓存未命中)。"""
|
||||
try:
|
||||
r = await get_redis()
|
||||
raw = await r.get(key)
|
||||
except Exception:
|
||||
return None
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def cache_set(key: str, value: dict, ttl: int) -> None:
|
||||
"""写入 JSON 缓存,带 TTL(秒)。Redis 不可用时静默跳过。"""
|
||||
try:
|
||||
r = await get_redis()
|
||||
await r.setex(key, ttl, json.dumps(value))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def cache_delete(*keys: str) -> None:
|
||||
"""删除缓存 key。Redis 不可用时静默跳过,缓存会自然过期。"""
|
||||
if not keys:
|
||||
return
|
||||
try:
|
||||
r = await get_redis()
|
||||
await r.delete(*keys)
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user