diff --git a/docs/09-底座使用文档.md b/docs/09-底座使用文档.md index 36dbcc3..e3cc148 100644 --- a/docs/09-底座使用文档.md +++ b/docs/09-底座使用文档.md @@ -33,7 +33,7 @@ Copy-Item .env.example .env ```dotenv JWT_ISSUER=jr-local JWT_AUDIENCE=jr-agent-platform -JWT_PUBLIC_KEY_PATH=config/jwt/jwt-public.pem +JWT_PUBLIC_KEY_PATH=config/jwt/dev/jwt-public.pem MYSQL_DSN=mysql+asyncmy://用户名:密码@127.0.0.1:3306/jr REDIS_URL=redis://127.0.0.1:6379/0 MILVUS_URI=http://127.0.0.1:19530 diff --git a/docs/19-业务Agent接入实操(示例验证版).md b/docs/19-业务Agent接入实操(示例验证版).md index 6af59e4..907bb0c 100644 --- a/docs/19-业务Agent接入实操(示例验证版).md +++ b/docs/19-业务Agent接入实操(示例验证版).md @@ -141,8 +141,9 @@ D:\conda\envs\jr_py313\python.exe tools\demo_agent_e2e.py D:\conda\envs\jr_py313\python.exe C:\Users\...\e2e_run_check.py --label 手工验证 ``` -单条链路的手工命令(客户身份 9001,JWT 用 `config/jwt/jwt-private.pem` 按 RS256 自签, -参考 `tools/acceptance_check.py::token()`): +单条链路的手工命令(客户身份 9001,JWT 用配置项 `JWT_PRIVATE_KEY_PATH` 指向的私钥按 RS256 自签, +参考 `tools/acceptance_check.py::token()`)。还没有密钥就先跑一次 +`python tools/generate_jwt_keys.py --out-dir config/jwt/dev`(详见 `docs/21-JWT密钥管理与轮换.md`): ```text POST /api/v1/agent-runs @@ -218,3 +219,11 @@ D:\conda\envs\jr_py313\python.exe tools\demo_agent_e2e.py (`reviews`/`activations`/`archivals`)且运行期读取生效(见 3.1)。 4. **`docs/05` §9.5 未收录 `agent-intent-configs` 的 `GET` 详情路径**:该接口实际存在(用于获取 `If-Match` 所需的 ETag),但权威接口文档未列出,属文档待补项。 +5. **没有登录接口(已决定推迟,不是遗漏)**:当前没有"账号密码换令牌"的接口,令牌由外部按 + RS256 用私钥自签(见 `docs/21-JWT密钥管理与轮换.md`)。**决定:等业务 Agent 开发阶段 + 结束后再补**。补的时候只需动签发侧(新增 `POST /auth/login`:校验密码 → 用私钥签令牌), + 验签侧 `JwtAuthenticator` 与身份解析侧 `IdentityService` 都不需要改。 + 代价与约束:开发阶段凡拿到私钥者都能以 `9001/9002/9003` 身份调用(实测**无法**伪造不存在的 + 用户、**无法**使用已禁用账号,边界是"签名有效 + 用户存在且启用"),因此私钥按 `docs/21` + 第 7 节的红线管理;同时业务代码必须始终只从 `RequestContext` 取身份,否则后补登录接口会 + 从"增量"变成"翻遍所有业务代码"。 diff --git a/docs/21-JWT密钥管理与轮换.md b/docs/21-JWT密钥管理与轮换.md index 14af812..ca9eb12 100644 --- a/docs/21-JWT密钥管理与轮换.md +++ b/docs/21-JWT密钥管理与轮换.md @@ -8,7 +8,7 @@ | 文件 | 用途 | 状态 | | --- | --- | --- | | `config/jwt/dev/jwt-private.pem` / `jwt-public.pem` | **开发专用**(服务端验签,脚本签发) | **正在使用** | -| `config/jwt/jwt-private.pem` / `jwt-public.pem` | 旧密钥(曾是默认) | **已退役**,配置不再指向它,用它签的令牌会被拒绝,可删 | +| ~~`config/jwt/jwt-private.pem` / `jwt-public.pem`~~ | 旧密钥(曾是默认) | **已于 2026-09-10 删除**(删除后全量门禁复跑通过,确认无残留引用) | | 生产密钥 | 上线时在生产机上生成 | 尚不存在(本就不该在开发机生成) | 开发密钥的公钥指纹可以用生成脚本查看;核对"服务端加载的是不是同一把公钥"时用它比对, diff --git a/tests/unit/core/test_security.py b/tests/unit/core/test_security.py index 6bdf3e8..817457d 100644 --- a/tests/unit/core/test_security.py +++ b/tests/unit/core/test_security.py @@ -8,6 +8,10 @@ from app.core.config import Settings from app.core.errors import UnauthorizedAgentError from app.core.security import JwtAuthenticator +# 开发专用密钥目录(tools/generate_jwt_keys.py 生成)。本测试需要真实密钥完成签发与验签, +# 所以路径只在这里定义一次:换密钥目录时改这一处,避免多处硬编码各自漂移。 +DEV_KEY_DIR = Path("config/jwt/dev") + def _settings() -> Settings: return Settings( @@ -18,13 +22,13 @@ def _settings() -> Settings: redis_url="redis://localhost", milvus_uri="http://localhost:19530", neo4j_uri="bolt://localhost:7687", - jwt_public_key_path="config/jwt/jwt-public.pem", + jwt_public_key_path=str(DEV_KEY_DIR / "jwt-public.pem"), ) def _token(subject: str = "1") -> str: now = datetime.now(UTC) - private_key = Path("config/jwt/jwt-private.pem").read_text(encoding="utf-8") + private_key = (DEV_KEY_DIR / "jwt-private.pem").read_text(encoding="utf-8") return jwt.encode( {"sub": subject, "iss": "jr-auth", "aud": "jr-agent-platform", "iat": now, "nbf": now, "exp": now + timedelta(minutes=5), "jti": "jti-1"}, @@ -65,7 +69,7 @@ def test_signed_invalid_subject_returns_401_before_identity_query(monkeypatch): def test_authenticate_rejects_expired_token() -> None: with pytest.raises(UnauthorizedAgentError): now = datetime.now(UTC) - private_key = Path("config/jwt/jwt-private.pem").read_text(encoding="utf-8") + private_key = (DEV_KEY_DIR / "jwt-private.pem").read_text(encoding="utf-8") token = jwt.encode( {"sub": "user-1", "iss": "jr-auth", "aud": "jr-agent-platform", "nbf": now - timedelta(minutes=2), "exp": now - timedelta(minutes=1), "jti": "jti-2"}, diff --git a/tools/demo_agent_e2e.py b/tools/demo_agent_e2e.py index 8ff990b..33c95f8 100644 --- a/tools/demo_agent_e2e.py +++ b/tools/demo_agent_e2e.py @@ -9,7 +9,8 @@ python tools/demo_agent_e2e.py # 全流程并清理 run 数据 python tools/demo_agent_e2e.py --keep-run # 保留 run/审计数据便于排查 -前置:数据库与配置中心已就绪;`config/jwt/jwt-private.pem` 存在; +前置:数据库与配置中心已就绪;JWT 私钥存在(配置项 `JWT_PRIVATE_KEY_PATH`,默认 +`config/jwt/dev/jwt-private.pem`,没有就先跑 `tools/generate_jwt_keys.py`); **运行前请停掉常驻 Worker**,否则它会抢走本次 run 并使用同一队列的执行路径。 复核方式:配置发布保留"提交审核 → 审核 → 激活"状态机,但**不再要求审核人不是创建人**,