Files
group_xinghuo_jinrong/app/gateway/jwt_service.py
T
zhanghongyu_0626 6d780d45c3 feat(auth): Enhance JWT handling and error management in authentication flow
- Updated the login endpoint to utilize shared JWT issuer/audience settings, improving consistency across modules.
- Introduced error handling for unknown accounts during token issuance, raising an UnauthorizedError when necessary.
- Enhanced traceability by adding trace and request IDs to responses, ensuring better tracking of requests.
- Refactored exception handling in middleware to properly bubble up application-specific errors, preventing them from being swallowed.
- Added new utility functions for generating trace headers to improve debugging capabilities.

This update strengthens the authentication process and enhances error visibility, contributing to a more robust and maintainable codebase.
2026-09-08 19:55:08 +08:00

145 lines
4.2 KiB
Python

"""JWT 签发与校验(开发环境 HS256)。"""
from __future__ import annotations
import uuid
from datetime import UTC, datetime, timedelta
from typing import Any
from jose import JWTError, jwt
from app.config.settings import settings
from app.model.schemas import TokenType
from app.utils.exceptions import UnauthorizedError
ISSUER = settings.jwt_issuer
AUDIENCE = settings.jwt_audience
ROLE_PERMISSIONS: dict[str, list[str]] = {
"customer": [
"agent:customer:chat",
"profile:l1:read",
"profile:l1:write",
"core:holding:read:self",
],
"advisor": [
"agent:advisor:chat",
"profile:l1:read",
"profile:l2:read",
"profile:l2:write",
"profile:l3:read",
"core:holding:read:assigned",
],
"analyst": [
"agent:analyst:chat",
"profile:l1:read",
"profile:l2:read",
"profile:l3:read",
"core:holding:read:scoped",
"sql:execute:readonly",
"risk:alert:read",
],
"risk_officer": [
"agent:risk:chat",
"profile:l1:read",
"profile:l2:read",
"profile:l3:read",
"profile:l3:write",
"risk:alert:write",
"risk:suitability:write",
"core:holding:read:all",
],
"compliance": ["audit:read:all", "agent:advisor:audit", "compliance:hit:read"],
"ops": ["agent:advisor:stats", "audit:read:aggregated"],
"service_risk": [
"agent:risk:suitability_check",
"risk:suitability:write",
"profile:l1:read",
"profile:l2:read",
"audit:write",
],
}
DEFAULT_ROLES_BY_ACTOR: dict[str, list[str]] = {
"STAFF-10086": ["advisor"],
"STAFF-10087": ["advisor"],
"STAFF-10088": ["advisor"],
"STAFF-10089": ["advisor"],
"STAFF-10090": ["advisor"],
"STAFF-10091": ["advisor", "compliance"],
"STAFF-20001": ["analyst"],
"STAFF-20002": ["analyst"],
"STAFF-30001": ["risk_officer"],
"STAFF-30002": ["risk_officer"],
"STAFF-40001": ["compliance"],
"STAFF-40002": ["compliance"],
"STAFF-50001": ["ops"],
"CUST-9527": ["customer"],
"CUST-1001": ["customer"],
"CUST-1002": ["customer"],
"CUST-1010": ["customer"],
}
def infer_roles(actor_id: str, token_type: TokenType, roles: list[str] | None) -> list[str]:
if roles:
return roles
if token_type == "customer":
return ["customer"]
# fail-closed:未知 actor 不给默认 analyst,避免越权(AL-09 / 宿主 P1)
return DEFAULT_ROLES_BY_ACTOR.get(actor_id, [])
def permissions_for_roles(roles: list[str]) -> list[str]:
perms: list[str] = []
seen: set[str] = set()
for role in roles:
for perm in ROLE_PERMISSIONS.get(role, []):
if perm not in seen:
seen.add(perm)
perms.append(perm)
return perms
def issue_token(
actor_id: str,
token_type: TokenType,
roles: list[str] | None = None,
) -> tuple[str, int]:
role_list = infer_roles(actor_id, token_type, roles)
now = datetime.now(UTC)
expires = now + timedelta(hours=settings.jwt_dev_expire_hours)
payload: dict[str, Any] = {
"iss": ISSUER,
"sub": actor_id,
"aud": AUDIENCE,
"exp": int(expires.timestamp()),
"iat": int(now.timestamp()),
"jti": str(uuid.uuid4()),
"token_type": token_type,
"roles": role_list,
"permissions": permissions_for_roles(role_list),
"tenant_id": "TENANT-001",
}
if token_type == "customer":
payload["customer_id"] = actor_id
if "advisor" in role_list and token_type == "staff":
payload["advisor_id"] = actor_id
token = jwt.encode(payload, settings.jwt_dev_secret, algorithm=settings.jwt_dev_algorithm)
return token, settings.jwt_dev_expire_hours * 3600
def decode_token(token: str) -> dict[str, Any]:
try:
payload = jwt.decode(
token,
settings.jwt_dev_secret,
algorithms=[settings.jwt_dev_algorithm],
audience=AUDIENCE,
issuer=ISSUER,
)
except JWTError as exc:
raise UnauthorizedError("Token 无效或已过期", error_code="AUTH_401_INVALID") from exc
return payload