chore: 删除退役的旧 JWT 密钥,同步过时引用并记录登录接口决策
1. 删除 config/jwt/jwt-private.pem 与 jwt-public.pem(旧密钥,配置已不再指向)。
删除后复跑全量门禁以确认没有残留依赖:ruff 通过、unit+contract 447 passed、
acceptance_check --production 7 PASS、demo_agent_e2e 9/9 PASS。
2. 修掉 4 处引用旧密钥路径的地方(它们会在删除密钥后直接失败或误导接入方):
- tests/unit/core/test_security.py 的三处硬编码路径改为单一常量 DEV_KEY_DIR,
否则删除旧密钥后该测试会因读不到文件而失败;
- tools/demo_agent_e2e.py 的 docstring 前置条件;
- docs/09 的配置示例;docs/19 的手工自签说明(改为指向配置项与
tools/generate_jwt_keys.py)。
3. docs/21 记录旧密钥已删除,并注明删除后已复跑验证无残留引用。
4. docs/19 未解决项新增第 5 条:无登录接口属于**有意识的推迟**(等业务 Agent 开发阶段
结束后再补),写明补的时候只需动签发侧、验签侧与身份解析侧都不需要改,
并附上当前私钥边界的实测结论(无法伪造不存在的用户、无法使用已禁用账号)。
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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` 取身份,否则后补登录接口会
|
||||
从"增量"变成"翻遍所有业务代码"。
|
||||
|
||||
@@ -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 删除**(删除后全量门禁复跑通过,确认无残留引用) |
|
||||
| 生产密钥 | 上线时在生产机上生成 | 尚不存在(本就不该在开发机生成) |
|
||||
|
||||
开发密钥的公钥指纹可以用生成脚本查看;核对"服务端加载的是不是同一把公钥"时用它比对,
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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 并使用同一队列的执行路径。
|
||||
|
||||
复核方式:配置发布保留"提交审核 → 审核 → 激活"状态机,但**不再要求审核人不是创建人**,
|
||||
|
||||
Reference in New Issue
Block a user