16 KiB
IAM v2 关联管理与本人跨平台快捷登录
默认状态与部署次序
首次部署 iam.v2_enabled、iam.v2_management_enabled、iam.v2_launch_enabled 都默认 false。全局未启用时不登记新来源会话,原密码、企微、IAM 入站登录保持原路径。产生v2关联后,全局核心必须保持true以持续中央停用检查、签名事件和代际补偿;维护/回退优先关闭management与launch两个新流程开关,不能以关闭全局核心绕过这些检查。本文只描述源码;生产开关、迁移、真实浏览器及三端联调需另行验收。
- 备份部署版本与配置,审查并执行
server/database/migrations/20261029_iam_v2.sql(按实际表前缀调整)。此迁移只加七张表及独立菜单/权限,不给现有角色自动授予权限,不改账号与角色。 - 中央先部署 v2 绑定墓碑、幂等恢复、固定来源回查、版本检查、源/目标访问授权、密码设置协议与签名事件。旧 v1 token 不得获得新管理权限。
- 向 IAM 注册 application
zyt的固定 HTTPS 回调;ZYT→ERP 固定目标zmxy-erp;IAM 管理后台/员工工作台可进入本人 ZYT 或 ERP,不增加 ERP→ZYT 按钮。OIDC client 追加精确 self-link 回调,禁用任意重定向。 - 用一次性合成账号完成三端测试后再开启 v2。配置放服务器私密环境,不写仓库或日志。
[IAM]
V2_ENABLED = false
V2_MANAGEMENT_ENABLED = false
V2_LAUNCH_ENABLED = false
V2_TOKEN = <dedicated-login-v2-credential-at-least-32-chars>
V2_MANAGEMENT_TOKEN = <different-management-v2-credential-at-least-32-chars>
V2_INTROSPECTION_TOKEN = <distinct-callback-credential-at-least-32-chars>
V2_EVENT_SECRET = <distinct-HMAC-event-key-at-least-32-chars>
ERP_URL = https://<ERP-origin>
ERP_CALLBACK_URL = https://<ERP-origin>/basic-api/api/erp/iam/v2/launch/callback
PORTAL_URL = https://<IAM-portal-origin>
V2_SOURCES_JSON = {"iam-admin":"https://<IAM-origin>/admin/v1/iam/v2/launch/continue","iam-portal":"https://<IAM-origin>/portal/v2/launch/continue"}
中央credentials数组分别注册:登录凭据 V2_TOKEN 仅 binding:read,binding:self,password:setup,launch:issue,launch:accept;管理凭据 V2_MANAGEMENT_TOKEN 仅 identity:search,identity:create,binding:read,binding:manage。identities/operations/audit/batch/jobs固定使用管理凭据,缺失时不fallback登录或v1凭据;target zyt 只接受 iam-admin / iam-portal。
API_URL、PUBLIC_URL、ISSUER、OIDC client 配置沿用原配置。ERP_URL/PUBLIC_URL/PORTAL_URL 是固定 origin,不接受浏览器 URL。新功能的 v2 凭据与原 APPLICATION_TOKEN 分离。
固定端点
源应用所有路径前缀 /adminapi/iam/v2:
POST /proofs/introspect、POST /sessions/check:专用 Bearer callback credential,不接受业务 token 代替。回包为中央约定的原生 JSON,不包业务 envelope。POST /events/revocation:X-IAM-Signature=sha256=<HMAC-SHA256(raw-body)>,X-IAM-Event-ID必须等于body.id;RFC3339 occurredAt、未来最多 60 秒,持久化 event ID + digest 幂等;耐久outbox旧时间事件仍可重试,不因超过5分钟被拒绝。旧 bindingVersion 只撤销旧/未知来源代际,不覆盖较新关联或另一员工的新会话;同业务token仍有新代际SID时保留token。历史本地无镜像时按签名事件externalAccountId或中央employeeId精确查询补齐映射,不从姓名/手机号猜测;无代际人员/授权事件在event handler回查当前中央状态后决定撤销,避免晚到事件踢掉重新授权会话。POST /launch/start:同源、当前业务 token + 独立 HttpOnly source SID;只返回目标/iam/launch?source=zyt&intent=...,在新标签页打开。GET /launch/continue?transaction_id=...&intent=...:IAM 注册的固定来源 continuation;目标 prepare 必须原样附加 opaque intent。比对源 HttpOnly intent cookie、源 SID、源 sessionVersion、固定两应用 ID 后,隐藏表单 POST 到ERP_CALLBACK_URL(生产公开反代前缀为/basic-api/api/erp/...,必须与中央注册精确一致)。grant 和业务 token 均不进 URL。POST /self-link/start→GET /self-link/callback:当前本地近期认证 + OIDC code/PKCE/nonce 证明 IAM 同一用户;后端提交已验证 ID token,中央独立验证。前端没有指定个人 employee ID 的能力。POST /password-setup→GET /password-continue?request_id=...&state=...:只对本人已验证且待设置密码的 IAM 账号;中央初始返回 url/expiry/requestId,源 cookie + SID 绑定请求;continuation 隐藏表单 POST 到固定PORTAL_URL/portal/password-setup/authorize。实际密码只在 IAM 页面输入。
用户 API 使用原业务 {code,data,msg} envelope。管理:accounts/identities/operations/audit/batch/jobs。每次 mutation 使用前端稳定 Idempotency-Key,自动 HTTP retry 关闭;中央按同一业务 payload 和 key 恢复已执行结果(proof 可刷新,不重复副作用)。operation/job 查询限定创建操作者及当前目标数据范围。
账号、会话与权限
- 新建来源会话保存独立浏览器 SID、原数据库 session ID、业务 token 的 SHA-256、账号凭据版本指纹、绑定employee/version快照、不可被续期更新的认证方法/时间、撤销版本。数据库会话轮换/到期、账号停用/删除、密码/企微关联/管理员标记改变立即使来源证明失效。
- 密码登录登记前再次真正核对密码 hash,不接受开发免密开关。企微入口的 TLS 必须校验后才登记
wecom。旧会话首次通过密码验证,或用原企微入口重新登录后登记;普通未关联业务使用不受影响。OIDC 使用原始auth_time,缺失时不算近期认证。 - 每次管理写要求 5 分钟内认证、独立动作权限、现有 DataScope。权限:
iamV2/read|bind|create|unbind|rebind|batch;不存在“未登记路由默认许可”的依赖。 - root、管理员/系统管理员/超级管理员角色或持有 IAM 管理写权限的目标视为敏感账号;仅本地 root/管理员类角色具备对应动作权限并完成近期核验时,可提交单人待审操作。中央返回 pending +
PROTECTED_ACCOUNT_REVIEW_REQUIRED+ 固定/admin/iam/review?request=...;界面新标签中央复核,批准后保持相同请求体/幂等键、新 proof 重试。无中央批准仍不执行,普通 IAM 管理角色不具备此特权。批量预检将其列为 review,不发送中央,不静默跳过核验。 - 中央身份和授权是真源;本地绑定表是最长 30 秒快照。已关联账号的活跃业务请求在快照超时后回查;失权、版本变更、签名事件撤销来源 SID 和业务会话。服务不可用时相关受保护请求失败关闭。不宣称空闲客户端会在 30 秒内主动收到推送。
- 解绑/换绑保留本地业务账号和历史记录。
iam_local_identity是开户幂等账本,不是历史绑定全量镜像,永不删除。v2 撤销墓碑拦截旧账本自动重绑;中央旧版 v1 必须同样维护墓碑。较旧或相同版本的 verified 状态不能覆盖本地 revoked。binding_revoked.bindingVersion是旧代际撤销上界;人员停用/应用失权事件只撤销会话并过期快照,不建立永久绑定墓碑。中央审核的新显式绑定可恢复登录,但自动开户分支仍拒绝墓碑。 - 管理列表来自所有可见本地账号并回查中央,首次展示历史绑定;按状态筛选基于已对账快照,未读取过的历史行应先在“全部”中分页核验。姓名/手机号不是自动合并依据。批量用明确 local ID/employee ID 映射预检。
IAM 管理后台/员工工作台 → ZYT target
中央注册 zyt.launchUrl=PUBLIC_URL/admin/iam/launch、targetDeliveryUrl=PUBLIC_URL/adminapi/iam/v2/launch/callback;V2_SOURCES_JSON 与中央两条 sourceContinueUrl 必须相同,来源只允许 iam-admin/iam-portal。
前端 /admin/iam/launch 先清除 URL 中的 intent/ticket,再调用 POST /launch/prepare。后端保存固定来源、PKCE、浏览器 hash 与已有本地账号,设置 HttpOnly Secure SameSite=None target cookie。中央 sourceContinueUrl 的 transaction_id 必须一致,后端追加原 opaque intent;已有 KC cookie 不是必要条件。
POST /launch/callback 核验固定来源 Origin、target cookie、单次事务后兑换 grant;已登录用户冲突在任何自动开户前拒绝。仍通过旧 IamAccountProvisioner 的持久账本/角色2开户,已有账号角色/密码不改。只读回查中央绑定完成后产生60秒一次票据。POST /launch/exchange 再比对当前 token 和独立 source cookie,成功才调用 LoginLogic 签发本地token,认证方法标记 iam_launch 并保留真实原authTime。账号不一致只显示显式退出当前浏览器并返回IAM重新发起,不静默替换。
目标来源代际保存在 iam_v2_target_session。活跃业务请求最长30秒调用中央 /launch-authorizations/check,仅检查employee/target授权与来源绑定代际,不把普通source logout当作全局登出。源 introspect/sessions-check 回调只查本地数据,绝不递归中央,避免中央state行锁死锁。
本地验证
php server/tests/IamV2SourceTest.php
php server/tests/IamV2TargetTest.php
php server/tests/IamV2RouteContractTest.php
NODE_PATH=admin/node_modules node server/tests/IamV2UiTest.mjs
php server/tests/IamLoginTransactionTest.php
php server/tests/IamProvisioningTest.php
php server/tests/IamOidcClientTest.php
NODE_PATH=admin/node_modules node server/tests/IamLoginUiContractTest.mjs
SQLite 合成测试不加载生产 .env、不发网络、不修改真实用户、角色或授权。UI 测试编译四个组件并执行新标签页处理逻辑,不能替代三端真实浏览器(Cookie/SameSite/新标签页)验收。原前端全量 typecheck 存量 60 条诊断,需按基线比较,不能批量掩盖。
回退
已有v2状态时先将 V2_MANAGEMENT_ENABLED=false、V2_LAUNCH_ENABLED=false,保持 V2_ENABLED=true 和当前安全核心;管理写/个人关联密码写/新的委托事务停止,普通授权核验、事件和代际补偿继续。仅可回到同样保持这些核心能力的兼容版本,不能盲退到无v2检查的旧代码。只有从未激活、七表无v2业务状态且全局仍false的首次部署,才可还原本次原文件与原index。保留七张新表、旧开户账本、事件与审计记录、所有真实账号和业务数据。关闭新流程开关停止新的委托登录/管理证明,不撤回中央已执行的人事操作;错误关联通过有审计的显式解绑/换绑恢复,而非删表或恢复旧数据库。
2026-10-29 只读生产入口核对与发布步骤
本轮只读核验(未部署、未迁移、未开关):
- SSH 别名
zyt;Nginx vhost/www/server/panel/vhost/nginx/admin.zhenyangtang.com.cn.conf的实际 root 是/www/wwwroot/zyt/server/public,PHP include 为enable-php-82.conf,现有 rewrite 经index.php?s=...进入 ThinkPHP。 - 运行仓库
/www/wwwroot/zyt的 HEAD 是e183e1cc62d3205576d5dfabd555e78bb86654c8;它与开发基线bcd42a5c399904ec3f9454c6fe1a74bde511ca2c不同,禁止整树覆盖。首版9个原文件哈希与开发基线完全一致,17个新增路径未碰撞;本轮追加改动仍须对最终发布manifest重新核对。 - PHP 二进制
/www/server/php/82/bin/php为 8.2.35;MySQL CLI 为5.7.44(此处不是数据库服务器版本声明)。服务私密配置留在/www/wwwroot/zyt/server/.env,本轮未读取或输出凭据。 - 公网只读
GET https://admin.zhenyangtang.com.cn/adminapi/iam/config返回code=1、enabled=true、既有/adminapi/iam/start登录地址;v2 仍未发布。 admin/scripts/release.mjs会先删除整个server/public/admin。不要在生产直接运行npm run build;它与docs/iam-quick-login.md要求的保留旧哈希资源冲突。仓库中的 Docker 示例不是当前 Nginx/PHP-FPM 运行入口,ERP ACR 发布脚本也不是 ZYT 发布入口。
得到发布授权后的受控步骤:
- 从指定已提交版本的干净副本构建,运行完整
scripts/test-iam-v2.sh、基线差分与vite build;只打包本次明确变更清单和前端构建,附各文件SHA256。不要携带.env、.local、上传目录或开发库。 - 只读核对线上所有被替换文件与发布前manifest哈希、新增路径无碰撞;若不一致先重审,不覆盖他人的新发布。保留服务器原文件/静态index及独立备份目录,保存私密配置备份与正常数据库备份。
- 在确认实际表前缀后执行
20261029_iam_v2.sql,使用既有受保护的MySQL凭据方式,不能把口令放命令行/日志。MySQL DDL不因后面的事务自动回退;逐表验证七表/索引和菜单节点,保留旧开户账本。菜单不自动给业务角色授权。 - 中央先完成v2迁移、凭据/固定URL/回查注册、原生IAM本人入口和中央复核部署;ZYT新代码先保持
V2_ENABLED=false。配置完整的API/portal/ERP公开callback URL及两条来源映射,尤其ERP公开/basic-api前缀。 - 后端按审核清单更新;静态资源先增量加入旧资源目录,最后原子切换index.html,不删旧哈希文件。按实际OPcache配置处理源码重载,不把CLI
opcache_reset()当成FPM已刷新,不重启无关服务。 - 先验证开关关闭时原登录/业务行为,再用授权测试账号开启v2验收:密码/企微→ERP、IAM管理后台本地session无KC cookie→ZYT/ERP、员工工作台→两端、同人/异人多标签、已有账号/角色2新开户、密码owner双阶段、敏感单人中央审批及批量跳过、签名事件/30秒补偿。逐项核对真实页面与后端会话,不以HTTP200代替登录验收。
- 初次未激活部署(全局false且七表空)可恢复发布前文件/index;一旦已有v2状态,回退优先只关闭管理/新登录两个开关、保持全局安全核心true,不盲退到旧代码;保留七表、事件、绑定历史、中央操作与所有业务数据。不要用整库恢复或删除IAM员工来回退代码。普通source logout按约定不是全局登出。
最终发布基线差异(追加)
最终32个变更路径核验:12个原文件中11个与开发基线相同,admin/src/stores/modules/user.ts 在线另有 serverTimeOffsetMs 状态、reset清理及 web_consultation_policy.server_time 的偏移计算;20个新增路径未发现碰撞。该生产逻辑必须保留。当前以bcd基线生成的整包前端仅为本地构建验证,不直接用于生产发布。 发布方应先获取/核验实际 e183e1c 及线上工作区差异,以其为发布副本应用本次IAM提交(保留时间偏移逻辑)再重新构建;不能通过覆盖生产user store或忽略差异来通过哈希校验。生产回退使用真实发布前备份,而非本地bcd基线副本。
保留生产前端的独立发布副本
只读补核了实际 admin/src、admin/public、index.html、package/lock、Vite/PostCSS/Tailwind、typings与TS配置,共710个输入,发现22个已变更文件和17个新增文件,无缺失或symlink。已有网页问诊/接诊、文字记录、复诊到期提醒/统计、处方订单、桌面下载等生产逻辑均按哈希快照保留。
独立前端副本位于 /Users/long/Work/zyt-iam-v2-release-20261029,分支 codex/zyt-iam-v2-live-frontend。它基于实际文件快照,而不是线上Git HEAD;仅应用10个受控IAM前端文件,705个非IAM生产输入逐字节不变。该副本仅用于前端构建,不使用其基线PHP目录发布后端。主实现树已三方保留生产user store的服务端时间偏移,并验证getUserInfo/reset行为。
没有读取生产.env:通过公开已部署JS核实 baseUrl="https://admin.zhenyangtang.com.cn//"、timeout=60000,最终构建显式使用 VITE_APP_BASE_URL=https://admin.zhenyangtang.com.cn/ 与 VITE_APP_REQUEST_TIMEOUT=60000 保持一致。发布审查应结合 frontend-production-manifest.json、production-frontend-drift.patch、release-frontend-manifest.json、public-build-config-match.json 与最终构建包,不采用前面bcd基线的旧构建结果。生产原文件CRLF/尾空白保留原样,不为通过格式检查改写业务源文件。