Files
group_fqcd_jr/pyproject.toml
T
lzf_0626 01ee68034d 新增登录接口:账号密码换访问令牌(POST /api/v1/auth/tokens)
背景:客户 / 员工 / 管理员三种身份此前无法区分。但区分逻辑其实早就完备 ——
bootstrap.py 里各 Agent 的 allowed_roles 一直是分开的(CustomerServiceAgent 只要
customer、RiskAgent 要 risk_operator/admin、PlatformProbeAgent 只要 admin),
唯独缺"怎么证明你是谁";sys_user.password_hash 字段也一直存在,只是全是占位符
(种子写 'x'、worker 身份写 !worker-only-no-password-login!),从没写过真实密码。

实现:
- app/service/auth_service.py:bcrypt 校验 + 签发只含 sub 的 JWT + 审计。令牌里只放 sub
  是刻意的:角色/权限/数据范围由 IdentityService 每次请求查库解析
  (identity_repository.load_context),权限变更因此立即生效,现有鉴权链路一行未改。
- app/api/controllers/auth.py + app/api/schemas/auth.py:POST /api/v1/auth/tokens,
  响应含 roles/data_scope 供前端决定进哪个界面(鉴权仍以库里实时数据为准)。
- tools/set_user_password.py:设密码(客户 123456 / 员工 666666 / 管理员 88888888)。
  ⚠️ 脚本与文档均标注"仅限演示环境",这三种弱口令上线前必须更换。
- pyproject / requirements 加 bcrypt(cryptography 只用于 JWT,不提供密码哈希)。

安全约定(逐条有实现与测试):失败不区分原因 —— 用户不存在/密码错/账号停用返回同一条
401,否则接口就成了账号枚举器;用户不存在时也跑一次 bcrypt 以抹掉时序差异;
成功与失败都写 interaction_audit(actor_id 可空正是为失败场景准备的);绝不记录密码。

过程中踩到一个自己挖的坑:给登录路由挂了通用的 enforce_rate_limit,而它声明依赖
build_request_context ⇒ 变成"要登录先登录",所有登录都 401。改为新增
enforce_login_rate_limit:按客户端 IP 独立限流(60 秒 10 次)、不依赖认证上下文。
集成测试据此调整:注入恒放行替身隔离跨用例的计数累积,同时保留一个恒超限用例验证闸门
确实会拦 —— 不能因为加了替身就把这道防线测丢。

接口登记:docs/05 §19 加 A034;并更新 §11 —— 那里原写"JWT 签发、刷新、注销由统一身份
认证模块负责,Agent 平台不重复实现",现注明平台只做登录这一步,刷新/注销仍归该模块。

验证:ruff 干净 / mypy 183 文件 0 错 / 文档守卫 37 份无重号(此前因 docs/21 重号失败)/
unit+contract 1140 passed / integration 90 passed。
2026-09-11 20:43:21 +08:00

80 lines
2.2 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[build-system]
requires = ["setuptools>=75"]
build-backend = "setuptools.build_meta"
[project]
name = "jr-agent-platform"
version = "0.1.0"
description = "金融系统 Agent 通用底座"
requires-python = ">=3.13,<3.14"
dependencies = [
"fastapi>=0.115,<1",
"uvicorn[standard]>=0.34,<1",
"pydantic>=2.10,<3",
"pydantic-settings>=2.7,<3",
"python-dotenv>=1.0,<2",
"greenlet>=3.1,<4",
"sqlalchemy>=2.0,<3",
"alembic>=1.14,<2",
"asyncmy>=0.2,<1",
"pymysql>=1.1,<2",
"redis>=5.2,<6",
"neo4j>=5.28,<6",
"pymilvus>=2.5,<3",
"PyJWT>=2.10,<3",
"cryptography>=44,<51",
"bcrypt>=4.0,<5",
"httpx>=0.28,<1",
"tzdata>=2025.1,<2027",
"alibabacloud_docmind_api20220711==1.4.14",
"alibabacloud_tea_openapi>=0.4.3,<1",
"alibabacloud_tea_util>=0.3.13,<1",
"alibabacloud_credentials>=1.0.2,<2",
"python-pptx>=1.0,<2",
"python-docx>=1.1,<2",
"openpyxl>=3.1,<4",
"Pillow>=10,<12",
"matplotlib>=3.9,<4",
"python-multipart>=0.0.20,<1",
]
[project.optional-dependencies]
dev = [
"pytest>=8.3,<9",
"pytest-asyncio>=0.25,<1",
"ruff>=0.9,<1",
"mypy>=1.14,<2",
# 测试用:场外邮件 Worker 等用例走 SQLAlchemy 的 SQLite 内存库。
# 原先没声明 —— 别人的环境装完依赖跑测试,这 6 个用例会直接
# ModuleNotFoundError: No module named 'aiosqlite'。
"aiosqlite>=0.20,<1",
]
[tool.setuptools.packages.find]
include = ["app*"]
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
markers = ["integration: requires local database services"]
# 临时目录的落点由 `tests/conftest.py` 覆盖 `tmp_path` 决定(落在仓库内 `.workdir/pytest-tmp`)。
# 这里**不**写 `basetemp`:pytest 只在命令行认它,写在 ini 里会被静默忽略(实测无效),
# 留着会让人误以为已经配好了。系统临时目录权限坏掉的原因与修法见 conftest 的说明。
[tool.ruff]
line-length = 100
target-version = "py313"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
[tool.ruff.lint.per-file-ignores]
"alembic/versions/*.py" = ["E501"]
"tools/*.py" = ["E501", "I001", "B007"]
[tool.mypy]
python_version = "3.13"
strict = true
packages = ["app"]