Implement CHIS session validation

This commit is contained in:
ila
2026-07-05 11:13:34 +08:00
parent 07e6c2b9a3
commit 83c1100aa8
7 changed files with 100 additions and 10 deletions
+16
View File
@@ -151,6 +151,22 @@ class ChisLoginClient:
self._ensure_chis_ok(result, "chis_login_failed", "CHIS myApps failed") self._ensure_chis_ok(result, "chis_login_failed", "CHIS myApps failed")
return result, _extract_jsessionid(response_headers) or jsessionid return result, _extract_jsessionid(response_headers) or jsessionid
def get_lander_info(self, cookie):
headers = self._headers_without_cookie()
headers["Cookie"] = cookie
payload = {
"serviceId": "chis.myPageService",
"serviceAction": "getLanderInfo",
"method": "execute",
}
result, _ = self.transport.post(
url=f"{self.base_url}/*.jsonRequest?",
headers=headers,
payload=payload,
)
self._ensure_chis_ok(result, "chis_session_invalid", "CHIS session invalid")
return result
def _select_role(self, roles_result): def _select_role(self, roles_result):
tokens = roles_result.get("body", {}).get("tokens", []) tokens = roles_result.get("body", {}).get("tokens", [])
for token in tokens: for token in tokens:
+1 -1
View File
@@ -27,7 +27,7 @@
| ID | 任务 | 依赖 | 验收要点 | 状态 | | ID | 任务 | 依赖 | 验收要点 | 状态 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| T-101 | 迁移 CHIS 登录链路 | T-002 | 参考 `D:\hans\chupd\chis\login_client_v2.py`;登录逻辑进入 `app/chis/auth.py` 或等价模块;public key 从配置读取;`CHIS_PROXY` 有值时登录请求走代理,空值直连;账号密码不落日志;失败返回明确错误 | DONE | | T-101 | 迁移 CHIS 登录链路 | T-002 | 参考 `D:\hans\chupd\chis\login_client_v2.py`;登录逻辑进入 `app/chis/auth.py` 或等价模块;public key 从配置读取;`CHIS_PROXY` 有值时登录请求走代理,空值直连;账号密码不落日志;失败返回明确错误 | DONE |
| T-102 | 接入账号信息查询验证会话 | T-101 | 能用已有 cookie 查询当前账号信息;失败可判断会话无效 | TODO | | T-102 | 接入账号信息查询验证会话 | T-101 | 能用已有 cookie 查询当前账号信息;失败可判断会话无效 | DONE |
| T-103 | 实现 Redis 会话缓存 | T-102 | 会话对象包含 cookies、账号、角色/机构、过期时间;有效会话复用;无效会话清理 | TODO | | T-103 | 实现 Redis 会话缓存 | T-102 | 会话对象包含 cookies、账号、角色/机构、过期时间;有效会话复用;无效会话清理 | TODO |
| T-104 | 实现失效重登策略 | T-103 | CHIS 返回未登录时清缓存、重登一次、重试一次;不会无限重试 | TODO | | T-104 | 实现失效重登策略 | T-103 | CHIS 返回未登录时清缓存、重登一次、重试一次;不会无限重试 | TODO |
+17
View File
@@ -178,6 +178,23 @@ ChisLoginSession(
- 从 `Set-Cookie` 提取 `JSESSIONID`,组装 CHIS cookie。 - 从 `Set-Cookie` 提取 `JSESSIONID`,组装 CHIS cookie。
- 登录失败、缺少允许角色、缺少 cookie 时抛出 `ChisLoginError`,错误对象包含稳定 `code`。 - 登录失败、缺少允许角色、缺少 cookie 时抛出 `ChisLoginError`,错误对象包含稳定 `code`。
### `ChisLoginClient.get_lander_info(cookie)`
输入:
```python
cookie: str
```
输出:CHIS `chis.myPageService / getLanderInfo` 原始字典响应。
职责:
- 使用已有 CHIS cookie 调用 `*.jsonRequest?`。
- 请求体固定为 `serviceId=chis.myPageService`、`serviceAction=getLanderInfo`、`method=execute`。
- 成功时返回当前账号信息响应。
- CHIS 返回非 200 业务码时抛出 `ChisLoginError(code="chis_session_invalid")`,用于判断会话无效。
### `ChisSessionManager.ensure_session(account_ref)` ### `ChisSessionManager.ensure_session(account_ref)`
输入: 输入:
+8 -8
View File
@@ -4,15 +4,15 @@
## 当前快照 ## 当前快照
- 日期:2026-07-04 - 日期:2026-07-05
- 阶段:MVP 起步 / Phase 0 地基已完成 - 阶段:MVP 起步 / Phase 1 CHIS 登录与会话验证推进中
- 技术栈:当前系统 Python 3.8、Flask 3.0.3 application factory、pytest 8.3.5、requests[socks] 2.32.4、gmssl 3.2.2;不创建虚拟环境 - 技术栈:当前系统 Python 3.8、Flask 3.0.3 application factory、pytest 8.3.5、requests[socks] 2.32.4、gmssl 3.2.2;不创建虚拟环境
- 生产代码:`app/__init__.py`、`app/api/health.py`、`app/config.py`、`run.py`、`app/chis/auth.py`、`app/chis/crypto.py`,以及 `app/middleware/`、`app/services/`、`app/mappers/`、`app/chis/`、`app/repositories/`、`app/validators/` 包目录 - 生产代码:`app/__init__.py`、`app/api/health.py`、`app/config.py`、`run.py`、`app/chis/auth.py`、`app/chis/crypto.py`,以及 `app/middleware/`、`app/services/`、`app/mappers/`、`app/chis/`、`app/repositories/`、`app/validators/` 包目录;`ChisLoginClient` 已支持登录和 `getLanderInfo` 会话验证
- 测试:`tests/test_app_factory.py`、`tests/test_config_and_structure.py`、`tests/test_project_setup.py`、`tests/test_chis_auth.py` - 测试:`tests/test_app_factory.py`、`tests/test_config_and_structure.py`、`tests/test_project_setup.py`、`tests/test_chis_auth.py`
- 数据 / 资料:`reverse_file/` 下已有 CHIS HAR、前端脚本和 schema;`D:\hans\chupd\chis\login_client_v2.py` 可作为登录链路参考 - 数据 / 资料:`reverse_file/` 下已有 CHIS HAR、前端脚本和 schema;`D:\hans\chupd\chis\login_client_v2.py` 可作为登录链路参考
- 标准启动路径:`python run.py` - 标准启动路径:`python run.py`
- 标准验证路径:`python -m pytest` - 标准验证路径:`python -m pytest`
- 当前 blocker:无硬阻塞;后续实现需要把 Django 登录代码迁移为 Flask 版本,并通过配置提供 CHIS public key;访问 CHIS 时需要支持可选 SOCKS5 代理 - 当前 blocker:无硬阻塞;后续实现需要接入 Redis 会话缓存,并在失效时清理缓存和重登一次
## 当前目录要点 ## 当前目录要点
@@ -23,7 +23,7 @@
| `reverse_file/20260704_query_health_check.har` | 已有 | 体检详情只读查询 HAR,包含 `getHMNIListOfHTML` 和 `getCheckInfoDetail` | | `reverse_file/20260704_query_health_check.har` | 已有 | 体检详情只读查询 HAR,包含 `getHMNIListOfHTML` 和 `getCheckInfoDetail` |
| `D:\hans\chupd\chis\login_client_v2.py` | 外部参考 | CHIS 登录、SM2 加密、角色选择、cookie 拼接、`getLanderInfo` 会话验证 | | `D:\hans\chupd\chis\login_client_v2.py` | 外部参考 | CHIS 登录、SM2 加密、角色选择、cookie 拼接、`getLanderInfo` 会话验证 |
| `app/` | 已有 | Flask application factory 和 health check API | | `app/` | 已有 | Flask application factory 和 health check API |
| `app/chis/` | 已有 | CHIS integration layer;T-101 已包含登录客户端与 SM2 加密 | | `app/chis/` | 已有 | CHIS integration layer;T-101 已包含登录客户端与 SM2 加密,T-102 已包含 `getLanderInfo` 会话验证 |
| `app/mappers/` | 已有 | 数据转换层包目录 | | `app/mappers/` | 已有 | 数据转换层包目录 |
| `app/middleware/` | 已有 | 中间件包目录 | | `app/middleware/` | 已有 | 中间件包目录 |
| `app/repositories/` | 已有 | 存储 / 缓存包目录 | | `app/repositories/` | 已有 | 存储 / 缓存包目录 |
@@ -38,9 +38,9 @@
任务状态以 [06-tasks.md](06-tasks.md) 为准。 任务状态以 [06-tasks.md](06-tasks.md) 为准。
- 已完成:DOC-001 建立 harness coding 项目文档;DOC-002 更新 CHIS 登录与只读查询验证路线;DOC-003 明确使用系统 Python 3.8;DOC-004 明确 Flask factory 模式;DOC-005 明确日志与请求归档方案;DOC-006 明确 CHIS SOCKS5 代理配置;T-001 初始化 Flask 项目骨架;T-002 建立基础配置与目录;T-003 建立最小测试框架;T-101 迁移 CHIS 登录链路。详见 [../progress.md](../progress.md)。 - 已完成:DOC-001 建立 harness coding 项目文档;DOC-002 更新 CHIS 登录与只读查询验证路线;DOC-003 明确使用系统 Python 3.8;DOC-004 明确 Flask factory 模式;DOC-005 明确日志与请求归档方案;DOC-006 明确 CHIS SOCKS5 代理配置;T-001 初始化 Flask 项目骨架;T-002 建立基础配置与目录;T-003 建立最小测试框架;T-101 迁移 CHIS 登录链路;T-102 接入账号信息查询验证会话。详见 [../progress.md](../progress.md)。
- 正在进行:无。 - 正在进行:无。
- 下一个可领取任务:T-102 接入账号信息查询验证会话。 - 下一个可领取任务:T-103 实现 Redis 会话缓存。
## 已确认技术事实 ## 已确认技术事实
@@ -55,7 +55,7 @@
- `.env.example` 只保留占位符和非敏感默认值,不写真实密钥。 - `.env.example` 只保留占位符和非敏感默认值,不写真实密钥。
- 登录链路已迁移到 `app/chis/auth.py`:`myRoles` -> 选择 `责任医生助理` / `责任医生` -> `myApps` -> 提取 `JSESSIONID` -> 拼接 cookie。 - 登录链路已迁移到 `app/chis/auth.py`:`myRoles` -> 选择 `责任医生助理` / `责任医生` -> `myApps` -> 提取 `JSESSIONID` -> 拼接 cookie。
- SM2 加密已迁移到 `app/chis/crypto.py`,兼容 `hans_chis.sm2.sm2_encrypt`:`gmssl.sm2.CryptSM2(mode=0)`,返回带 `04` 前缀密文。 - SM2 加密已迁移到 `app/chis/crypto.py`,兼容 `hans_chis.sm2.sm2_encrypt`:`gmssl.sm2.CryptSM2(mode=0)`,返回带 `04` 前缀密文。
- 会话验证可用 `chis.myPageService / getLanderInfo`。 - 会话验证已接入 `ChisLoginClient.get_lander_info(cookie)`,请求 `chis.myPageService / getLanderInfo`;CHIS 非 200 业务码会抛出 `chis_session_invalid`。
- 体检详情查询第一版可用 `healthCheck + phrId + idCard` 调 `getHMNIListOfHTML`,用 `idCard + checkDate` 调 `getCheckInfoDetail`。 - 体检详情查询第一版可用 `healthCheck + phrId + idCard` 调 `getHMNIListOfHTML`,用 `idCard + checkDate` 调 `getCheckInfoDetail`。
- 当前不能证明只传 `healthCheck` 就能查完整详情。 - 当前不能证明只传 `healthCheck` 就能查完整详情。
- 运行日志保存到 `logs/`,每天一个综合日志文件,保留 1 年。 - 运行日志保存到 `logs/`,每天一个综合日志文件,保留 1 年。
+9
View File
@@ -115,3 +115,12 @@
- 阻塞:无。 - 阻塞:无。
- 决策:T-101 只迁移登录链路,不接 Redis,不做会话有效性查询;登录客户端从 `CHIS_BASE_URL`、`CHIS_PUBLIC_KEY`、`CHIS_PROXY` 创建,完成 `myRoles`、角色选择、`myApps`、JSESSIONID 提取和 cookie 组装。 - 决策:T-101 只迁移登录链路,不接 Redis,不做会话有效性查询;登录客户端从 `CHIS_BASE_URL`、`CHIS_PUBLIC_KEY`、`CHIS_PROXY` 创建,完成 `myRoles`、角色选择、`myApps`、JSESSIONID 提取和 cookie 组装。
- 下一步:T-102 接入账号信息查询验证会话。 - 下一步:T-102 接入账号信息查询验证会话。
## 2026-07-05 T-102 接入账号信息查询验证会话
- 状态:DONE
- 变更:`app/chis/auth.py` 新增 `ChisLoginClient.get_lander_info(cookie)`,使用已有 cookie 调用 `chis.myPageService / getLanderInfo`;`tests/test_chis_auth.py` 新增会话验证成功和会话失效测试;更新任务看板、API 合约、当前状态和根目录任务总览。
- 验证:先执行 `python -m pytest tests\test_chis_auth.py -k lander`,看到 `AttributeError: 'ChisLoginClient' object has no attribute 'get_lander_info'`;实现后执行同一命令结果 `2 passed, 6 deselected`;执行 `python -m pytest` 结果 `12 passed`。
- 阻塞:无。
- 决策:T-102 只做已有 cookie 的账号信息查询和失效判断;Redis 缓存、清理缓存和重登策略留给 T-103 / T-104。
- 下一步:T-103 实现 Redis 会话缓存。
+1 -1
View File
@@ -14,7 +14,7 @@ MVP 起步。当前仓库已具备逆向资料、项目文档、Flask applicatio
| T-002 | 建立基础配置与目录 | DONE | | T-002 | 建立基础配置与目录 | DONE |
| T-003 | 建立最小测试框架 | DONE | | T-003 | 建立最小测试框架 | DONE |
| T-101 | 迁移 CHIS 登录链路 | DONE | | T-101 | 迁移 CHIS 登录链路 | DONE |
| T-102 | 接入账号信息查询验证会话 | TODO | | T-102 | 接入账号信息查询验证会话 | DONE |
| T-103 | 实现 Redis 会话缓存 | TODO | | T-103 | 实现 Redis 会话缓存 | TODO |
| T-201 | 实现通用 CHIS jsonRequest client | TODO | | T-201 | 实现通用 CHIS jsonRequest client | TODO |
| T-202 | 实现体检详情查询 client | TODO | | T-202 | 实现体检详情查询 client | TODO |
+48
View File
@@ -150,3 +150,51 @@ def test_login_client_accepts_plain_host_config():
assert client.base_url == "http://chis.example.test:9002/chis" assert client.base_url == "http://chis.example.test:9002/chis"
assert client.host == "chis.example.test:9002" assert client.host == "chis.example.test:9002"
def test_get_lander_info_uses_existing_cookie_to_query_account_info():
transport = FakeTransport(
[
(
{
"code": 200,
"body": {"userId": "u001", "userName": "doctor"},
"msg": "success",
},
{},
),
]
)
client = ChisLoginClient(
base_url="http://chis.example.test/chis",
public_key="public-key",
transport=transport,
encrypt=fake_encrypt,
)
result = client.get_lander_info("JSESSIONID=abc123; sessionId1=abc123")
assert result["body"]["userId"] == "u001"
assert transport.calls[0]["url"] == "http://chis.example.test/chis/*.jsonRequest?"
assert transport.calls[0]["headers"]["Cookie"] == "JSESSIONID=abc123; sessionId1=abc123"
assert transport.calls[0]["payload"] == {
"serviceId": "chis.myPageService",
"serviceAction": "getLanderInfo",
"method": "execute",
}
def test_get_lander_info_raises_session_invalid_when_chis_rejects_cookie():
transport = FakeTransport([({"code": 403, "msg": "not login"}, {})])
client = ChisLoginClient(
base_url="http://chis.example.test/chis",
public_key="public-key",
transport=transport,
encrypt=fake_encrypt,
)
with pytest.raises(ChisLoginError) as exc_info:
client.get_lander_info("JSESSIONID=expired")
assert exc_info.value.code == "chis_session_invalid"
assert "not login" in str(exc_info.value)