# T10 · 登录·华为账号(代码先行,联调后置) > 对应任务:`T-P1-12` | 需求:`FR-ACC-01`(登录成功率 ≥99%、UnionID 映射唯一、首次自动建用户)| 分支:`task/t10-auth-huawei` | worktree:`/home/xsl/meida_backend-t10` > 先读《README.md(协作规程)》,遵守全部纪律。 ## 1. 目标 实现 `POST /auth/huawei`:客户端传华为 Account Kit 签发的 **id_token**,服务端按 OIDC 标准做 **JWKS 验签 + claims 校验**,映射到内部用户并签发双令牌(access + refresh,复用 T4 已交付的 `issue_session`)。 **本任务性质是"代码先行"**:华为 AGC 后台尚未配置(见《前置开通清单》§3),因此真实联调后置;全部逻辑用**自造 RSA 密钥对 + 本地 JWKS** 做单测/集成,凭证到位后只改 `.env` 即可上线。 ## 2. 已就绪的契约(协调提交已提供) ### 配置(config.py 已加) ```python huawei_jwks_url: str = "https://oauth-login.cloud.huawei.com/oauth2/v3/certs" # HUAWEI_JWKS_URL huawei_audience: str = "" # HUAWEI_AUDIENCE,= AGC App ID,联调时填 huawei_issuer: str = "https://accounts.huawei.com" # HUAWEI_ISSUER(以 Account Kit 文档为准,可配) huawei_jwks_cache_seconds: int = 86400 ``` ### 可复用的现有设施(先读再写) - `packages/identity/service.py`:`get_or_create_by_external_identity(provider, union_id, open_id, register_source)`(T4 已有);`issue_session(user_id) -> SessionIssued`(T4 交付,含 refresh 轮换结构)。 - `packages/shared/security.py`:`create_access_token(...)`(HS256 access)。 - `packages/identity/schemas.py`:`TokenPairResponse`(T4 交付)——**直接复用,不要在本任务重复定义**。 - `pyjwt[crypto]` 已装(含 cryptography,RS256 验签可用)。 ### 端点归属 `apps/api/routers/auth.py` 归你(T4 已合并完,本批无他人碰它)。`apps/api/routers/auth_sms.py` 归 T11,**禁碰**。 ## 3. 实现要点 允许改动:`packages/identity/huawei.py`(新建,核心逻辑全在此)、`apps/api/routers/auth.py`(追加端点)、测试文件。**不改 identity/service.py 与 identity/schemas.py**(避免与 T11 冲突——你的模型定义在 huawei.py 内)。 1. **`packages/identity/huawei.py`**: - `HuaweiIdTokenVerifier`:构造参数 `(jwks_url, audience, issuer, cache_seconds, *, http_fetch=None, now=None)`——`http_fetch` 是 `Callable[[str], Awaitable[str]]`(单测注入本地 JWKS,**不连外网**);JWKS 内存缓存(TTL 按配置,kid 变化时强制刷新一次并重试验签); - `verify(id_token) -> HuaweiClaims`(frozen dataclass:`union_id/open_id/exp/iat`):RS256 验签(`jwt.decode` + `PyJWK`/手工 kid 匹配)、`aud`(等于配置 audience,空配置时**拒绝**而不是跳过——联调前后都必须严格)、`iss`、`exp/iat`、`union_id` 缺失拒绝(华为子账号场景无 union_id 时降级用 open_id 的策略**不做**,注释说明留给联调期决定); - 错误映射:验签失败/claims 不符 → `AppError(TOKEN_INVALID)`(40102,已有);JWKS 拉取失败 → `AppError(UPSTREAM_ERROR)`(60000,已有)——**不需要新错误码**。 2. **路由**(auth.py 追加): ```python @router.post("/huawei", summary="华为账号授权登录(FR-ACC-01)") async def huawei_login(payload: HuaweiLoginRequest, ...) -> ...: claims = await verifier.verify(payload.id_token) user = await identity.get_or_create_by_external_identity( provider="huawei", union_id=claims.union_id, open_id=claims.open_id, register_source="huawei") issued = await identity.issue_session(user.user_id) access = create_access_token(...) return ok(TokenPairResponse(...)) # 复用 T4 模型 ``` - verifier 用依赖注入(路由模块内建一个 `get_huawei_verifier` 的 FastAPI Depends,从 settings 构造并缓存到 `app.state` 或模块级 lru——**不改 deps.py**,在 auth.py 内实现); - `HuaweiLoginRequest(id_token: str)` 定义在 huawei.py。 3. **模块 docstring** 写明:真实联调前置 = AGC 应用创建 + 证书指纹双套 + `HUAWEI_AUDIENCE` 填 App ID(引用《前置开通清单》§3)。 ## 4. 测试要求 - 单测 `tests/unit/test_huawei_verifier.py`:测试内用 `cryptography` 生成 RSA 密钥对、自签 id_token + 本地 JWKS dict: 1. 合法 token → claims 解析正确; 2. 错误签名(换密钥)→ 40102;过期 → 40101(复用 TOKEN_EXPIRED);aud 不符 → 40102;audience 未配置 → 40102; 3. JWKS 拉取失败 → 60000;kid 轮换 → 缓存刷新后验签成功。 - 集成 `tests/integration/test_auth_huawei.py`(新文件):monkeypatch 注入 FakeFetcher(本地 JWKS)→ `POST /v1/auth/huawei` → 200 双令牌;再次登录同 union_id → 同一 user_id(映射唯一);新 access 可调 `GET /v1/quota`。 ## 5. 禁改清单 通用禁改之外:`packages/identity/{service,repo,schemas,tokens}.py`、`apps/api/routers/auth_sms.py`、`packages/{kb,rules,retrieval}/**`。 ## 6. 验收标准(FR-ACC-01) 1. 验签逻辑完整(签名/aud/iss/exp/iat 全查),audience 未配置时拒绝启动校验路径; 2. UnionID→内部 user_id 映射唯一,首次登录自动建用户(集成断言同 ID 复登); 3. 全链路不连外网(FakeFetcher 注入); 4. 全部检查绿(mypy 范围同 T6 列表)。 ## 7. 提交信息 ``` feat(identity): Huawei Account Kit login via OIDC JWKS verification POST /auth/huawei:JWKS 验签(kid 缓存/轮换)+ claims 严格校验 + UnionID 映射 + T4 双令牌签发(FR-ACC-01;真实联调待 AGC 配置)。 偏差说明:<如有写在这里,无则写"无"> ```