239 KiB
铁路无人机智能巡检平台统一身份认证与权限管理设计方案
版本:V1.0 编制日期:2026-08-01 适用基线:
AItrackwalker当前工作区代码、Docker Compose 部署和 Vue 3 管理端 设计范围:登录、人员、组织、身份来源、角色、功能权限、数据权限、服务身份、会话、审计、迁移与验收 核心技术:Spring Security、Spring Cloud Gateway、OAuth 2.0 / OpenID Connect、可选 SAML 2.0、Redis、PostgreSQL 结论口径:本文是实施设计,不把尚未提供的企业 IdP、人员主数据或生产证书视为已接入
1. 结论与推荐方案
本平台不应把“人员”“登录账号”“角色”继续当作同一个概念。推荐建设为以下四层:
- **统一认证中心(IdP)**负责密码、MFA、OIDC/SAML 协议、登录会话和外部身份代理。
- Spring Cloud Gateway作为浏览器和 API 的唯一入口,采用 BFF(Backend for Frontend)模式保存服务端登录会话,并向下游转发短期访问令牌。
- 巡检平台身份权限域负责平台人员、组织归属、角色、权限、数据范围、外部人员同步和审计,不保存外部系统密码。
- 各业务服务中的 Spring Security负责最终接口和数据授权;网关鉴权不能替代后端授权。
推荐的默认认证中心是自托管 Keycloak,原因是当前需求同时包含本地账号、OIDC、SAML、LDAP/AD 或其他身份源代理、账号管理和会话撤销。Spring Security 仍是网关与业务服务的安全执行框架,Keycloak 仅承担认证协议和凭据管理。若建设单位已有满足这些能力的统一身份平台,可以替换 Keycloak,而不改变网关、业务权限模型和接口契约。
不建议第一阶段直接基于 Spring Authorization Server 自研完整身份平台。它适合作为 OAuth 2.0/OIDC 授权服务器基础,但人员后台、SAML 身份代理、LDAP/AD 联邦、MFA 运维和账号生命周期仍需平台自行建设,首期成本和安全风险明显更高。
1.1 推荐拓扑
flowchart LR
U["浏览器 / 移动作业端"] --> G["Spring Cloud Gateway\n统一入口 + BFF 会话"]
U --> IG["IdP Continuity Gate\nwitness 租约 + fail closed"]
G --> F["Vue 3 前端静态服务"]
G --> B["巡检平台 Spring Boot\nResource Server"]
G --> IG
IG --> I["统一 IdP\n本地账号 + 身份代理"]
I --> O["企业 OIDC IdP"]
I --> S["企业 SAML IdP"]
I --> L["LDAP / AD(可选)"]
B --> P["PostgreSQL\n人员、角色、权限、审计"]
G --> CP["Security Control Plane\n撤销 / control-state / ack"]
B --> CP
CP --> P
G --> R["Redis\nGateway Session/OAuth 专用"]
G --> C["Redis\n限流与可丢弃授权缓存"]
B --> C
P --> RP["撤销 Outbox Projector\n顺序投影"]
RP --> SR["Security Revocation Redis\nnoeviction 快速拒绝"]
RP --> K["Kafka\nSSE 与安全广播"]
G --> SR
B --> SR
B --> UAS["无人机接入服务\nResource Server"]
B --> AI["AI / 制品内部服务\n服务身份"]
X["HR / 其他业务系统 / 文件"] --> SYNC["人员同步与导入"]
SYNC --> P
SYNC --> I
1.2 最重要的设计决策
| 决策 | 推荐结论 |
|---|---|
| 人员与登录账号 | 分离;一个平台人员可以绑定多个身份来源,也可以暂时没有登录身份 |
| 浏览器令牌存储 | 不将访问令牌写入 localStorage;网关以 HttpOnly Cookie 保存服务端会话 |
| 外部单点登录 | OIDC 优先,SAML 通过统一 IdP 代理;平台后端只信任一个内部发行方 |
| 本地自建账号 | 在统一 IdP 中创建,平台数据库不保存密码哈希或明文重置令牌 |
| 外部人员导入 | 建立独立的预检、差异、提交、回滚和冲突处理流程,不与首次 SSO 登录混为一体 |
| 授权模型 | RBAC 决定“能做什么”,数据范围决定“能操作哪些数据” |
| 权限执行 | 网关负责入口级策略;业务后端负责方法、对象和行级授权;前端只负责展示体验 |
| 操作人来源 | 一律从 Spring Security 认证上下文取得,不再信任请求中的 operator_id、created_by 等字段 |
| JIT 首次登录 | 生产默认关闭;可选只创建“待审核、无角色”人员,禁止首次登录自动获得业务权限 |
| 外部组映射 | 外部组先映射为受控平台角色,禁止直接把任意 IdP 组声明当作权限 |
| 数据范围合并 | 多角色的允许范围取并集;首期不实现显式拒绝,职责分离作为独立规则 |
| 停用与删除 | 人员、身份和角色绑定均软停用,历史任务、审批、告警、工单和审计不得失去主体引用 |
| 多租户 | 当前按单平台、组织树设计,不提前引入租户隔离;若未来多单位独立运营,再增加 tenant_id |
2. 建设目标与边界
2.1 建设目标
- 支持本地自建人员和账号登录。
- 支持通过 CSV/XLSX/JSON、REST API、数据库适配器或 SCIM 导入、同步其他系统人员。
- 支持企业 OIDC 或 SAML 单点登录,并可同时保留本地应急账号。
- 支持人员、组织、岗位、多组织归属、角色、权限和数据范围的页面管理。
- 支持按页面、按钮、接口、业务对象、组织、线路、任务和责任人进行权限控制。
- 支持用户停用、角色撤销、会话撤销和权限快速生效。
- 支持登录、同步、授权、拒绝、高风险业务动作的统一审计。
- 为后续移动端、第三方 API 和服务间调用提供同一套身份契约。
2.2 不在本设计中直接完成的事项
- 不在巡检平台自行实现密码学协议、MFA 算法或 SAML XML 安全解析。
- 不把大疆司空 2、纵横云或其他厂商的项目成员权限直接等同于铁路巡检业务权限。
- 不根据姓名、手机号或邮箱静默合并不同来源人员。
- 不允许前端隐藏按钮代替后端权限校验。
- 不在访问令牌中放入完整组织树、线路清单或大量动态权限。
- 不在企业 IdP、人员接口、证书和字段映射尚未提供时宣称生产 SSO 已验收。
3. 当前系统现状与差距
3.1 可复用基础
| 能力 | 当前落点 | 可复用方式 |
|---|---|---|
| 组织 | organizations |
保留组织主键和树结构,增加多组织成员关系和来源字段 |
| 人员 | platform_users |
保留 id 作为稳定业务人员标识,停止把它等同于单一登录账号 |
| 角色 | roles |
保留角色主表,逐步把 JSON 权限迁移到规范化关系表 |
| 用户角色 | user_roles |
迁移为带组织范围、授权来源和有效期的角色绑定 |
| 数据归属 | 多表已有 owner_org_id、assignee_user_id、created_by |
前两者可作为主要属性;created_by 仅在改为服务端主体并完成可信回填后用于授权 |
| 工作流权限 | FoundationCompletionService.requireWorkflowPermission |
迁移为统一 AuthorizationService,不再仅覆盖审批动作 |
| 业务审计 | platform_events、model_audit_logs 等 |
与安全审计关联,但不能替代安全审计 |
| Redis | 当前 Compose 已部署 | 本地可复用镜像;目标态拆为 Gateway Session/OAuth 专用实例、Security Revocation noeviction 实例和可丢弃限流/授权缓存域 |
3.2 现有缺口证据
| 缺口 | 当前证据 | 风险 |
|---|---|---|
| 后端无统一认证链 | platform/backend/pom.xml 未引入 Spring Security 或 OAuth2 Resource Server |
业务 API 默认匿名可访问 |
| 无网关服务 | infra/docker-compose.yml 由前端 Nginx 直接代理 backend:8080 |
无统一登录、会话、限流和入口策略 |
| 前端无登录状态 | frontend/src/services/api.ts 仅创建普通 Axios 客户端 |
无 401 跳转、CSRF、当前人员和会话过期处理 |
| 路由无权限元数据 | frontend/src/router/index.ts 仅定义标题和组件 |
所有人可进入全部页面 |
| 当前人员是静态文案 | frontend/src/layouts/AppShell.vue 固定显示“巡检调度中心 / 综合管理席” |
无法显示真实用户、角色和退出 |
| 操作人可由前端伪造 | 多处写死 user-dispatcher、user-approver、user-field |
审批、发布、工单和审计不可抵赖性不足 |
| 角色权限未规范化 | roles.permissions 为 JSONB,user_roles 无范围和有效期 |
难以查询、委派、审计和增量变更 |
| 数据权限未统一 | 仅少量工作流动作查询角色权限 | 列表、详情、下载、导出和统计可能出现越权差异 |
| UAV 服务仍用静态内部令牌 | InternalApiTokenFilter 校验 X-Access-Token;Actuator/文档和 FlightHub 回调绕过该令牌,其中回调由 Controller 另验 X-FlightHub-Event-Token |
多套长期静态令牌的轮换、服务身份、受众和最小权限不足 |
| Actuator 经前端 Nginx 暴露 | frontend/nginx.conf 代理 /actuator/ |
生产运维端点暴露面过大 |
当前主后端约有 225 个请求映射声明,无人机接入服务约有 10 个请求映射声明。实施时必须生成并维护完整的“接口—权限—数据范围—匿名策略”矩阵,禁止只保护新增的身份管理接口。
3.3 当前模型的迁移原则
- 不删除现有
platform_users.id,避免历史任务、告警、工单、审批和归档失去操作人。 - 现有种子用户映射为本地开发身份,逐步删除前端硬编码。
- 现有角色代码保持兼容,权限代码统一升级为本文定义的三段式稳定代码。
roles.permissions在迁移期保留只读兼容,最终以role_permissions为准。- 普通旧接口先在非生产做观察/影子计算再按业务域强制;系统管理、身份权限、飞行控制、审批发布、敏感读取和下载导出从首次接入即强制,生产不使用观察模式放行。
4. 总体技术架构
4.1 组件职责
4.1.1 统一身份提供者
参考实现采用自托管 Keycloak,职责包括:
- 本地用户名、密码、MFA 和密码策略;
- OIDC Authorization Code Flow;
- 外部 OIDC、SAML 身份代理;
- 可选 LDAP/AD 用户联邦;
- 登录失败锁定、会话、单点登出和密钥轮换;
- 管理 API,用于平台创建或停用本地身份;
- 服务账号和 Client Credentials。
IdP 不负责铁路巡检业务对象的数据权限,不直接维护线路、任务、告警或工单授权。
平台代码只依赖 IdpAdminProvider 接口,但 IdP 管理/连续性调用按权限拆为无公网入口的独立执行身份,普通 backend API Pod 不挂载任何 Admin Token:
identity-user-session-worker:只拥有已批准 realm 内的用户创建/停用、必要 action、会话查询/撤销权限,消费identity_provisioning_jobs;不得管理 realm、client、key 或管理员角色。identity-client-lifecycle-worker:使用另一 principal,仅能对service_clients中已登记的 client 执行禁用、重新启用和受控凭据轮换;不能创建任意 client、改变 redirect/audience/mapper、访问用户或 realm/key 配置。identity-continuity-sentinel:只读 IdP 数据库连续性元数据、realm 安全摘要和公开 keyset,续签外部 witness;不得读取用户 credential/Secret/私钥或写 IdP。security-recovery-controller:只持有 Kubernetes 围栏和 control-state 编排权限,不持有 realm、Redis、KMS 或数据凭据。keycloak-bootstrap-admin-job:只在所有 Keycloak 节点停止、网络硬隔离且双人审批的 FENCED 窗口,使用锁定的同版本 Keycloak 镜像和官方bootstrap-admin service命令,从 Vault/HSM 一次性 Secret 在 master realm 创建临时 admin service account;它只连接 IdP 数据库和 Secret 源,不进入常态部署。identity-idp-recovery-job:只有进入已审批的 FENCED 恢复流程后,才通过短期 Secret CSI/Vault 租约取得精确 recovery principal,用于按签名 manifest 对账、推进 not-before、撤销全量会话、轮换 Gateway/broker client 与 signing key,完成后立即撤销租约并卸载 Secret。
backend API 仅在数据库事务中创建幂等 job/outbox,worker 通过 allowlist Provider 执行并回写结果。所有管理调用限流、审计且不向前端返回 Admin Token、原始错误或 Secret;各 principal、Secret、Deployment 和 NetworkPolicy 必须分离,不能以一个 realm-admin 凭据覆盖全部能力。
4.1.2 Spring Cloud Gateway
建议新增独立模块 platform/gateway,采用独立的 Spring Cloud Gateway Server WebFlux 应用,只承担入口与 BFF 职责。现有两个 Spring Boot 服务继续保持 Servlet MVC,避免把 WebFlux 依赖混入业务后端:
oauth2Login()、oauth2Client()和登录回调;- Redis 服务端会话,以及 Redis/会话级
ReactiveOAuth2AuthorizedClientService;不得沿用 Token Relay 默认的单实例内存存储; - Token Relay,将当前会话的短期访问令牌转发给业务服务;
- CSRF、防重放、CORS、请求体限制和安全响应头;
- 路由、限流、统一 401/403、请求 ID 和访问日志;
- 清除客户端伪造的
X-User-*、X-Role-*等身份头; - 只执行粗粒度入口规则,不承担业务对象行级权限。
Gateway 必须成为浏览器访问平台的唯一入口。生产环境不得继续暴露后端、UAV 服务和 AI 服务的宿主机端口。
Gateway 使用与当前 Spring Boot 3.3.x 基线兼容的 Spring Cloud BOM 管理版本,不单独拼接未经验证的 Security、Gateway 和 Session 版本。版本升级必须经过登录、Token Relay、SSE、大文件上传和会话恢复回归。
4.1.3 巡检平台后端
现有 Spring Boot 业务服务增加:
spring-boot-starter-security;spring-boot-starter-oauth2-resource-server;- JWT 的 issuer、audience、有效期和签名校验;
JwtAuthenticationConverter到平台CurrentActor的转换;@EnableMethodSecurity和统一AuthorizationService;- JDBC 查询层的数据范围谓词;
- 安全审计和授权缓存失效。
后端是业务权限的最终裁决者。即使请求绕过前端按钮或构造对象 ID,也必须被方法权限和数据范围拒绝。
4.1.4 身份权限领域模块
第一阶段建议仍放在现有 platform/backend 内,按领域包隔离,不立即拆成微服务:
com.ai.trackwalker.identity
├─ api 当前用户、人员、组织、角色、权限、身份源、同步、审计接口
├─ authentication JWT 主体解析与平台人员绑定
├─ authorization 功能权限、数据范围、职责分离和委派边界
├─ provisioning 本地身份、外部导入、同步、JIT 和冲突处理
├─ securitycontrol 撤销账本、control-state、ack 与恢复状态机
├─ audit 安全审计、脱敏和查询
└─ config Spring Security 与身份配置
等人员同步量、独立发布或跨系统复用确有需要时,再抽取 identity-service;首期避免为了身份管理额外引入分布式一致性。
4.1.5 Security Control Plane
安全控制代码可先复用 backend 镜像和 identity/securitycontrol 模块,但必须以独立 security-control-plane profile、Deployment、ServiceAccount、数据库角色和无公网 Service 运行,不能与会被全局 fencing 终止的普通 backend API Pod 共用进程。它只监听 security-control-internal:8081,暴露 12.7 的精确 mTLS 撤销、control-state、ack 和 recovery 端点,直接访问强持久化 PostgreSQL;不加载业务 Controller,不接受浏览器 Cookie/公众 Bearer,也不依赖 Redis 完成自身认证。
该工作负载固定使用 rail.security/traffic=recovery 标签和单独的恢复 NetworkPolicy,在普通流量被隔离或所有业务 Pod 被终止时仍可用。workload registry 中全部 normal 工作负载、continuity gate/label controller 和恢复控制器只通过它读取/提交安全控制状态;普通 backend 可直接读取同库做本地判定,但仍通过该平面提交与 Pod UID 绑定的 ack。
4.1.6 IdP Continuity Gate 与流量标签控制器
identity-continuity-gate 是 IdP 公共域名前的独立轻量反向代理/sidecar Deployment,不与 Keycloak Pod readiness 合并。它以只读 mTLS 身份每 5 秒内刷新 security-control-plane 状态,仅当 recovery 正常、IdP witness 为 VERIFIED 且未过期、generation/realm/keyset digest 一致时,才把授权、登录、token、device、userinfo、introspection、logout、metadata/JWK 等公众身份路由转发到 Keycloak public Service;状态未知、过期或控制面不可达立即 503/fail closed。identity-admin.internal 的只读 digest/recovery Service 使用独立 listener、Service 和 recovery NetworkPolicy,不经过 public gate,也不因 public gate readiness 失败而被摘除。
凡运行时需要取得 rail.security/traffic=normal 的 Pod template 都永久以 quarantine 创建,并进入版本化 workload registry;范围不仅是 Gateway/Resource Server,还包括 frontend、普通 Outbox/projector、identity user-session/client-lifecycle worker、provisioning/sync、notification/异步 worker、CronJob/批处理等所有可发出业务命令或访问 normal 数据边的工作负载。固定 recovery 的 control plane、continuity gate/sentinel、label/fence controller 和命名恢复 Job 才是明确例外。
registry 为每种 workload 声明 init gate、主进程 attester、期望 ready phase、启动期最小依赖与 SecurityStartupBarrier。Java/Resource Server 可内置 attester/barrier;Nginx、Python、projector 或普通 worker 镜像使用同一受限 sidecar/启动包装器。准入规则拒绝任何 quarantine template 缺 gate/attester/barrier 或未登记的 workload。常驻 security-traffic-label-controller 只可读取 control-state/ack 和 patch 当前 Pod metadata 的这一枚标签;它确认同一 Pod UID 的 gate ack 后才改为 normal,不能改 Deployment/StatefulSet template、scale、Service、NetworkPolicy 或任意其他标签。
这里必须明确两阶段边界:为让 Spring/JPA 等主进程完成启动,Pod 在 gate ack 后先取得 normal 标签和相应网络可达性,主进程随后才可能提交 APPLICATION_READY/WORKER_READY;因此 ready ack 不是数据端口的 NetworkPolicy 开关,文档不宣称 ready 前完全无法建立数据库连接。该窗口由三层约束:Kubernetes readiness=false 使 Pod 不进入业务 Endpoint;直接 Pod IP 请求由最前置 startup filter 返回 503;应用 barrier 在 ready ack 前强制 Kafka listener autoStartup=false、暂停 scheduler/job claim/projector lease/outbound business client 和领域写入,只允许 registry 声明的配置、证书、control-state、数据库 schema validation/health 等启动依赖。生产 Flyway 使用独立受控 migration Job,应用运行角色无 DDL;确需兼容内嵌迁移时必须单独登记并审计。主进程加载完整 control tuple、本地健康和启动依赖后提交 ready ack,才释放 readiness、消费、任务和租约。这样正常滚动、HPA/CronJob 和灾后恢复产生的每个新 UID 都逐 Pod 通过同一可测试门禁,同时诚实区分网络可达性与业务执行许可。
4.2 浏览器登录和 API 调用时序
sequenceDiagram
actor User as 用户
participant Browser as 浏览器
participant Gateway as Gateway/BFF
participant IdPGate as IdP Continuity Gate
participant IdP as 统一 IdP
participant Backend as 巡检平台后端
participant Redis as Redis 会话
User->>Browser: 打开巡检平台
Browser->>Gateway: GET /
Gateway-->>Browser: 302 /oauth2/authorization/rail
Browser->>IdPGate: OIDC 授权请求
IdPGate->>IdP: witness 有效后转发
IdP->>User: 本地登录或选择企业 SSO
User->>IdP: 完成认证/MFA
IdP-->>Gateway: Authorization Code
Gateway->>IdPGate: 服务端换取令牌
IdPGate->>IdP: witness 有效后转发
Gateway->>Redis: 保存会话和 Authorized Client
Gateway-->>Browser: HttpOnly 会话 Cookie
Browser->>Gateway: GET /api/v1/me
Gateway->>Backend: Bearer access_token
Backend->>Backend: 校验 JWT,映射平台人员和权限
Backend-->>Gateway: 当前人员、角色、权限、数据范围摘要
Gateway-->>Browser: 返回页面初始化数据
4.3 多身份来源统一链路
flowchart LR
A["本地自建账号"] --> IDP["统一 IdP 主体"]
B["企业 OIDC"] --> IDP
C["企业 SAML"] --> IDP
D["LDAP / AD"] --> IDP
IDP --> AP["authentication_principals<br/>内部 iss/sub"]
AP --> PU["platform_users"]
B -. "外部主体" .-> UI["user_identities"]
C -. "外部主体" .-> UI
D -. "外部主体" .-> UI
E["HR / 业务系统导入"] --> UI
F["文件导入"] --> UI
UI --> PU
UI -. "登录身份可关联" .-> AP
PU --> OM["组织与岗位关系"]
OM --> RB["角色绑定"]
RB --> RP["权限 + 数据范围"]
核心规则是:外部登录成功只证明“此人是谁”,不自动证明“此人可以做什么”。平台角色和数据范围仍由受控授权流程决定。
5. 身份提供者选型
| 方案 | 优点 | 代价 | 结论 |
|---|---|---|---|
| 自托管 Keycloak | 本地账号、OIDC、SAML、LDAP/AD、身份代理、MFA、会话和管理 API 完整 | 新增一个基础服务,需要备份、升级和安全运维 | 推荐默认参考实现 |
| 直接接企业 IdP | 组件少,账号生命周期由企业统一维护 | 企业 IdP 必须同时满足本地/外部身份和管理要求;不同客户差异大 | 已有成熟统一身份时可替换默认 IdP |
| Spring Authorization Server 自研 | 完全由 Java/Spring 团队控制 | 需自行建设人员后台、SAML/LDAP、MFA、风控、会话运维 | 首期不推荐 |
| 每个外部 IdP 由 Gateway 直连 | 初期看似简单 | 多 issuer、claims、退出和账号绑定逻辑扩散到每个服务 | 不推荐作为长期方案 |
Keycloak 是参考实现而不是业务模型依赖。平台通过标准 OIDC、Admin Provider 接口和 identity_sources 抽象与 IdP 交互,避免业务代码直接依赖特定厂商对象。
6. 认证与会话设计
6.1 浏览器端采用 BFF
- 浏览器只持有随机会话 Cookie,不接触 access token 和 refresh token。
- Cookie 必须使用主机限定的
__Host-rail_session,设置HttpOnly、Secure、SameSite=Lax、Path=/,且不设置Domain;本地 HTTP 自测可使用单独的非生产 Cookie 名,不能把关闭Secure的配置带入生产。 - Gateway 使用 Redis 保存会话和 OAuth2 Authorized Client;不得使用只适合单实例的内存存储。实现自定义 Redis-backed
ReactiveOAuth2AuthorizedClientService,以registrationId + 内部 subject + sid/平台 sessionId形成会话级隔离键,采用带版本号的受控序列化和信封加密;刷新令牌更新使用 Redis 锁或 CAS,保证多 Gateway 实例并发刷新时只有一个请求调用 IdP,其余请求复用新结果。若所用 Spring Cloud Gateway 版本的标准接口只能取得principalName,则增加 session-aware Token Relay/Authorized Client 适配层,不能退化为“同一用户名的所有会话共享一个 refresh token”。 - 加密记录保存非敏感
schema_version/key_id/algorithm/nonce与密文;KEK 来自 KMS/Secret,代码和 Redis 均不保存明文主密钥。Gateway 维护“一个当前写 key + 多个仅解密旧 key”的 keyring:先把新 key 下发到所有新旧 Pod 并验证双读,再切换新写,旧 key 至少保留到最大绝对会话期、回滚窗口和旧密文清零之后,最后审计退役。回滚版本必须已具备新key_id解密能力;禁止先删旧 key 或让旧 Gateway 无法读取新写 Authorized Client。 - Redis 必须划分安全边界:
rail:gateway:session:*与rail:gateway:oauth:*仅 Gateway 会话凭据可读写,包含会话和敏感令牌;生产将它们放入只承载 Session/OAuth/反向会话索引的专用实例/集群,不与限流、授权缓存或撤销事实混用,以便灾备时整库失效且不读取令牌内容。rail:security:revocation:*是 PostgreSQL 撤销账本的安全投影。Gateway 的独立 revocation 凭据只能读取control:*,并在取得数据库账本确认后写sid:*快速 tombstone,不能写jti/principal/user/client/control;backend 普通身份 writer 只写 PostgreSQL,不持有 Redis control 权限;Outbox projector 使用独立凭据按顺序写sid/jti/principal/user/client,且只有正常 projector 或互斥的 recovery rebuild job 可写control:security-epoch、三个 token cutoff、control:authorization-required-epoch、control:idp-continuity-state、control:idp-witness-generation、control:idp-witness-expires-at、control:realm-security-digest、control:trusted-signing-keyset-digest与control:projection-watermark。各 Resource Server 只读所需 tombstone/control 子前缀;rail:authz:*和限流由另一凭据/淘汰域读写。backend 内使用不同 Redis ConnectionFactory/用户名区分只读认证链和 projector,普通请求代码不能取得 projector 凭据。不同 ACL Key Pattern 与命令白名单或独立 Redis 实例均可,但仅换逻辑 DB 号不构成安全隔离。backend 不得读取 Session/Authorized Client 键。 - Security Revocation Redis 使用独立实例/集群或至少独立且不可跨越的内存淘汰域,生产固定
maxmemory-policy=noeviction(或托管服务的等价保证),为最大 token/session 数和故障重放预留容量并设置 70%/85% 水位告警。达到高水位时停止签发新会话/服务 token并 fail closed,不能在 TTL 前静默淘汰 tombstone;可丢弃的rail:authz:*缓存不得与撤销事实共享 LRU/LFU 淘汰域。各区使用 TLS/受控内网、严格 TTL 和加密备份策略。 - 访问令牌建议短期有效,参考基线为 5 分钟;刷新令牌仅保存在 Gateway 服务端。
- 会话参考基线为 30 分钟空闲过期、8 小时绝对过期;正式值由建设单位安全制度确认。
- 高风险动作可要求近 10 分钟内完成 MFA 或二次认证,并检查
acr/amr。 - 所有状态变更请求启用 CSRF 防护;SPA 通过受控 CSRF Cookie/Header 机制提交令牌。
- 同源部署为默认方案,生产环境不开启宽泛 CORS。
Gateway 使用有序且互斥的 Security WebFilter Chain,不在一条链里猜测身份类型:
| 链路 | 匹配路径/请求 | 身份方式 | CSRF | 未认证响应 |
|---|---|---|---|---|
| 浏览器协议链 | /oauth2/**、/login/**、POST /logout |
OAuth2 Login + 服务端会话 | 登录回调按框架 state/nonce 校验;登出必须校验 CSRF |
受控重定向或协议错误页 |
| IdP 后台登出链 | 精确 POST /oauth2/backchannel-logout/rail |
验签后的 OIDC logout_token,不接受用户 Cookie/Bearer 代替 |
服务间协议不使用浏览器 CSRF;强制防重放 | JSON 200/400/401 |
| 浏览器页面链 | HTML 导航、SPA fallback | 服务端会话 Cookie | GET 不改状态;变更操作不放在页面路由 | 302 到登录入口 |
| 浏览器 API/SSE 链 | 除精确回调外的全部 /api/**,无论是否已有 Cookie |
仅服务端会话 + Token Relay;无 Cookie 直接未认证 | 非安全方法强制 X-CSRF-TOKEN |
JSON 401/403,绝不返回登录 HTML |
| 移动/第三方 API 链 | /mobile-api/**、/partner-api/** |
Bearer Resource Server | 关闭 Cookie/CSRF,要求独立 audience/scope | JSON 401/403 |
| 厂商回调链 | /integration/callbacks/** 及精确兼容别名 /api/v1/events/flighthub |
mTLS、签名或专用服务令牌 | 不使用浏览器 CSRF;执行时间戳/nonce 防重放 | JSON 401/403 |
| Gateway 会话管理链 | 独立内部端口 /internal/gateway/v1/sessions/** |
mTLS/Client Credentials + gateway.session.admin;拒绝 Cookie |
服务间请求不使用浏览器 CSRF | JSON 401/403,未知内部路径默认拒绝 |
| 健康探针链 | 独立管理端口精确 /actuator/health/liveness、/actuator/health/readiness |
仅管理端口/探针网络,无应用凭据 | 只读 GET | 200/503;不返回依赖细节 |
| 内部管理链 | 独立管理端口其余 /actuator/** |
管理凭据/mTLS + NetworkPolicy | 不接受浏览器业务 Cookie | 默认网络拒绝 |
公众端口链路顺序固定为“厂商精确回调/IdP 后台登出 → OAuth2 浏览器协议 → 移动/合作方 Bearer → 全部 /api/**/SSE → HTML 页面 → 默认拒绝”;内部/管理端口使用各自独立链且不监听公众 Service。通用 API matcher 显式排除精确回调;未知 callback 子路径不因前缀相似而放行。API 请求没有 Cookie 时仍由 API 链返回 JSON 401,绝不能落入页面链变成 302。任何同时携带平台会话 Cookie 和 Authorization: Bearer 的 API 请求一律返回 400/AMBIGUOUS_CREDENTIALS 并审计,防止凭据混淆。/logout 仅允许 POST,不提供可被跨站图片触发的 GET 登出。后端 Resource Server 保持无状态并关闭自身 CSRF,因为浏览器 CSRF 已在持有 Cookie 的 Gateway 层处理;后端仍验证 Gateway 转发的 JWT,不能只信任 Cookie 或身份头。
当前前端已有原生 EventSource 场景,浏览器不能方便地为其附加 Bearer Header。BFF 的同源安全 Cookie 可以自然覆盖 SSE 建连,这也是本方案不选择“SPA 将 Bearer Token 放入浏览器存储”的重要原因。SSE 建连后仍需在后端校验订阅对象的数据权限;连接按 sid + platform_user_id 登记,人员停用、会话撤销或关键权限回收事件到达时主动断开,另以最大连接寿命兜底强制重新认证。
移动 App 或受信第三方客户端不复用浏览器 Cookie。移动端注册为独立 OAuth 公共客户端,使用 Authorization Code + PKCE 和系统安全存储;机器客户端使用 Client Credentials。不同客户端使用独立路径、Security Chain、audience、scope、回调地址和撤销策略,不能与 BFF Cookie 身份混合解析。
6.2 Token 校验
业务服务至少校验:
iss:只接受配置的内部统一 IdP;aud:必须包含目标资源服务;- 签名算法、
kid与强控制面当前允许的签名公钥集合; exp、nbf、允许的时钟偏差;- 用户或服务主体状态;
- 必需的客户端和认证强度。
令牌建议只携带稳定、低变化的身份声明:
{
"iss": "https://identity.example/realms/rail",
"sub": "idp-stable-subject",
"aud": ["rail-platform-api"],
"sid": "idp-session-id",
"jti": "access-token-id",
"azp": "rail-gateway",
"preferred_username": "dispatcher",
"rail_login_source": "enterprise-oidc-a",
"rail_security_epoch": "2ff5d68c-9c1d-4d12-bb08-08af34b15484",
"acr": "RAIL-ACR-2",
"amr": ["pwd", "otp"],
"auth_time": 1893452400,
"iat": 1893455700,
"exp": 1893456000
}
Claim 最小集合按 token 类型固定:
| 类型 | 必需声明 | 说明 |
|---|---|---|
| 所有 access token | iss、sub、aud、iat、exp、jti、azp/client_id、rail_security_epoch |
签名、算法、时钟、客户端、安全代次均校验 |
| 用户/会话 token | 上述 + sid、auth_time、内部 acr、amr、rail_login_source |
支持会话撤销、step-up 和登录来源审计 |
| Client Credentials token | 通用集合 + scope/roles |
没有用户会话时不要求 sid/auth_time/rail_login_source |
rail_security_epoch 由统一 IdP 的受控、不可由客户端输入覆盖的 Protocol Mapper 写入所有 ID/access token;其期望值来自平台强持久化 control-state,而不是把令牌自带值当权威。Gateway 与每个 Resource Server 必须同时满足“令牌 epoch 精确等于当前 security_epoch_uuid”和“JWT kid 位于当前 trusted_signing_keyset”;声明缺失、多值、mapper 漂移、未知 kid 或只在本地 JWK 缓存可找到但已不在允许集合的旧公钥均拒绝。这样即使旧 Keycloak 数据库/私钥快照重新上线并签出 iat 很新的 JWT,也不能跨越新的随机安全代次或签名密钥围栏。
preferred_username、姓名和邮箱仅用于显示,不参与人员匹配或授权。保留声明缺失、类型错误或多个来源声明冲突时 fail closed。
客户端与 audience 使用允许对表:浏览器 BFF 为 rail-gateway -> rail-platform-api,移动端为独立公共客户端 -> rail-mobile-api,每个合作方使用独立 client -> rail-partner-api,UAV/AI/制品分别使用自己的服务 audience。Gateway 和后端同时校验 aud + azp/client_id + 外部路径类别;不签发一个可跨所有资源服务的万能多 audience 特权 token。
不建议把完整权限和数据范围全部写入 JWT。后端通过 (issuer, subject) 映射到 platform_user_id,再从数据库或 Redis 授权缓存读取当前权限,避免撤权必须等待长令牌过期,也避免令牌过大。
6.3 统一登录主体映射
统一 IdP 代理外部 OIDC/SAML 后,业务后端实际看到的是内部 IdP 签发的 iss/sub,不是上游 IdP 的原始 subject。运行时映射必须采用以下两级模型:
- 认证主体映射:
(internal_issuer, internal_subject)唯一映射到一个活动authentication_principal和一个platform_user,这是每次请求建立主体的唯一认证键。 - 上游身份关联:
(source_id, external_subject)用于人员预配、身份绑定和来源审计,通过user_identities关联到认证主体;它不直接作为业务后端解析 JWT 的键。
内部 IdP 通过受控 Protocol Mapper 把本次登录方式写入签名声明,例如 rail_login_source=LOCAL 或 Keycloak broker session note identity_provider。后端先用内部 iss/sub 找到人员,再把登录来源解析为具体 user_identity。本地登录、OIDC broker 和 SAML broker 的来源映射规则必须版本化。
以下情况一律 fail closed,不按用户名、姓名、邮箱或手机号兜底:
- 内部
iss/sub没有唯一活动主体; - 主体关联人员为
PENDING/SUSPENDED/DISABLED/LEFT; - 签名声明指向外部来源,但对应身份为
PENDING/CONFLICT/DISABLED; - 一个内部主体映射到多个人员,或一个外部主体出现冲突;
- 本次登录来源无法按配置解释,而身份源要求强制来源审计。
Keycloak First Broker Login Flow 必须关闭按邮箱静默自动链接。账号链接只能基于已预配的稳定外部 ID或经重新认证、人工确认的绑定流程。一个 Keycloak 内部用户可以关联多个外部身份,但内部 iss/sub 仍只能归属一个平台人员。
6.4 MFA 与二次认证
平台定义自己的认证强度,而不是直接信任任意上游字符串:
| 内部等级 | 含义 | 典型用途 |
|---|---|---|
RAIL-ACR-1 |
单因素或企业基础登录 | 普通查询和低风险操作 |
RAIL-ACR-2 |
受信 MFA | 人员停用、普通审批、数据导出 |
RAIL-ACR-3 |
近期 step-up + 受信 MFA | 飞行高风险命令、特权授权、模型生产发布、身份源密钥变更 |
- 每个身份源配置“上游 ACR/AMR → 内部 ACR”的白名单,未知值最多映射到最低等级。
- 内部 IdP 的认证流程负责签发可信
acr/amr/auth_time;后端只信任内部签名后的映射结果。 - 高风险接口同时检查最低 ACR 和
auth_time,参考要求为最近 10 分钟完成认证。 - 不满足时返回 403、
reason_code=STEP_UP_REQUIRED、required_acr和受控return_to标识。 - 前端调用 Gateway step-up 入口,Gateway 发起带
acr_values、max_age=0的重新认证;成功后使用原幂等键重试业务动作。 - 不同上游无法证明相应强度时,必须切换内部 MFA 或拒绝该高风险动作。
6.5 当前主体模型
后端统一生成不可由请求覆盖的 CurrentActor:
actor_type USER | SERVICE | SYSTEM
platform_user_id 平台稳定人员 ID;服务主体为空
principal_id 内部认证主体记录
identity_id 本次登录来源对应的上游身份记录
issuer / subject 令牌发行方与主体
display_name 展示名快照
primary_org_id 主组织
org_ids 当前有效组织归属
permission_set 当前功能权限
data_scopes 当前数据范围
authz_version 授权版本
session_id 会话标识
security_epoch 从强持久化安全控制状态解析的当前代次,不直接信任请求声明
token_issued_at JWT `iat` 的规范化值,用于全局 user-token-valid-after 比较
auth_time 用户最近一次完成认证的时间,用于阻止旧 refresh/offline session 换发新 token
acr / amr 认证强度
request_id 请求追踪标识
现有接口中的以下字段必须逐步废弃:
operator, operator_id, operator_name,
created_by, createdBy, requested_by, requestedBy,
approvedBy, publishedBy, uploadedBy, confirmedBy
兼容期内可以保留字段以避免旧客户端立即失败,但服务端必须忽略其身份含义并覆盖为 CurrentActor;若客户端仍提交,记录迁移告警。兼容期结束后从 DTO 删除。
6.6 登出、停用和撤权
- 普通登出:先按本节耐久命令提交
sid/user撤销事实,确认后再清除 Gateway Redis 会话/Authorized Client 并调用 IdP 结束会话;即使后续清理失败也保留可重试账本。 - 管理员停用:在同一数据库事务内停用平台人员/身份并写撤销账本与 Outbox,随后撤销 IdP 会话、清除授权缓存;不得先销毁唯一主体/会话上下文再尝试记账。
- 高风险撤权目标:60 秒内对新请求生效,不仅依赖 access token 到期。
- IdP 后台登出或 Back-channel Logout:IdP 只以
application/x-www-form-urlencoded向精确POST /oauth2/backchannel-logout/rail发送logout_token。Gateway 验证签名、算法、iss、aud、iat、exp、jti重放、events必含 backchannel-logout 事件,以及sid或sub;若存在typ则只接受logout+jwt,并优先在 IdP 配置该推荐类型。拒绝普通 access token、包含nonce的异常 token 和未知 client。验证后先以sid/sub提交耐久撤销事实并取得账本确认,再删除会话/写 Redis 投影;账本失败时返回可重试错误。 - 角色变更:递增
platform_users.authz_version,发布缓存失效事件。 - 已停用人员的历史业务主体仍可展示,显示“已停用”而非删除名称。
Gateway 在 Redis 维护 platform_user_id/principal_id/sid -> gateway_session_id 的双向会话索引,并通过独立内部 Service/端口上的 Session Administration API 提供查询和撤销;该端点不挂载公众 Gateway 路由,只接受 backend 身份模块的 mTLS/Client Credentials 和 gateway.session.admin scope。平台后端不直接解析 Spring Session 的内部序列化。统一会话页面合并 Gateway 浏览器会话与 IdP 的移动/SSO 会话,标明来源和能力。列表展示使用平台生成的会话显示 ID,不暴露 Redis Key、refresh token 或完整 sid。
浏览器、移动端、后台登出和管理员强退都使用同一耐久命令:Gateway 生成稳定 event_id,以独立 mTLS 工作负载身份和 security.revocation.write 调用 security-control-plane 内部端口的 POST /internal/security/v1/revocations;control plane 在一个 PostgreSQL 事务内幂等写入 security_revocations 与 security_revocation_outbox 后返回 DURABLE_ACCEPTED + ledger_sequence。随后 Gateway 删除自己的 Session/Authorized Client;存在 sid 时可用受限凭据写 sid:* 快速 tombstone,再调用 IdP logout;只有 sub 时不越权写 user/principal key,由顺序 Outbox projector 完成全部 Redis/Kafka 投影。Redis/Kafka 短时故障不丢撤销事实。数据库确认不等于“全局撤销成功”;只有投影达到该序号且所有当前可服务的目标 Resource Server RUNTIME ack(或已摘流/终止)后才可使用该表述。浏览器在内部命令暂不可用时仍立即清除本地 Cookie,但页面必须明确“本机已退出、全局撤销待恢复”并告警,不能返回“全部会话已撤销”;Back-channel Logout/管理员强退则返回可重试失败,不能伪造成功。
删除 Gateway 或 IdP 会话不会自动使已经签发的 JWT 失效。为满足快速撤权,所有用户/会话型访问令牌必须包含 sid 和 jti;Client Credentials 服务令牌至少包含 jti,并支持按 client_id 紧急禁用。后端在认证后检查 Redis 中的撤销/禁用索引:
rail:security:revocation:sid:v{hash_version}:{issuer_hash}:{sid_hash}
rail:security:revocation:jti:v{hash_version}:{issuer_hash}:{jti_hash}
rail:security:revocation:principal:{principal_id}
rail:security:revocation:user:{platform_user_id}
rail:security:revocation:client:{service_client_uuid} # status + generation + cutoff + attested_security_epoch
rail:security:revocation:control:security-epoch
rail:security:revocation:control:user-token-valid-after
rail:security:revocation:control:user-auth-valid-after
rail:security:revocation:control:service-token-valid-after
rail:security:revocation:control:authorization-required-epoch
rail:security:revocation:control:idp-continuity-state
rail:security:revocation:control:idp-witness-generation
rail:security:revocation:control:idp-witness-expires-at
rail:security:revocation:control:realm-security-digest
rail:security:revocation:control:trusted-signing-keyset-digest
rail:security:revocation:control:projection-watermark
issuer_hash 使用平台固定算法从 canonical issuer 计算,避免把 URL 分隔符直接拼入 ACL Key。sid/jti 查询键统一使用版本化 HMAC-SHA-256;Resource Server 对已验签 claim 本地计算当前和轮换窗口内的 hash,Outbox/Redis/Kafka 只携带同一 hash,任何一层都不混用原始值。HMAC key 通过只读 Secret/KMS keyring 下发,记录 hash_version/key_id;先全量部署双读能力,再切换新写。旧 key 只有在所有使用该版本的 sid/jti 账本事实均超过各自 session、refresh/offline session 和 token 最长有效期且清理水位已审计确认后才能退役,不能统一只等 access-token TTL。服务 client 则先按受信 (issuer_hash, client_id) 注册表映射为稳定 service_client_uuid,client 状态键不依赖可轮换 HMAC;未知 client、缺失状态键或过期投影均 fail closed。该键在 client 存续期间不设置普通 token TTL:DISABLED/ARCHIVED 始终拒绝;ACTIVE 还必须满足 attested_security_epoch == control.security_epoch、token iat >= max(service_token_valid_after, token_valid_after) 且 generation/ledger watermark 新鲜。新随机 security epoch 会让数据库回退出的旧 ACTIVE 记录自动失配,rebuild job 禁止替 client 填写新 epoch。安全 cutoff 使用整数 epoch 秒精确比较,不套用 JWT 验签的宽松时钟偏差。单会话撤销写入 sid_hash,泄露令牌撤销写入 jti_hash,人员全部会话撤销写入 user/principal 禁用索引并调用 IdP Admin API。Back-channel Logout、移动端退出和管理员强退都先进入 PostgreSQL 撤销账本,再投影到 Redis/Kafka。
security_epoch、user_token_valid_after、user_auth_valid_after、service_token_valid_after、authorization_required_epoch_uuid、idp_witness_generation/idp_witness_expires_at 与 trusted_signing_keyset_digest 的权威值位于 PostgreSQL security_control_state,Redis 只保存带账本序号的投影。所有用户 token 除常规声明外必须同时满足 rail_security_epoch == security_epoch、iat >= user_token_valid_after 和 auth_time >= user_auth_valid_after;后者阻止旧 refresh/offline session 在恢复后换出带新 iat 的 access token。所有 Client Credentials token 必须满足相同 epoch、可信 kid 和 iat >= service_token_valid_after,并继续满足 per-client status/generation/cutoff;全局 cutoff 防止数据库回退丢失近期 client 禁用或轮换事实时灾前服务 JWT 复活。上述安全 cutoff 都使用整数秒精确比较,不应用负向时钟宽容。业务授权图只接受已按当前 authorization_required_epoch_uuid 和对象版本再证明的节点/边,数据库回退出的旧 ACTIVE 用户、绑定或宽范围没有当前证明即不生效。每个 Gateway Session 保存创建时的 security_epoch,使用/刷新前必须等于当前值。Gateway 和每个 Resource Server 使用独立服务身份定期调用 GET /internal/security/v1/control-state,只缓存不超过 5 秒的 mTLS 可信结果,并把返回版本与 Redis projection watermark 比较;backend 自身直接读 PostgreSQL。所有判断使用强数据库时间或与其有界校准的节点时钟,并要求 now < idp_witness_expires_at;control-state 过期/不可达、witness 租约过期、Redis 拓扑变更、watermark 回退/缺失、IdP continuity 状态不是 VERIFIED 或系统状态为 FENCED/REBUILDING 时,全部用户请求和内部命令 fail closed,只保留健康检查与受控恢复入口,不能让同一份陈旧 Redis 投影或最后一次 VERIFIED witness 自证为 NORMAL,也不能把“不存在键”解释为“未撤销”。
同一 control-state 还以 idp_continuity_state 和 realm_security_digest 约束身份入口及所有接收方;两者与 witness generation/expiry、keyset digest 任一不匹配时,不得只凭签名或 iat 放行。
正常运行时,每个会处理请求、消费事件、领取任务或取得 projector lease 的 normal 主进程都在读取新的连续 ledger sequence、确认所需 watermark一致后,以 RUNTIME attester 持续更新当前 Pod UID 的短期 ack;纯静态 frontend 也要由受限 attester 在本地健康后提交启动 ready ack。启动 readiness/开始消费/领取 lease 都要求先完成当前序号和 registry 指定 phase 的 ack。部署控制器维护必须执行撤销检查的 workload registry,传播协调器只把“已 ack 的当前可服务/可执行 Pod”或“已摘流、停止消费且终止的 Pod”计为完成。Pod 网络分区后最多使用 5 秒缓存,随后即 fail closed/停止消费;未 ack 但仍可服务或执行命令的 Pod 会阻止全局生效状态。
所有接收 Client Credentials 的 Resource Server(backend、UAV、AI、制品等)在本地验签后还要以只读 ACL 检查 jti、全局 service cutoff、client 当前状态/cutoff/generation 与 attested security epoch;IdP 禁用/轮换阻止旧 secret 签发,epoch attestation 防止回退数据库把旧 ACTIVE 自动恢复,cutoff 防止禁用前 token 在重新启用后复活。暂不能接 Redis 的遗留服务只能在受控迁移期使用不超过 2 分钟 token 并对高风险动作启用 introspection,最终仍统一撤销检查。撤销区不可用、投影不新鲜或安全控制状态未知时,全部用户请求和所有内部命令接口 fail closed。
6.7 IdP 故障策略
| 场景 | 推荐行为 |
|---|---|
| 新登录时 IdP 不可用 | 拒绝新登录,展示明确故障页,不降级为匿名 |
| 已登录且 access token 仍有效 | 仅在 control-state 可达、IdP continuity witness 仍为 VERIFIED 且未过期、epoch/keyset/watermark 均匹配时继续到令牌或会话策略允许的时间点;witness 到期即 fail closed |
| 需要刷新令牌时 IdP 不可用 | 结束会话并提示重新登录,不无限延长 |
| 上游企业 IdP 不可用、内部统一 IdP 仍可用 | 可使用内部 IdP 的独立应急管理员账号、MFA、双人保管和高优先级审计 |
| 内部统一 IdP 进程不可用、数据库 continuity 仍可由 sentinel 证明 | 不允许 Gateway 绕过认证;新登录关闭,既有令牌仅在未过期 witness 与全部本地校验通过时继续;sentinel/数据库也不可达则短租约到期后全请求 fail closed |
| IdP 计划内切换且数据库 timeline/LSN、外部 witness 和 desired-state digest 均可证明连续 | 身份入口先保持关闭,continuity sentinel 验证后再开放;平台既有请求仍按当前 token/会话策略运行 |
| IdP 故障转移、PITR、数据库恢复或 realm/key 结果无法证明连续 | 立即关闭 IdP 身份入口并触发与业务数据库不确定恢复相同的外部维护入口和应用硬围栏;不得先恢复登录/换 token 再补检查 |
| 发现 IdP 回退或配置/密钥 digest 漂移 | 生成全新 security/authz epoch 与 cutoff,清会话,轮换签名密钥和全部相关客户端凭据,人员凭据/MFA 按 17.4.5 再验证后方可开放 |
| Gateway/Redis 不可用 | 登录会话失败,不绕过网关直连后端 |
7. 人员、身份与组织数据模型
7.1 领域关系
erDiagram
ORGANIZATIONS ||--o{ USER_ORG_MEMBERSHIPS : contains
ORGANIZATIONS ||--o{ ORGANIZATION_EXTERNAL_REFS : maps
PLATFORM_USERS ||--o{ USER_ORG_MEMBERSHIPS : belongs
PLATFORM_USERS ||--o{ AUTHENTICATION_PRINCIPALS : authenticates
AUTHENTICATION_PRINCIPALS o|--o{ USER_IDENTITIES : optionally_links
IDENTITY_SOURCES ||--o{ USER_IDENTITIES : issues
PLATFORM_USERS ||--o{ ROLE_BINDINGS : granted
ROLES ||--o{ ROLE_BINDINGS : binds
ROLE_BINDINGS ||--o{ ROLE_BINDING_SCOPES : restricts
ROLES ||--o{ ROLE_PERMISSIONS : contains
PERMISSIONS ||--o{ ROLE_PERMISSIONS : grants
IDENTITY_SOURCES ||--o{ IDENTITY_SYNC_JOBS : runs
IDENTITY_SYNC_JOBS ||--o{ IDENTITY_SYNC_ROWS : details
PLATFORM_USERS ||--o{ SECURITY_AUDIT_EVENTS : acts
USER_IDENTITY 对 AUTHENTICATION_PRINCIPAL 是 0..1:纯人员同步/文件来源可以没有认证主体;一旦 login_enabled=true,必须且只能关联一个内部认证主体。
7.2 表设计
7.2.1 platform_users
继续作为平台稳定业务人员主表,建议增加:
| 字段 | 说明 |
|---|---|
account |
迁移为可空的人员别名/兼容字段,不再作为认证键;非空时可保留条件唯一索引 |
employee_no |
工号,可空,不作为跨来源唯一键 |
email、mobile |
联系信息,按字段权威来源更新 |
user_type |
EMPLOYEE、CONTRACTOR、EXTERNAL、SYSTEM_ADMIN |
primary_org_id |
主组织,兼容现有 org_id 后迁移 |
status |
PENDING、ACTIVE、SUSPENDED、DISABLED、LEFT |
authz_version |
权限版本,角色或组织变更时递增 |
source_summary |
仅用于展示的来源摘要 |
disabled_reason、disabled_at |
停用原因和时间 |
created_by、updated_by |
从认证主体获取 |
version_no |
乐观锁 |
人员记录可以没有登录身份,例如只作为工单责任人或历史人员存在。
现有 platform_users.account NOT NULL UNIQUE 必须通过向前兼容迁移解除“一个人员只能有一个登录名”的语义。运行时认证唯一性转移到 authentication_principals(issuer, subject),上游来源唯一性使用 user_identities(source_id, external_subject);旧 account 在兼容期仅用于检索和显示,不能参与密码校验或跨来源自动合并。
7.2.2 identity_sources
| 字段 | 说明 |
|---|---|
id、code、name |
身份源标识 |
source_type |
LOCAL_IDP、OIDC、SAML、LDAP、SCIM、REST、FILE、DATABASE |
source_category |
AUTHENTICATION、PROVISIONING、HYBRID |
supports_login、supports_provisioning |
明确该来源能否登录、能否同步人员 |
issuer_uri |
仅 OIDC/SAML 等认证来源使用;文件/REST/数据库来源为空 |
idp_alias |
统一 IdP 中的代理别名 |
provisioning_mode |
PRE_PROVISIONED、JIT_PENDING、JIT_ALLOWED、SYNC_ONLY |
matching_policy |
外部主体匹配规则 |
field_authority |
哪些人员属性由该来源权威维护 |
authoritative_for_status |
是否可以决定整个平台人员的在职/停用状态 |
authoritative_for_org |
是否可以决定组织和成员关系 |
authority_rule_version |
引用版本化字段/能力权威规则,不能只靠两个布尔值解决多来源优先级 |
secret_ref |
密钥引用,不存明文 |
status |
DRAFT、TESTING、ACTIVE、DISABLED、ERROR |
last_sync_at、last_success_at |
运维状态 |
认证源与人员同步源共用一个来源注册表,但能力、必填字段、网络策略和停用权限完全按 source_category 与能力标志区分。不能登录的 FILE/REST/DATABASE/SCIM 来源不得创建可登录身份或填写伪造 issuer。
多来源权威使用独立、版本化规则,建议表为:
identity_source_authorities(
id, source_id, authority_domain, field_name,
priority, conflict_policy, valid_from, valid_to,
rule_version, approved_by, status
)
authority_domain 至少区分 EMPLOYMENT_STATUS、PRIMARY_ORG、MEMBERSHIP 和 PROFILE_FIELD。每个人员/字段在同一时刻原则上只能有一个最高优先级权威源;更低优先级只能补空值,不能覆盖。若同优先级权威源给出不同在职状态,立即把人员置为 SUSPENDED/CONFLICT 并撤销会话;若主组织或成员关系冲突,保留最后一次已批准关系但冻结新的范围扩张,冲突行进入人工裁决。禁止用“最后写入获胜”。规则变更属于高风险配置,需版本、审批、影响预览和审计。
7.2.3 authentication_principals
保存业务后端实际验证的内部 IdP 主体:
id, user_id, issuer, subject, idp_user_id,
status, last_login_at, created_at, updated_at
(issuer, subject) 必须唯一并只指向一个 platform_user。该表不保存密码、访问令牌或 refresh token。人员停用后主体状态立即变为 DISABLED,即使 IdP 会话撤销暂时失败,业务后端也必须拒绝。
7.2.4 user_identities
| 字段 | 说明 |
|---|---|
id |
平台身份记录 ID |
user_id |
关联 platform_users.id |
authentication_principal_id |
关联内部统一 IdP 主体;仅人员同步/文件来源时可空,允许登录的 identity 必须非空 |
source_id |
关联身份源 |
issuer、external_subject |
外部稳定主体;issuer 对纯 provisioning 来源可空,唯一性始终按 source_id + external_subject |
external_username |
仅展示和检索,不用于跨来源匹配 |
login_enabled |
是否允许登录 |
link_status |
PENDING、LINKED、CONFLICT、UNLINKED、DISABLED |
attributes |
受控保存的来源属性,不保存令牌和密码 |
last_login_at、last_seen_at |
使用情况 |
唯一约束必须包含 source_id + external_subject。禁止仅凭姓名或邮箱自动合并。
7.2.5 user_org_memberships 与组织外部引用
支持主组织、兼职组织和临时借调:
id, user_id, org_id, membership_type,
position_code, profession_code, primary_flag,
valid_from, valid_to, source_id, source_ref,
status, created_by, created_at, updated_at
组织本身继续使用 organizations,建议增加路径缓存或闭包表 organization_closure,用于高效计算 ORG_TREE。外部组织使用以下稳定映射:
organization_external_refs(
id, org_id, source_id, external_org_id,
external_parent_id, status, source_version, updated_at
)
每个活动人员必须恰好有一个有效主组织,数据库使用约束/触发器保证 primary_org_id 对应活动、primary_flag=true 的 membership。组织同步必须先于人员关系;未映射组织进入冲突区,不把人员自动放入根组织。
7.2.6 权限规范化表
permissions(
id, code, name, module, resource, action,
risk_level, status, description, created_at, updated_at
)
role_permissions(
role_id, permission_id, created_by, created_at
)
role_bindings(
id, user_id, role_id,
grant_source, grant_source_ref,
valid_from, valid_to, status,
granted_by, approved_by,
created_at, updated_at, version_no
)
role_binding_scopes(
id, binding_id, dimension, operator,
values, range_start, range_end,
created_at
)
role_binding_scopes 只保存结构化范围,例如组织、线路、数值化里程、对象、项目、环境或 assignee,不保存可执行表达式。角色自身描述功能集合,具体人员绑定决定范围和有效期。
首期一个角色绑定的范围作用于该角色包含的全部权限;同一人员若需要“全局只读、局部可写”,应分别绑定只读角色和业务操作角色。首期不引入任意表达式或单权限脚本化范围,避免权限不可解释。
7.2.7 外部组、派生授权与委派边界
external_group_memberships(
id, source_id, external_group_id, user_identity_id,
valid_from, valid_to, status, source_version
)
external_group_mappings(
id, source_id, external_group_id, external_group_name,
role_id, scope_template, scope_policy_id, status,
approved_by, approved_at, created_at, updated_at
)
role_delegation_policies(
id, delegator_role_id, grantable_role_id,
max_scope_dimensions, max_scope_values,
allow_all,
approval_required, status, created_at, updated_at
)
role_delegation_policy_scopes(
id, policy_id, dimension, operator,
values, range_start, range_end
)
只有白名单映射可以产生显式、可追踪的 role_bindings。派生绑定使用来源组成员关系的稳定唯一键,来源组被移除时只撤销该来源产生的绑定。外部 IdP 任意新增的组声明不得自动成为权限,也不得映射超级管理员、身份管理员、角色管理员、安全审计员或应急角色。
外部组映射的创建、修改、启用和每次 EXTERNAL_SYNC 派生绑定都必须复用人工授予的同一个 AuthorizationService,禁止同步任务直接写活动绑定绕过校验。映射模板 R 必须是审批人的委派范围 D 与 scope_policy_id 指向的策略上限 P 的子集。外部组派生绑定一律禁止 ALL,上游组名、Claims 或人员属性不能动态注入组织/线路/项目范围;运行时实例化只能收窄已批准模板。模板、来源组或策略变化都重新预览影响、审批并使旧派生绑定失效。
角色管理员授予角色前必须通过 role_delegation_policies 校验“可授予角色、最大数据范围和是否需审批”,不能仅依赖一个笼统管理权限。请求范围 R 必须同时是授予人当前有效委派范围 D 和策略上限 P 的子集,即 R ⊆ (D ∩ P);组织树、线路、里程、项目和环境逐维做包含判断,无法比较或缺失维度即拒绝。授予人的普通业务查看范围不自动等于委派范围,D 只来自包含 system:role:grant* 权限的有效绑定。
ALL 默认 allow_all=false,不能通过空 scope 或减少维度绕过;只有授予人本身拥有经批准的 ALL 委派范围、策略显式允许、调用者具备 system:role:grant-privileged、完成 RAIL-ACR-3 且另一人批准时才可产生。授权预览必须展示 R/D/P 和每个维度的包含证明,并保存到审计快照。
7.2.8 Provisioning、同步和导入
identity_provisioning_jobs(
id, user_id, operation, idempotency_key,
status, idp_request_ref, retry_count,
last_error, outbox_event_id, created_at, updated_at
)
service_client_lifecycle_jobs(
id, service_client_id, operation, idempotency_key,
requested_by, requested_auth_strength, reason,
impact_snapshot, expected_client_version,
approved_by, approved_at, approval_ref,
status, platform_ledger_sequence, propagation_status,
target_workload_snapshot, idp_request_ref, retry_count,
last_error, outbox_event_id, created_at, updated_at
)
identity_sync_jobs(
id, source_id, trigger_type, mode, idempotency_key,
status, total_count, create_count, update_count,
disable_count, conflict_count, error_count,
started_by, started_at, completed_at, checksum, summary
)
identity_sync_rows(
id, job_id, row_no, external_subject,
raw_payload, normalized_payload, diff,
decision, status, error_code, error_message,
target_user_id, processed_at
)
敏感字段必须脱敏或加密;原始密码、完整令牌、私钥不得进入 raw_payload。identity-user-session-worker 只领取 identity_provisioning_jobs,identity-client-lifecycle-worker 只领取 service_client_lifecycle_jobs;两类任务都使用唯一 idempotency_key、有界重试、租约/抢占恢复和明确终态。服务 client 重新启用与凭据轮换必须先有双人审批引用,worker 还要再次核对 service_clients 登记状态与允许操作,不能仅信任队列载荷。
7.2.9 安全审计
security_audit_events(
id, event_type, occurred_at, request_id, trace_id,
actor_type, actor_user_id, identity_id, issuer, subject,
effective_user_id, session_id,
permission_code, data_scope_snapshot,
resource_type, resource_id, action,
result, reason_code, reason,
client_ip, user_agent, auth_method, auth_strength,
before_snapshot, after_snapshot, metadata
)
该表只追加,不允许普通管理员更新或删除;按月分区并配置保留策略。日志中禁止记录密码、Cookie、Authorization Header、完整 access token/refresh token 和身份源密钥。
7.2.10 安全控制状态与撤销账本
Redis 用于低延迟拒绝,但不承担撤销事实和灾备 fencing 的唯一权威。PostgreSQL 增加:
security_control_state(
singleton_id, security_epoch_uuid,
user_token_valid_after, user_auth_valid_after,
service_token_valid_after,
authorization_required_epoch_uuid, authorization_recovery_state,
idp_continuity_state, idp_witness_generation, idp_witness_expires_at,
realm_security_digest, trusted_signing_keyset_digest,
recovery_state, ledger_sequence, updated_at, updated_by, version_no
)
idp_continuity_witnesses(
witness_generation, issuer_hash, idp_cluster_id,
db_system_identifier, db_timeline_id, durable_lsn,
timeline_ancestry_digest, realm_security_digest,
trusted_signing_keyset_digest, gateway_client_generation,
witness_source, evidence_ref, signature, status,
observed_at, accepted_at, expires_at, accepted_by
)
security_revocations(
event_id, ledger_sequence, subject_kind,
effect,
issuer_hash, subject_hash, hash_version, hash_key_id,
platform_user_id, principal_id, service_client_id,
reason_code, occurred_at, expires_at, created_by
)
service_clients(
id, issuer_hash, client_id, audience, allowed_scopes,
platform_status, idp_sync_status, disable_approval_mode,
token_generation, token_valid_after,
credential_version, rotation_mode, token_drain_until,
recovery_status, attested_security_epoch_uuid,
credential_ref, idp_ref,
created_at, updated_at, version_no
)
security_revocation_outbox(
event_id, ledger_sequence, payload, status,
retry_count, next_retry_at, published_at, last_error
)
security_control_acks(
workload_id, pod_uid, attester, ack_phase,
security_epoch_uuid, authorization_required_epoch_uuid,
idp_witness_generation, idp_witness_expires_at,
realm_security_digest, trusted_signing_keyset_digest,
ledger_sequence, observed_state,
acknowledged_at, expires_at
)
authorization_recovery_attestations(
id, authorization_required_epoch_uuid,
object_type, object_id, object_version, state_hash,
attestation_source, approval_ref, requested_by, approved_by,
status, attested_at, expires_at, created_at
)
security_control_state只有单行,以行锁/乐观版本更新;security_epoch_uuid是随机 128-bit 全局会话代次而不是可能在数据库回退后复用的自增数,user_token_valid_after/user_auth_valid_after分别是用户 JWT 签发时间和原始认证时间下限,service_token_valid_after是所有 Client Credentials JWT 的全局签发时间下限。authorization_required_epoch_uuid只在不能证明授权数据库连续性时换成新的随机值,authorization_recovery_state为NORMAL/QUARANTINED/PARTIAL;idp_continuity_state为VERIFIED/UNKNOWN/ROLLBACK_DETECTED/RECOVERING;recovery_state为NORMAL/FENCED/REBUILDING。realm_security_digest覆盖 issuer/redirect/scope/audience/mapper/auth-flow/MFA/required-action 等无密钥期望配置,trusted_signing_keyset_digest对允许签名公钥的算法、kid与指纹做 canonical hash。文中简称security_epoch_uuid为security_epoch。idp_continuity_witnesses位于 Keycloak 故障域之外的平台安全库,只追加保存 IdP 数据库 system identifier、timeline/祖先链、持久 LSN、受控配置摘要和签名公钥集合摘要;原始 Secret、密码材料和私钥不入表。每条在线 witness 使用强数据库时间签发短租约expires_at,基线 15 秒且不可由客户端时间延长;sentinel 必须持续续写新 generation,过期即视为UNKNOWN,不能让最后一次VERIFIED永久有效。它是在线判定记录,不是唯一灾备根:对应的签名 desired-state manifest、备份/恢复证明和 witness 链头还要写入独立 KMS 签名、对象锁/WORM 或建设单位等价不可变存储。IdP 与平台数据库同时不连续时,只能从该外部证明重新建立信任,不能让任一回退数据库自证连续。service_clients以内部 UUID 稳定标识每个服务 client,(issuer_hash, client_id)唯一;platform_status至少包含ACTIVE/DISABLED/ARCHIVED,idp_sync_status独立记录IN_SYNC/PENDING/FAILED,recovery_status至少包含ATTESTED/QUARANTINED/ATTESTING/FAILED。token_generation/token_valid_after在每次重新启用时单调推进,使禁用前 token 永久不能复活;attested_security_epoch_uuid只有完成当前 epoch 的凭据轮换与双人复核后才能写。credential_ref只保存 Secret Manager 引用而非密钥值。禁用/重新启用必须更新 IdP 和平台状态并审计,不能删除后用同名 client 绕过旧禁用事实。security_revocations.event_id幂等唯一;subject_kind明确SID/JTI/CLIENT/USER/PRINCIPAL/GLOBAL,其中SID/JTI的subject_hash/hash_version/hash_key_id与 Redis 查询键完全一致,CLIENT使用稳定service_client_id,避免把完整 claim 写入普通日志;effect默认DENY,仅已完成强认证和双人审批的CLIENT_REENABLE/RECOVERY_REATTEST可以写携带新token_generation/token_valid_after/attested_security_epoch_uuid的ALLOW。projector 按连续 ledger sequence 投影 client 当前状态,ACTIVE 也保留该记录而不是删除 key;rebuild 只能重放已存在的 attested epoch,不能改写为新 epoch。账本和 Outbox 同事务提交,运行时账号不能更新/删除已提交事实。- 每个创建安全事实的数据库事务都先锁定
security_control_state单行,以previous_ledger_sequence + 1分配唯一连续序号,在同一事务写撤销事实/Outbox并推进 control-state;事务回滚不消费序号。禁止应用实例、本地时间或独立 sequence 在事务外预分配,否则 gap 无法区分回滚与丢事件。 - Outbox projector 使用 PostgreSQL advisory lock/行锁保证每个
security_epoch只有一个活动顺序投影器,standby 只能在取得锁后接管。它严格按连续ledger_sequence处理,先确认全部 tombstone/control 数据键写入,再最后推进 Redis watermark;任何 gap、跨 slot 写失败或结果不确定都保持旧 watermark并告警,绝不让N+1越过N。Gateway/Resource Server 将可信 control-state 的期望(security_epoch, ledger_sequence)与 Redis watermark 比较,不相等即 fail closed。Kafka 事件复用event_id/security_epoch/ledger_sequence,消费者幂等。 - workload registry 中每个 normal Pod 核对强 control-state 与所需投影 watermark 后,以 mTLS workload/pod 身份写短期
security_control_acks;ack 同时绑定 security epoch、authorization epoch、IdP witness generation/expiry、realm security digest、可信签名 keyset digest 和 ledger sequence。ack 的有效期不得越过其观察到的 witness expiry。attester/ack_phase区分 quarantine gate 的REBUILDING/NORMAL与主进程的APPLICATION_READY/WORKER_READY,同一 Pod UID 的 gate 不能冒充尚未启动的业务/worker 进程。恢复控制器从 Kubernetes API 和 registry 获取当前期望 Pod UID/phase,不能用已终止 Pod、错误 workload 类别或旧 ack 冒充完成。 authorization_recovery_attestations不复制整张业务表,只证明某个稳定对象的精确object_version + state_hash在当前授权 epoch 可参与授权。对象至少覆盖人员状态、组织成员关系、角色定义、角色—权限边、角色绑定、数据范围策略、外部组映射和委派策略;任一节点/边缺证明、版本不符或 hash 不符即忽略/拒绝。平时经授权 API 的变更在同一强事务为新版本写当前 epoch 证明;灾后旧记录不会因数据库显示 ACTIVE 自动获得新证明。- 安全账本按最大 token/session 有效期、审计制度和灾备窗口保留。清理只能删除已过期且低于所有投影安全水位的记录,并留下不可变清理批次审计。
8. 人员来源与生命周期
8.1 本地自建
- 身份管理员在平台创建
platform_user,可先创建无登录能力的人员。 - 选择“启用本地登录”后,平台通过受控 IdP Admin Provider 创建身份。
- IdP 发送一次性激活链接或要求首次登录改密;平台不展示临时密码明文。
- 角色分配与账号创建分离,高权限角色需要独立审批。
- 停用人员时同步禁用 IdP 身份并撤销会话。
本地账号至少支持密码复杂度、历史密码、连续失败锁定、MFA、恢复码安全管理和管理员重置审计。具体策略由 IdP 配置,不在业务数据库重复实现。
平台数据库与 IdP 之间不使用分布式事务。创建本地身份时先在平台保存 PENDING 人员和幂等 provisioning 任务,再由可靠任务调用 IdP Admin Provider,成功后写入 user_identities 并激活;失败可重试和人工修复。停用时平台状态先立即生效,后端即使收到尚未过期的 JWT 也必须拒绝该人员,IdP 禁用和会话撤销通过可靠任务持续重试。
Provisioning 状态机:
REQUESTED -> PLATFORM_PENDING -> IDP_CREATED -> PLATFORM_LINKED -> ACTIVE
| | | |
+-------------> FAILED <--------+----------------+
|
+-> COMPENSATING -> COMPENSATED | MANUAL_REVIEW
- 每个操作使用平台生成的幂等键,并写事务 Outbox;后台任务至少一次执行,IdP Provider 必须把重复请求收敛为同一结果。
- IdP 创建成功、平台链接失败时,不直接删除可能已被使用的 IdP 账号;先禁用/隔离,再由对账任务补链或人工补偿。
- 平台人员与业务角色由平台数据库权威维护;密码、MFA、IdP 会话和内部 IdP 用户状态由 IdP 权威维护;外部人员属性按
field_authority决定。 - 定期对账平台主体、IdP 用户、上游身份链接和会话状态,差异进入
MANUAL_REVIEW,不静默创建第二个人员。
8.2 外部系统导入和同步
支持三类通道:
| 通道 | 适用情况 | 推荐方式 |
|---|---|---|
| 文件 | 首次上线、低频批量维护 | CSV/XLSX/JSON 上传,预检后提交 |
| REST/数据库 | 已有 HR、工单或人员系统 | 定时拉取、增量游标或对方推送 |
| SCIM | 对方支持标准人员生命周期 | 优先使用 SCIM,仍保留平台冲突与审计 |
统一流程:
CREATED -> VALIDATING -> READY -> COMMITTING -> COMPLETED
| | -> PARTIAL_SUCCESS
| -> CANCELLED
-> FAILED
COMPLETED / PARTIAL_SUCCESS -> ROLLED_BACK(仅回滚本批次可逆变更)
导入规则:
- 使用
(source_id, external_subject)幂等识别,不使用姓名作为键。 - 先解析到 staging 行,展示新增、更新、停用、无变化、冲突和错误数量。
- 映射配置必须版本化,导入任务保存映射版本和文件摘要。
- 同一文件或同一增量游标重复执行不得创建重复人员、组织关系或角色绑定。
- 姓名、手机号、组织、岗位、状态、角色分别配置权威来源。
- 外部来源删除主体时,默认只停用该来源的
user_identity、结束该来源的组织成员关系并撤销该来源生成的角色绑定;不得删除其他来源或平台手工维护的关系。 - 只有
authoritative_for_status=true的权威雇佣来源,或具备权限的身份管理员,才能把整个平台人员转为DISABLED/LEFT。 - 权威结果先按活动
identity_source_authorities的字段规则和优先级计算;没有唯一最高权威、同优先级结果冲突或规则版本缺失时不得按到达顺序覆盖,按前述SUSPENDED/CONFLICT或冻结组织扩权策略处理。 - 来源恢复同一主体时复用原
platform_user_id。 - 模糊匹配、重复证件号、组织不存在、角色越权等进入冲突队列,不静默覆盖。
- 来源提供的角色只能经过白名单映射,并标记
grant_source=EXTERNAL_SYNC。 - 平台手工授权与来源授权分开保存,来源同步不能删除平台手工授权。
组织同步规则:
- 先同步组织和
organization_external_refs,再同步人员 membership; - 外部组织移动需要校验循环、父级映射和影响范围,完成后递增组织树策略版本;
- 外部组织合并或停用不得物理删除业务历史,未决人员进入冲突队列;
- 每个来源只能更新自己权威维护的组织引用和 membership;
- 主组织变化按版本化权威规则计算;只有唯一最高优先级结果可提交,冲突时保留最后已批准关系并冻结范围扩大,必须始终保证恰好一个活动主组织;
- 兼职组织只进入明确授权绑定的范围,不自动扩大该人员全部业务权限。
8.3 连接器与导入安全
身份源测试、REST/数据库同步和文件导入具有 SSRF、凭据滥用和恶意文件风险,必须执行:
- 只允许配置批准的协议、域名、端口和证书;解析前后检查 DNS,阻止 DNS rebinding、云元数据、loopback、link-local 和未批准内网地址;
- 连接器走固定出站代理或 NetworkPolicy,HTTP 重定向后的目标重新校验;
- 数据库连接使用固定驱动和参数模板、只读账号、查询白名单、行数/时间限制,禁止管理员提交任意 JDBC 参数或任意 SQL;
- IdP Admin 权限按 4.1.1 拆分:日常 user/session principal 不授予 realm、client、密钥和全局管理权;client-lifecycle principal 只能操作已登记 client;continuity sentinel 只读;realm/key recovery principal 仅挂载给审批恢复窗口内的
identity-idp-recovery-job; - 文件限制大小、行数、嵌套压缩率和类型,防压缩炸弹、恶意宏、公式注入和 CSV 导出注入;
- 连接测试结果只返回分层状态和脱敏错误,不返回凭据、完整响应体、证书私钥或内部网络细节;
- 所有测试和同步任务限流、可取消并记录发起人、目标、映射版本和结果。
8.4 单点登录与受控 JIT
推荐模式按安全性从高到低:
- 预配模式:先从人员系统同步,再允许 SSO 登录。生产默认使用。
- JIT 待审核:首次 SSO 仅创建
PENDING人员和身份,无任何业务角色,管理员审核后启用。 - 受控 JIT:仅特定身份源、邮箱域和外部组可获得预设最低角色;必须有映射白名单和审计。
账号绑定:
- 外部主体只在所属身份源内唯一。
- 已登录用户绑定第二身份时要求重新认证和二次确认。
- 管理员人工合并人员需要展示双方全部身份、业务引用和角色差异,并经过审批。
- 默认不按邮箱自动链接;若确需邮箱链接,只允许已验证邮箱、同一受信域和人工确认。
- 解绑最后一个登录身份前提示该人员将失去登录能力。
8.5 人员、身份和来源停用
停用必须先区分四种对象:来源身份、组织成员关系、来源角色绑定和整个平台人员。某个低权威来源消失不能自动停用仍由 HR 或其他有效来源维护的人员。
整个人员停用动作应通过业务事务 + Outbox 可靠事件链完成:
- 更新人员状态;
- 禁用所有登录身份;
- 撤销 Gateway/IdP 会话;
- 使授权缓存失效;
- 处理未完成工单、值班和任务责任转交;
- 写入安全审计;
- 保留历史显示名称和业务引用。
人员有效状态按“平台安全停用覆盖一切 > 唯一最高优先级权威雇佣规则 > 经批准的人工裁决 > 非权威来源”计算。同优先级权威状态冲突时选择安全结果 SUSPENDED/CONFLICT,而不是继续使用 ACTIVE。身份源只能撤销自己创建的 identity/membership/role binding;整个人员停用后,即使仍存在其他外部身份,也不能登录或访问业务。仅停用一个身份时,人员可以继续使用其他活动身份登录。
9. 角色、权限与数据范围
9.1 权限代码规范
统一使用稳定的小写代码:
<domain>:<resource>:<action>
例如:
inspection:plan:read
inspection:plan:create
inspection:plan:update
inspection:task:dispatch
route:version:edit
route:version:publish
uav:mission:command
alarm:decision:review
workorder:evidence:upload
model:version:release
system:user:disable
system:identity:reset
system:role:grant
security:audit:read
read、create、update、delete、execute、approve、publish、export、download 分开授权。高风险操作不应包含在泛化的 manage 中。
9.2 数据范围
数据范围不是用户所有组织关系的简单集合,而是每一条有效 role_binding 上的显式约束。首期支持以下结构化维度:
| 维度/范围 | 含义 | 典型字段与约束 |
|---|---|---|
ALL |
全平台 | 仅高风险系统角色;不能与其他范围混合以掩盖配置错误 |
ORG |
明确列出的组织 | owner_org_id IN (:org_ids) |
ORG_TREE |
明确根组织及其当前下级 | owner_org_id + 版本化组织闭包 |
LINE |
明确线路 | line_id IN (:line_ids) |
MILEAGE |
线路内标准化里程区间 | 数值字段 mileage_m,闭区间 [start_m, end_m];不对字符串里程做安全比较 |
OBJECT |
明确巡检对象 | object_id IN (:object_ids) |
PROJECT |
厂商项目或业务工作区 | project_id / workspace_id |
MODEL_GROUP / ENVIRONMENT |
模型组、数据集或发布环境 | 模型与算法领域的独立边界 |
ASSIGNEE_SELF |
当前人员是有效责任人 | assignee_user_id = actor.platform_user_id |
CREATOR_SELF |
当前人员是可信创建人 | 服务端固化的 created_by_user_id = actor.platform_user_id |
NONE |
无可用范围 | 缺失、失效或冲突时默认拒绝 |
合并算法必须固定并可测试:
- 同一绑定内,不同维度取交集(AND)。例如某绑定同时给出
ORG_TREE=工务段 A、LINE=京广线、MILEAGE=[123000,130000],只有三个条件同时满足才可访问。 - 同一权限来自多条有效绑定时,各绑定结果取并集(OR)。例如绑定 A 允许上述线路区间,绑定 B 允许
ASSIGNEE_SELF,有效谓词为A OR B。 - 同一维度的一组值默认取并集,例如
ORG IN (A,B);区间必须先标准化、校验起止并按线路隔离。 ALL只能由明确批准的活动绑定产生;空数组、未知维度、过期绑定、无法解析值都按NONE处理。- 兼职、借调和次组织成员关系不会自动扩大范围。只有角色绑定显式包含该组织或相应组织树时才纳入授权。
- 数据范围按权限分别计算,不能把“全局只读”角色的范围借给“局部写入”角色。
CREATOR_SELF 只有在 created_by_user_id 已由服务端 CurrentActor 固化且历史数据完成可信回填后才可作为安全边界。现有由请求传入的 created_by/createdBy 仅能展示或审计,未回填对象不得据此放行。
责任规则用于自动派发告警和工单,不等同于访问授权。人员的 actor.org_ids 只描述有效成员关系,不能替代 role_binding_scopes。
9.3 授权执行层次
flowchart TB
R["请求"] --> G["Gateway:是否登录、入口限流、公开路径"]
G --> M["后端方法权限:是否允许执行该动作"]
M --> D["数据范围:是否能访问目标组织/线路/对象"]
D --> S["职责分离/状态机/二次认证"]
S --> A["业务执行 + 安全审计"]
- Gateway:所有业务路由默认要求认证,只放行静态资源、登录回调和明确的供应商回调。
- 授权图再证明:人员、成员关系、角色、权限边、绑定、范围和委派策略的当前版本都必须有
authorization_required_epoch_uuid对应的有效 attestation;任一缺失即不产生权限,超级管理员也不能绕过。 - 方法权限:使用统一授权组件和
@PreAuthorize检查权限代码。 - 数据权限:在 JDBC 查询条件中注入范围,列表与详情使用同一策略。
- 对象状态:权限通过后仍需校验业务状态机和乐观锁。
- 职责分离:创建人不能审批自己创建的任务、航线版本或模型发布。
- 高风险动作:飞行控制、密钥配置、角色授予、模型发布可要求 MFA/二次认证。
概念示例:
@PreAuthorize("@authz.has(authentication, 'inspection:task:dispatch')")
public DispatchResult dispatchTask(String taskId) {
dataScope.requireTask(authentication, taskId);
separationOfDuties.requireAllowed(authentication, "TASK_DISPATCH", taskId);
// 执行业务逻辑
}
该代码仅表达设计意图,具体实现应复用当前 JDBC 服务结构,不强制引入 JPA。
9.4 业务对象的数据归属传播
数据范围不能只依赖少数已有 owner_org_id。实施迁移时应为每类对象确定唯一、可解释的归属链:
| 业务对象 | 推荐授权归属 |
|---|---|
| 巡检对象、计划 | 自身 owner_org_id |
| 任务 | 固化任务 owner_org_id,来源于计划或主巡检对象 |
| 航线/航线版本 | 来源巡检对象的组织和线路,版本保存授权快照 |
| UAV 任务与遥测 | 继承巡检任务的组织、线路和设备项目范围 |
| 资源、预处理、分析任务/结果 | 继承巡检任务;无任务资源必须显式指定组织 |
| 告警、工单 | 使用自身 owner_org_id,并保留任务来源链 |
| 证据、附件、归档、GIS 导出 | 继承源任务/告警/工单;下载时沿源对象复核 |
| 样本、数据集、模型 | 使用模型组、数据集、组织和环境的独立范围 |
| 人员、角色、身份源 | 使用管理委派范围,不套用普通业务 owner_org_id |
高频查询对象建议直接固化 owner_org_id、line_id 等授权列并建立索引,避免每次跨多级业务链 JOIN;创建时保存来源和范围快照。归属变更必须走受控迁移并审计,不能由普通编辑接口任意改写。
9.5 列表、详情和文件权限一致性
- 列表查询必须在数据库条件中限制范围,禁止先查全量再由 Java 或前端过滤。
- 详情、更新和删除使用与列表相同的数据谓词;范围外对象默认按 404 处理,避免枚举。
- 对用户可见但动作无权限的对象返回 403,并给出稳定错误码。
- 附件、原图、视频、点云、模型制品、GIS 导出和归档下载均为独立权限。
- 预签名 URL 必须短期有效、绑定文件和用途;生成前重新校验源业务对象权限。
- 统计和总览只能聚合当前数据范围,不能泄露全平台数量。
- 异步导出记录发起人和授权快照;下载时再次检查当前权限。
9.6 授权缓存
- Redis Key 建议为
rail:authz:user:{user_id}:a{authorization_required_epoch}:u{authz_version}:p{policy_version}:o{org_tree_version}。缓存内容包含权限代码、逐权限结构化范围、来源绑定和 attestation 摘要,不缓存可执行脚本;授权 epoch 改变后旧缓存天然不可寻址。 - 用户身份、成员关系或绑定变化时递增
platform_users.authz_version;角色权限/委派策略变化时递增全局policy_version;组织父子关系变化时递增org_tree_version。三个版本均由数据库持久化,Redis 只是加速层。 - 维护
role -> users、org-root -> users/bindings的反向索引,用于定向失效和影响预览;发生大范围修改或索引不完整时直接提升全局版本,不能漏删旧缓存。 - 缓存 TTL 取“默认 TTL”和相关绑定/成员关系最近一次
valid_from或valid_to状态切换点的较小值;未来生效的授权不能长期看不到,过期授权也不能因旧缓存继续有效。 - 停用、应急账号禁用、特权角色撤销和飞行权限撤销同时写入即时拒绝索引、撤销会话并更新版本;不能只等待事件总线或 TTL。
- 失效事件至少一次投递并可重放。消费延迟、失败和版本漂移需要指标与告警;高风险请求以数据库/即时拒绝索引为准。
- 缓存不可用时读取数据库;数据库也不可用时拒绝请求,不得因缓存故障自动放行。
9.7 角色与授权生命周期
角色状态:
DRAFT -> ACTIVE -> DISABLED -> ARCHIVED
角色绑定状态:
DRAFT -> PENDING_APPROVAL -> ACTIVE -> REVOKED
\------> EXPIRED
PENDING_APPROVAL -> REJECTED
- 角色
code创建后不可修改;名称和说明可以版本化更新。 - 内置系统角色可以复制但不能删除或降低其安全约束。
- 修改活动角色的权限前展示受影响人员、扩权/缩权差异和职责冲突。
- 高风险权限新增、
ALL范围和委派边界扩大必须审批。 - 禁用角色立即使其全部绑定失效并递增相关人员
authz_version。 - 已被历史业务和审计引用的角色、权限和绑定只归档,不物理删除。
- 权限代码由代码/迁移注册,页面不得创建后端没有执行点的任意“虚拟权限”。
10. 初始权限目录
10.1 页面与关键动作
| 页面/领域 | 查看权限 | 关键动作权限 |
|---|---|---|
| 工作总览 | overview:dashboard:read |
overview:dashboard:export |
| GIS 态势 | gis:map:read |
gis:map:export、gis:layer:update、gis:rule:update |
| 全链路运行 | inspection:run:read |
inspection:run:start、inspection:run:retry、inspection:run:cancel |
| 巡检计划 | inspection:plan:read |
inspection:plan:create、inspection:plan:update、inspection:plan:delete、inspection:object:import |
| 业务编排 | workflow:inbox:read |
workflow:task:approve、workflow:route:approve、workflow:template:publish |
| 巡检任务 | inspection:task:read |
inspection:task:create、inspection:task:update、inspection:task:dispatch、inspection:task:batch |
| 航线中心 | route:version:read |
route:version:edit、route:version:approve、route:version:publish |
| 无人机运行 | uav:operation:read |
uav:mission:dispatch、uav:mission:command、uav:connection:update |
| 数据资源 | resource:asset:read |
resource:asset:upload、resource:preprocess:execute、resource:asset:download |
| 智能分析 | analysis:job:read |
analysis:job:execute、analysis:rule:create、analysis:rule:update |
| 视频 AI 演示 | analysis:video-demo:read |
analysis:video-demo:execute、analysis:video-demo:export |
| 告警中心 | alarm:item:read |
alarm:decision:review、alarm:decision:suppress、alarm:workorder:create |
| 工单中心 | workorder:item:read |
workorder:item:assign、workorder:item:process、workorder:item:review |
| 样本与模型 | model:asset:read |
sample:asset:create、sample:asset:update、model:test:execute、model:version:release |
| 报告归档 | archive:package:read |
archive:package:create、archive:package:download |
| 系统运维 | system:operation:read |
system:integration:update、system:config:update |
| 人员管理 | system:user:read |
system:user:create、system:user:update、system:user:disable、system:identity:create、system:session:revoke |
| 角色权限 | system:role:read |
system:role:update、system:role:grant、system:role:grant-privileged、system:permission:read |
| 身份来源 | system:identity-source:read |
system:identity-source:configure、system:identity-source:secret-rotate、system:identity-source:test、system:identity-sync:execute |
| 服务身份 | system:service-client:read |
system:service-client:disable、system:service-client:enable、system:service-client:credential-rotate、system:service-client:recovery-reattest、system:service-client:approve |
| 安全恢复 | security:recovery:read |
security:recovery:prepare、security:recovery:authorization-attest、security:recovery:approve;仅 PARTIAL/受控恢复入口可用 |
| 安全审计 | security:audit:read |
security:audit:export |
实施时应由代码扫描和人工复核共同生成完整端点矩阵,不以上表代替逐接口登记。
系统管理权限必须继续拆分,禁止重新引入 system:*:manage 作为万能写权限:
| 对象 | 最小权限代码 | 补充控制 |
|---|---|---|
| 人员 | system:user:read、system:user:create、system:user:update、system:user:disable |
停用要求 RAIL-ACR-2;不能借更新接口修改角色 |
| 本地/外部身份 | system:identity:create、system:identity:reset、system:identity:bind、system:identity:unbind、system:identity:disable |
重置只返回一次性流程,不返回密码;绑定/解绑审计 |
| 会话 | system:session:read、system:session:revoke、system:session:revoke-all |
撤销他人全部会话要求 RAIL-ACR-2 |
| 角色定义 | system:role:read、system:role:create、system:role:update、system:role:disable |
仅定义角色,不隐含授予能力 |
| 角色授予 | system:role:grant、system:role:grant-privileged、system:role:approve |
委派边界校验;特权授予要求 RAIL-ACR-3 和双人审批 |
| 权限目录 | system:permission:read、system:permission:register、system:permission:disable |
register/disable 仅部署迁移或受控平台管理员 |
| 身份源 | system:identity-source:read、system:identity-source:configure、system:identity-source:secret-rotate、system:identity-source:test |
密钥轮换要求 RAIL-ACR-3,Secret 永不回显 |
| 人员同步 | system:identity-sync:preview、system:identity-sync:execute、system:identity-sync:commit、system:identity-sync:rollback |
预览不隐含提交;批量停用需审批 |
| 服务 client | system:service-client:read、system:service-client:disable、system:service-client:enable、system:service-client:credential-rotate、system:service-client:recovery-reattest、system:service-client:approve |
禁用先预览影响并按 client 策略决定是否双人审批;启用/轮换/灾后再证明固定 RAIL-ACR-3、双人审批,发起人不能审批;普通管理员不能伪造 recovery epoch |
| 安全恢复 | security:recovery:read、security:recovery:prepare、security:recovery:authorization-attest、security:recovery:approve |
仅查看/准备差异和提交外部审批引用;真正 fence、epoch、IdP/key/client 恢复只接受 JIT mTLS Job,平台超级管理员也不能绕过 |
| 敏感个人信息 | system:pii:read、system:pii:export |
与普通人员查看分离,导出加水印并审计 |
| 安全审计 | security:audit:read、security:audit:export |
审计员不能修改被审计配置 |
10.2 建议系统角色
| 角色 | 主要职责 | 默认数据范围 |
|---|---|---|
| 平台超级管理员 | 平台初始化和应急恢复 | ALL,仅应急账号使用 |
| 身份管理员 | 人员、组织、身份源、账号停用 | 组织管理范围;默认无业务审批权 |
| 服务身份管理员 | 服务 client 登记信息、禁用/启用或轮换申请 | 系统范围;默认无自身申请审批权,也不能管理人员角色 |
| 服务身份审批员 | 审核服务 client 启用和凭据轮换 | 系统范围;只读影响信息,不持有 IdP Admin 凭据 |
| 角色管理员 | 角色模板和授权绑定 | 仅可授予自身委派边界内角色 |
| 安全审计员 | 登录、授权和高风险事件查询 | ALL 只读,不能修改用户和业务 |
| 系统运维员 | 健康、集成和运行配置 | 系统范围,不默认读取业务原始媒体 |
| 任务计划员 | 计划、任务、对象和批量操作 | ORG_TREE / 指定线路 |
| 任务审批人 | 任务审批 | ORG_TREE,不能审批本人发起对象 |
| 航线管理员 | 航线编辑、校验和提交 | 指定组织/线路 |
| 航线发布人 | 航线审批和发布 | 指定组织/线路,不能发布本人编辑版本 |
| 飞行管理员 | 设备、下发和普通飞行命令 | 指定项目/设备/线路 |
| 数据管理员 | 上传、预处理和质量处置 | 指定组织/任务 |
| 算法工程师 | 分析、样本和测试 | 指定模型组/数据集 |
| 模型审批人 | 模型发布和回滚 | 指定环境,不能批准本人申请 |
| 告警研判员 | 告警确认、抑制和转单 | 指定组织/线路 |
| 工单调度员 | 派单、改派和 SLA 管理 | ORG_TREE |
| 现场处置人员 | 接单、处置和证据上传 | ASSIGNEE_SELF |
| 专业复核人员 | 工单复核、退回和复检 | 指定组织/专业 |
| 管理只读用户 | 总览、GIS、统计和归档查询 | 被授权组织/线路只读 |
超级管理员、身份管理员、角色管理员和审计员必须相互分离。普通业务角色不得管理自己的授权。
11. 职责分离和高风险控制
至少实施以下约束:
| 高风险业务 | 约束 |
|---|---|
| 任务审批 | 创建人不能审批自己的任务 |
| 航线发布 | 编辑或提交人不能批准并发布同一版本 |
| 模型上线 | 制品上传/发布申请人与上线批准人不同 |
| 空间豁免 | 创建人与批准人不同,保存有效期和原因 |
| 角色授予 | 被授权人与批准人不同;高权限角色双人审批 |
| 身份合并 | 需要身份管理员发起、另一管理员确认 |
| 人员批量停用 | 展示影响范围并二次确认,记录转交结果 |
| 飞行高风险命令 | 权限、任务状态、设备能力、MFA 和明确确认同时满足 |
| 密钥和身份源配置 | 只能更新密钥引用,不返回原文;变更需审计和回滚窗口 |
| 服务 client 生命周期 | 禁用先做调用方/依赖影响预览;启用和凭据轮换要求 RAIL-ACR-3,申请人与批准人不同,worker 不能自行批准 |
职责分离是业务约束,不应通过给用户添加更多角色绕过。
12. API 设计
12.1 当前用户
GET /api/v1/me
GET /api/v1/me/permissions
GET /api/v1/me/menus
GET /api/v1/me/sessions
POST /api/v1/me/sessions/{sessionId}/revoke
GET /api/v1/me 返回:人员 ID、显示名、主组织、有效组织、角色摘要、权限代码、数据范围摘要、身份来源、认证强度和会话到期时间。不得返回令牌。
12.2 人员与组织
GET /api/v1/admin/users
POST /api/v1/admin/users
GET /api/v1/admin/users/{userId}
PATCH /api/v1/admin/users/{userId}
POST /api/v1/admin/users/{userId}/enable
POST /api/v1/admin/users/{userId}/disable
POST /api/v1/admin/users/{userId}/local-identity
POST /api/v1/admin/identities/{identityId}/reset
POST /api/v1/admin/identities/{identityId}/disable
POST /api/v1/admin/users/{userId}/identity-links/preview
POST /api/v1/admin/users/{userId}/identity-links
DELETE /api/v1/admin/users/{userId}/identity-links/{identityId}
GET /api/v1/admin/users/{userId}/sessions
POST /api/v1/admin/users/{userId}/sessions/revoke
GET /api/v1/admin/users/{userId}/effective-access
GET /api/v1/admin/organizations/tree
POST /api/v1/admin/organizations
PATCH /api/v1/admin/organizations/{orgId}
GET /api/v1/admin/users/{userId}/memberships
PUT /api/v1/admin/users/{userId}/memberships
effective-access 必须能解释“权限来自哪个角色、角色来自哪个授权、数据范围是什么、何时失效”,用于授权前预览和越权排查。本地身份重置、外部身份绑定/解绑、会话查看/撤销使用彼此独立权限;人员通用 PATCH 不得顺带完成这些动作。
12.3 角色与权限
GET /api/v1/admin/permissions
GET /api/v1/admin/roles
POST /api/v1/admin/roles
PATCH /api/v1/admin/roles/{roleId}
PUT /api/v1/admin/roles/{roleId}/permissions
GET /api/v1/admin/role-bindings
POST /api/v1/admin/role-bindings/preview
POST /api/v1/admin/role-bindings
POST /api/v1/admin/role-bindings/{bindingId}/approve
POST /api/v1/admin/role-bindings/{bindingId}/reject
DELETE /api/v1/admin/role-bindings/{bindingId}
角色授权前必须预览新增权限、扩大数据范围、职责冲突和影响对象。删除绑定采用撤销状态,不物理删除审计事实。
特权绑定的创建、审批和普通授予分别校验 system:role:grant-privileged、system:role:approve 和 system:role:grant;同一人不能同时发起并批准自己的特权授权。
12.4 身份来源和同步
GET /api/v1/admin/identity-sources
POST /api/v1/admin/identity-sources
PATCH /api/v1/admin/identity-sources/{sourceId}
POST /api/v1/admin/identity-sources/{sourceId}/secret-rotation
POST /api/v1/admin/identity-sources/{sourceId}/test
POST /api/v1/admin/identity-sources/{sourceId}/sync/preview
POST /api/v1/admin/identity-sources/{sourceId}/sync/execute
GET /api/v1/admin/identity-sync-jobs
GET /api/v1/admin/identity-sync-jobs/{jobId}
GET /api/v1/admin/identity-sync-jobs/{jobId}/rows
POST /api/v1/admin/identity-sync-jobs/{jobId}/commit
POST /api/v1/admin/identity-sync-jobs/{jobId}/rollback
POST /api/v1/admin/identity-conflicts/{conflictId}/resolve
导入建议复用当前对象导入已经形成的“预检—提交—回滚”交互模式,但身份冲突不得复用对象主键匹配逻辑。
通用 PATCH 不接收明文 Secret;密钥轮换端点只接收新的密钥引用并要求 system:identity-source:secret-rotate、RAIL-ACR-3 和审计。preview/execute/commit/rollback 分别检查其细粒度同步权限。
12.5 服务 client 生命周期
GET /api/v1/admin/service-clients
GET /api/v1/admin/service-clients/{clientId}
POST /api/v1/admin/service-clients/{clientId}/lifecycle-preview
POST /api/v1/admin/service-clients/{clientId}/disable
POST /api/v1/admin/service-clients/{clientId}/enable
POST /api/v1/admin/service-clients/{clientId}/credential-rotations
POST /api/v1/admin/service-clients/{clientId}/recovery-reattest
GET /api/v1/admin/service-client-lifecycle-jobs
GET /api/v1/admin/service-client-lifecycle-jobs/{jobId}
POST /api/v1/admin/service-client-lifecycle-jobs/{jobId}/approve
POST /api/v1/admin/service-client-lifecycle-jobs/{jobId}/reject
lifecycle-preview 接收目标操作但不改变状态,返回短期 preview_id、依赖/在途任务/调用方影响、当前版本和审批要求。后续写端点要求 Idempotency-Key、expected_client_version、原因和未过期 preview_id,服务端重新计算并核对预览摘要;不接受修改 issuer/client_id/audience/mapper/idp_ref 的通用 PATCH,也不接收或返回原始 client secret。列表、详情、预览、禁用、启用、轮换和审批分别检查 system:service-client:* 的精确权限。
生命周期状态机固定如下:
disable先检查调用依赖、在途任务和 client 的disable_approval_mode。无需审批时由请求事务直接完成;需要审批时先进入PENDING_APPROVAL,由另一名审批员确认。- 禁用最终事务锁定
service_clients并重验版本,原子写platform_status=DISABLED/idp_sync_status=PENDING、CLIENT DENY安全账本/Outbox 和DISABLEworker job。事务提交只表示DURABLE_ACCEPTED,API 返回 202、job_id/ledger_sequence/propagation_status,不能宣称远端已立即生效;IdP 暂时失败也不能回滚平台禁用。 enable与credential-rotations一律要求RAIL-ACR-3和另一人审批,状态依次为PENDING_APPROVAL -> APPROVED/ENQUEUED -> RUNNING -> SUCCEEDED,失败区分FAILED_RETRYABLE/FAILED_FINAL;拒绝、版本变化或超时进入REJECTED/CANCELLED,不能执行陈旧批准。轮换请求必须显式选择PLANNED或EMERGENCY_REVOKE,影响预览分别显示旧 JWT 排空窗口或服务中断窗口,不能用默认值掩盖语义。- 普通重新启用时 worker 先确认登记 allowlist 和审批,且
attested_security_epoch_uuid必须已等于当前 control epoch;若不相等,只能走recovery-reattest。使用数据库时间生成未来整秒 cutoffT(至少晚于当前秒和允许的最大时钟漂移),把 IdP client not-before 推进到T并等待受信时钟到达/超过T。确认 IdP 已只允许新签发后,在数据库事务将token_generation + 1、token_valid_after=T、平台状态ACTIVE与携带同值/当前 attested epoch 的CLIENT ALLOWledger 事件原子提交;projector 更新持久 client 状态并推进 watermark 后才允许 token。Resource Server 对 cutoff 不应用负向 clock-skew 容差,禁用前iat < T的 token 永久拒绝。任何 IdP 调用、等待或数据库提交结果不确定都保持禁用。 PLANNED凭据轮换只用于无泄露迹象的例行换密:把新版本写入批准的 Secret Manager 路径,按 Provider 能力使用有界双凭据窗口或审批维护窗,验证调用方已切换后撤销旧 Secret。它不撤销此前已签 JWT;数据库记录credential_version/token_drain_until=max(old JWT exp),页面在此之前固定显示SECRET_ROTATED_TOKEN_DRAINING,不得宣称旧 token 已失效。到达排空时间且旧凭据换 token 的负向探测通过后才结束该提示。EMERGENCY_REVOKE用于疑似泄露或要求立即淘汰旧 JWT:先提交CLIENT DENY并等到GLOBALLY_ENFORCED,再生成/部署新凭据、撤销全部旧凭据;使用数据库受信时间生成未来安全整秒T,推进 IdP client not-before,等待到达T后在强事务递增token_generation、写token_valid_after=T和当前 epoch 的CLIENT ALLOW,最后等待投影与目标 ack。此模式在完成前保持 client 禁用;IdP、Secret Manager、数据库或传播结果不确定时按request_id + credential_version对账并保持FAILED_RETRYABLE/DISABLED,禁止报成功或回退放行。recovery-reattest仅用于 client 的 attested epoch 落后于当前 security epoch;它强制撤销全部旧凭据、生成并部署新凭据、推进 IdP/per-client not-before、验证旧凭据不能取 token 和新调用方身份正确,并由另一人批准。条件存储过程才可写recovery_status=ATTESTED/attested_security_epoch_uuid=current与新的 generation/cutoff/ALLOW 事件;不能把普通 enable 或数据库回填当再证明。
backend API 只创建/批准事务和 job,不持有 IdP Admin 凭据;identity-client-lifecycle-worker 只消费已批准 job,不能扩大 client allowlist、伪造审批或任意更新平台状态。重新启用和紧急轮换只能调用同时核对 job、审批、版本、轮换模式和 IdP 结果的条件存储过程完成第 4/6 步。
禁用传播状态依次为 DURABLE_ACCEPTED -> PROJECTED -> GLOBALLY_ENFORCED:projector 把对应 ledger_sequence 写入 Security Revocation Redis 后进入 PROJECTED;所有当前可服务的目标 Resource Server 以 RUNTIME attester ack 到该序号,或者已被编排器摘流/终止,才进入 GLOBALLY_ENFORCED。正常态 control-state 最长缓存 5 秒,因此允许窗口是有界传播而不是零时延;基线验收 SLA 为 60 秒,超时进入 PROPAGATION_OVERDUE、告警并在页面显示未完成目标,不得返回“全局禁用成功”。确需零窗口的事故处置必须使用 17.4.1 流量围栏,不能靠文案把异步投影称为立即。
12.6 安全审计
GET /api/v1/admin/security-audit-events
GET /api/v1/admin/security-audit-events/{eventId}
POST /api/v1/admin/security-audit-exports
GET /api/v1/admin/security-audit-exports/{exportId}/content
12.6.1 PARTIAL 安全恢复页面 API
GET /api/v1/admin/security-recovery/status
GET /api/v1/admin/security-recovery/authorization-diff
GET /api/v1/admin/security-recovery/batches
GET /api/v1/admin/security-recovery/batches/{batchId}
POST /api/v1/admin/security-recovery/batches/{batchId}/prepare
POST /api/v1/admin/security-recovery/batches/{batchId}/approval-references
这些端点只在 recovery_state=NORMAL + authorization_recovery_state=PARTIAL 时开放,用于查看强 control-state 的脱敏摘要、下载签名差异清单和登记外部双人审批引用;分别要求 security:recovery:read/prepare/approve,且不能直接写 epoch、attestation、IdP、Secret Manager 或 Redis。真正再证明仍由 17.4.3 的短期 mTLS Job 调用内部端点完成。FENCED/REBUILDING 时公众 Gateway 不提供本 API,防止用回退数据库中的旧管理员权限启动恢复。
12.7 内部安全控制 API
POST /internal/security/v1/revocations
GET /internal/security/v1/control-state
POST /internal/security/v1/control-state/ack
POST /internal/security/v1/recovery/fence
POST /internal/security/v1/idp-continuity-witnesses
POST /internal/security/v1/recovery/idp-epoch-sync-complete
POST /internal/security/v1/recovery/authorization-attestations
POST /internal/security/v1/recovery/service-clients/{clientId}/reattest-complete
POST /internal/security/v1/recovery/rebuild-complete
这些端点只监听独立 security-control-plane 的内部端口,不进入公众 Gateway 路由,并使用与普通 Resource Server 完全分离的精确 mTLS Security Chain。证书 URI SAN/工作负载注册表映射到 security.revocation.write、security.control.read、security.control.ack、security.idp-witness.write、security.recovery.execute、security.recovery.idp-sync、security.recovery.authz-attest 或 security.recovery.client-reattest;不接受 Cookie、公众 Bearer 或身份 Header,也不依赖 Security Revocation Redis 才能认证,否则故障时会自锁。撤销写只接受 Gateway/身份管理 writer 的 mTLS 身份与幂等 event_id;ack 只能写当前证书对应的 workload/pod UID。IdP witness 端点只接受 continuity sentinel 的证书、当前 DB lease、签名证据和严格递增 generation,expiry 由 control plane 的数据库时间计算,调用方不能指定未来任意时间。idp-epoch-sync-complete 只接受 identity recovery Job,且必须同时提交 IdP 读回 epoch、mapper/realm digest、witness generation 和审批/run ID;control plane 自行复核当前 security epoch/continuity,不接受调用方直接设置 control 值。attestation/reattest 端点只接受对应短期 Job 身份、外部双人审批引用、当前对象版本/hash 与 IdP/Secret Manager 证明,不能由通用 recovery scope 伪造。
GET /internal/security/v1/control-state 是 FENCED/REBUILDING 期间唯一允许的只读状态端点,直接以只读事务查询强持久化 PostgreSQL,不读取 Redis;配套 ack 是唯一允许的实例确认写。仅 workload registry 中获批的 normal gate/主进程 attester、continuity gate/label controller 和恢复身份可按各自 scope 调用。恢复写端点只对精确 recovery controller/Job 开放;恢复 NetworkPolicy 只保留该内部端口,具体 HTTP 方法/路径继续由 mTLS Security Chain 拒绝。rebuild-complete 必须证明 PostgreSQL ledger sequence、三个 cutoff、security/authorization epoch、未过期 IdP witness generation/realm/keyset digest、撤销投影、必要 service-client re-attestation 和授权恢复状态符合选定的 FENCED/PARTIAL 策略,且 Kubernetes API + registry 返回的每个当前期望 Pod UID 都对同一完整 control/watermark 提交了匹配 workload 类别/phase 的未过期 ack;状态切到 NORMAL 后仍要等待全部 gate NORMAL 与主进程 APPLICATION_READY/WORKER_READY,不能由普通管理员手工跳过。除此之外的业务和内部命令在 FENCED 时全部拒绝。
12.8 错误语义
| HTTP | 业务语义 |
|---|---|
| 401 | 未登录、会话失效或令牌不可接受 |
| 403 | 主体已认证但无动作权限、认证强度不足或职责冲突 |
| 404 | 对象不存在,或对象在当前数据范围外且需要防枚举 |
| 409 | 版本冲突、重复绑定、状态冲突或同步冲突 |
| 422 | 字段映射或导入行语义不合法 |
| 429 | 登录、同步、导出或高频 API 触发限流 |
当前成功体是 code / message / request_id / data,错误体是 code / message / request_id / timestamp,结构并不一致。实施时应定义统一的新错误契约,至少包含 code / message / request_id / reason_code,可选包含安全的 data 和 timestamp;生产响应不得泄露令牌解析、SQL 或内部堆栈。
Spring Security 的 401/403 发生在控制器之前,必须配置统一 AuthenticationEntryPoint 和 AccessDeniedHandler。当前 Jackson SNAKE_CASE 已使成功响应的 requestId 对外序列化为 request_id;新安全响应仍必须复用 Gateway 请求 ID,不能再生成彼此无关的追踪标识。
13. 前端设计
13.1 新增页面
/sign-in 登录入口或身份源选择;`/login/**` 保留给 Gateway 协议端点
/403 无权限页面
/system/users 人员管理
/system/users/:userId 人员、身份、组织、角色、会话和审计详情
/system/roles 角色管理
/system/permissions 权限目录(只读为主)
/system/identity-sources 身份来源与连接测试
/system/identity-sync-jobs 导入、同步和冲突中心
/system/service-clients 服务身份、影响预览、禁用/启用、轮换和审批
/system/security-recovery PARTIAL 状态下查看 epoch/witness、授权差异、再证明批次和审批;FENCED 时不作为恢复控制台
/system/security-audit 安全审计
/system/sessions 活跃会话与强制下线
13.2 前端认证状态
建议增加集中式 authStore(可使用 Pinia,或项目统一的单例 composable),保存:
me当前人员;- 权限集合;
- 数据范围摘要;
- 登录和会话状态;
- CSRF Token;
- 会话到期提示。
Axios 拦截器负责:
- 默认
withCredentials; - 状态变更请求附加 CSRF Header;
- 401 跳转 Gateway 登录并携带受控
returnTo; - 403 展示权限原因,不自动重试;
- 记录并展示
request_id,便于审计排查。
禁止前端读取或保存 access token/refresh token。
13.3 路由和菜单
路由增加权限元数据:
meta: {
title: "巡检任务",
anyPermissions: ["inspection:task:read"]
}
navigationGroups根据/me权限过滤。- 页面可见但动作受限时保留页面并禁用按钮,显示需要的权限。
- 完全无查看权限的页面从菜单隐藏,直接访问时进入 403。
v-permission或统一组件只改善体验,不是安全边界。- AppShell 显示真实人员、组织、角色摘要、身份来源、会话和退出入口。
/system/security-recovery只在系统已按策略进入 PARTIAL 后提供差异查看、签名清单下载和审批引用提交;全量 FENCED 时普通 Gateway/页面均不可用,必须从独立运维恢复入口执行,不能用回退出的超级管理员会话解围栏。
13.4 人员管理交互
- 列表支持组织、状态、身份源、角色、是否可登录、最近登录筛选。
- 人员详情分为“基本信息、组织岗位、登录身份、角色授权、有效权限、会话、审计”。
- 角色授权先展示差异和数据范围,再提交;高风险角色进入审批。
- 停用前展示未完成任务、工单、值班、责任规则和会话影响。
- 导入页面支持字段映射、预检、逐行错误、冲突处理、提交和回滚。
- 身份源测试应分别显示网络、元数据、证书/签名、客户端认证、Claims 映射和业务握手。
14. Gateway 路由与安全策略
14.1 路由边界
| 路径 | 目标 | 策略 |
|---|---|---|
/assets/**、图标和登录所需静态文件 |
前端静态服务 | 可匿名获取,不包含用户或业务数据 |
/sign-in |
前端登录选择页 | 可展示已启用来源,不返回客户端 Secret 或敏感配置 |
/ 及业务 SPA 路由 |
前端静态服务 | HTML 导航未登录时 302 到 OAuth2 登录;登录后允许 |
/oauth2/**、/login/** |
Gateway/IdP | 协议端点,严格回调白名单;不与 Vue 路由冲突 |
POST /oauth2/backchannel-logout/rail |
Gateway | IdP 专用;验签 logout_token、防重放后按 sid/sub 撤销,不接受浏览器凭据代替 |
POST /logout |
Gateway/IdP | 会话 + CSRF,清理本地会话并触发 IdP logout |
/api/v1/**、SSE |
平台后端 | 浏览器会话、CSRF、Token Relay、限流;未认证返回 JSON 401 |
/mobile-api/v1/**、/partner-api/v1/** |
平台后端 | 仅 Bearer、独立 audience/scope;认证后显式 RewritePath 为既有 /api/v1/**,不接受 Cookie 身份 |
/api/v1/uav/** |
平台后端 | 用户权限由后端判断,不直接把浏览器路由到 UAV 服务 |
/integration/callbacks/** |
厂商回调适配 | 独立签名、mTLS 或服务令牌,不使用浏览器会话;仅登记路径可访问 |
/api/v1/events/flighthub |
当前 UAV 服务兼容回调 | 迁移期保留精确别名和现有 X-FlightHub-Event-Token 校验,不等同于内部 API token bypass |
独立管理端口 /actuator/health/liveness |
内部探针 | 不进入公众 Gateway;仅容器网络/编排器允许 |
独立管理端口 /actuator/** |
内部运维 | 不配置公众 Gateway 路由;管理员权限和网络双重限制 |
/api-docs/**、/swagger-ui/** |
内部运维或生产关闭 | 如保留则置于内部专用 Service/路由,不进入公众 Gateway |
所有路由默认拒绝,公开路径使用明确白名单。禁止使用“未匹配即放行”。
移动端逻辑改写为 ^/mobile-api/v1/(?<remaining>.*)$ -> /api/v1/${remaining},合作方同理;使用 YAML 配置 Spring Cloud Gateway 时按其语法把替换串的 $ 转义为 /api/v1/$\{remaining}。后端沿用既有控制器路径,但根据 token 的 aud/azp/scope 区分客户端能力。公众 /api/v1/** 链拒绝 Bearer,因此合作方不能绕过外部前缀;Gateway 不接受客户端伪造的“原路径类别”Header。
14.2 安全策略
- 限制登录、密码重置、身份源测试、同步、导出和文件上传频率。
- 设置 CSP、HSTS、
X-Content-Type-Options、Referrer-Policy和 Frame 策略。CSP 的connect-src精确加入本地http://objects.rail.test:9000或生产https://objects.inspection.example;若页面直接展示签名图片/视频,再分别加入对应img-src/media-src,禁止*和任意对象域。 - Gateway 生成或透传可信
X-Request-Id,覆盖外部不合法值。 - Ingress、Gateway、Keycloak 和 APM 对
/login/oauth2/code、授权/登出、密码重置、邀请激活和对象签名路径不记录原始 query;采用路径级丢弃或参数白名单,必须剔除code、state、logout_token、action/reset token、签名、credential 和安全票据。排障只保存请求 ID、参数存在性和不可逆摘要。 - 只接受可信代理的
Forwarded/X-Forwarded-*。 - 清理客户端提交的
X-User-*、X-Role-*、X-Actor-*、内部令牌头和调试头;若后端需要审计上下文,只接受平台自身签名的专用上下文令牌。 - Gateway 在读取或缓存请求体之前完成路由匹配、凭据冲突检查、认证、CSRF 和粗粒度限流;认证失败不得先接收数 GB 请求体。
- 禁止在文件路由使用
CacheRequestBody、ModifyRequestBody、自动Retry、请求体日志或本地响应缓存。 - WebSocket/SSE 建连时认证,订阅主题再次校验任务、设备和线路数据权限。
- 页面导航的未认证响应可以 302;
/api/**、SSE、回调和移动端始终返回 JSON 401/403,不能返回 HTML 登录页。 - 未识别的安全模式、路径分类、issuer、audience 或回调配置必须使 Gateway 启动失败,不能回落为
permitAll。
14.3 SSE 跨实例运行规则
当前后端使用进程内 emitter,单纯把 Gateway 或 backend 扩成多副本不能使 SSE 高可用。生产固定采用 RAIL_SSE_DELIVERY_MODE=kafka-broadcast,复用现有 Kafka;local-memory 只允许个人本地单 backend 实例。Helm 生产值缺失、未知或为 local-memory 时应用启动失败,不能带着未选择的模式进入验收。
生产实现规则:
- PostgreSQL 的业务事件表是 replay 权威源,使用
(stream_type, stream_id, sequence_no)唯一约束,并维护business_event_stream_heads(stream_type, stream_id, last_committed_sequence, version_no)。事件创建事务必须先取得该 stream 的事务级 advisory lock/FOR UPDATEhead 行锁,按last_committed_sequence + 1分配序号,再在同一事务插入业务事件与 Transactional Outbox、最后更新 head;锁保持到提交。前一事务回滚时 head 不前进,后一事务取得锁后复用该序号,因此任何可见的last_committed_sequence都代表数据库中最高连续已提交序号,禁止在 JVM/Redis/Kafka 中预分配或先更新 head 后异步补事件。 - Outbox 幂等发布到
rail.sse.events.v1,Key 为stream_type:stream_id。多 publisher 通过独立的 PostgreSQL advisory lock/发布游标行锁按 stream 分片,同一 stream 同时最多一个 in-flight,并且只允许最小未发布sequence_no在前序确认后发送;不同 stream 可并行。Kafka broker 确认后才标记 Outbox published,避免两个 worker 把N+1先于N追加到同一 partition;publisher 还必须验证待发序号连续,发现 business event/head/Outbox 不一致即阻断该 stream 并告警。 - 每个 backend Pod 使用包含 Pod UID 的独立广播 consumer group,使每个实例都收到实时事件,再只投递给本机匹配 emitter;禁止共享 group。新 group 固定
auto.offset.reset=none,assignment 回调立即取得各 partition 的 end offsetE0并显式seek(E0),验证 position 后完成至少一次成功 poll 才把 consumer 标为 ready;不依赖隐式latest或尚未建立的 committed offset。E0之前的数据由数据库 replay 覆盖,E0之后由本机 consumer 缓冲。consumer 固定enable.auto.commit=false,只有记录已进入有序缓冲或完成幂等处理后才提交 offset;崩溃重投由event_id/sequence_no去重。 - backend Pod 只有在上述 consumer-ready 屏障完成后才接受 SSE。建连时先注册 provisional emitter 和按
sequence_no排序/去重的本机缓冲,再从 PostgreSQL writer/具有事务一致性的同一数据源读取 stream head 作为W1,并回放Last-Event-ID < sequence_no <= W1;随后读取第二个 barrierW2,从数据库补回(W1, W2],丢弃缓冲中<= W2的重复项,再原子切换为 live 并按序排空> W2的条目。每段 replay 都必须从 expected sequence 起逐条验证连续性和终点,缺少任一序号就暂停、告警并重试,不能把max(sequence_no)、记录数或有复制延迟的读库结果当连续 watermark,也不能带 gap 切到 live。切换期间继续入缓冲,必要时重复 barrier;禁止在 emitter/缓冲注册前取得 watermark。 - 每个 live stream 维护
next_sequence_no。Kafka 收到received > next时先从 PostgreSQL 补[next, received)并按序投递,再处理当前事件;数据库仍缺号时暂停该 stream、告警并重试,不能越过 gap。received < next作为重复丢弃。该防线即使 publisher/rebalance 异常也维持不丢不乱序。 - 人员停用、关键撤权等由 backend 发起的安全事件,与业务状态及 Transactional Outbox 在同一个 PostgreSQL 事务提交,再发布到
rail.security.session-events.v1;不能先改状态、后以普通 Kafkasend补发。 - 普通登出和 Back-channel Logout 复用 6.6 的内部耐久撤销命令;PostgreSQL
security_revocations + security_revocation_outbox是权威,Redis tombstone 是快速投影。安全事件只含event_id/ledger_sequence/sid_hash/platform_user_id/authz_version/security_epoch/reason/occurred_at等最小索引,不含 token、Cookie 或 PII 快照。 - 每个 SSE 心跳(15~30 秒)按连接索引批量检查安全账本/control state、人员/主体状态和
authz_version;Redis 投影健康时可先做快速拒绝,但 backend 可直接用 PostgreSQL watermark/账本验证,不能把 Redis 缺键当作未撤销。状态不可判定、处于 fencing/rebuild 或账本查询失败时主动断开连接。因此 Kafka 中断或 Redis 投影延迟时,活动连接仍在一个心跳周期内 fail closed。 - Kafka 短时不可用时,业务事件和安全事件都保留在各自 PostgreSQL Outbox;恢复后按原
event_id/ledger_sequence补投,backend Pod 消费端去重。客户端重连仍可从 PostgreSQL 回放业务事件;禁止捕获发送异常后静默丢弃。积压超过 SLA 时 readiness/运维策略按风险降级,不伪造实时成功。
Kafka 生产安全契约:
- 当前单机 Chart 的
PLAINTEXT://kafka:9092只可用于隔离的本地自测。生产 client、inter-broker 和 KRaft controller listener 均启用 TLS,并以 mTLS 或 SASL/SCRAM 建立服务身份;缺少协议、TrustStore/证书或 ACL 时生产启动/部署预检失败。 rail.sse.events.v1、rail.security.session-events.v1和各通知 Topic 由受限 bootstrap job 在应用启动前创建,生产关闭应用自动建 Topic。三节点基线下 replication factor 至少 3、min.insync.replicas至少 2、禁用 unclean leader election;实际副本数/保留期在锁定 values 中按故障域校验。安全 Topic 保留期不短于最大绝对会话期与最大故障恢复窗口,业务 SSE Topic 不短于 Outbox 最长补投窗口;容量不足时告警/扩容,不静默缩短。- 所有可靠 producer 固定
acks=all、enable.idempotence=true、有界delivery.timeout.ms、重试和max.in.flight.requests.per.connection<=5,只有收到满足 minISR 的 broker 确认后才把 Outbox 标记已发布;数据库event_id仍作为端到端幂等键。生产预检验证 broker/topic 配置,不能只依赖客户端默认值。 - backend 业务 Outbox producer 仅有获批业务 Topic 的
Write/Describe,身份/授权安全 Outbox 使用独立 producer principal 且仅有rail.security.session-events.v1的Write/Describe;backend 广播 consumer 仅有rail.sse.events.v1、rail.security.session-events.v1的Read/Describe,其 Group 仅允许rail-sse-broadcast-前缀。UAV、通知及其他消费者使用不同 principal、Topic 和 Group ACL。每个启用幂等写的 producer principal 只额外获得 Kafka Cluster 资源的IdempotentWrite,继续禁止ClusterAction、Topic 通配符、任意 Group 和 ACL 管理权。 - NetworkPolicy 只限制可达性,不能代替 broker 认证授权。安全事件和 SSE Topic 使用最小载荷,消费者仍从受数据范围保护的数据库读取详情。无法解析/校验的安全事件不得跳过后继续确认同分区 offset;隔离原始记录并阻断该分区、告警和人工修复,权威撤销账本仍持续 fail closed。
- 凭据/证书轮换使用短暂双凭据重叠并滚动验证,旧凭据随后撤销;错误 principal、越权 Topic/Group、过期证书和伪造安全事件必须纳入负向测试与审计。
连接层同时满足:
- 每 15~30 秒发送心跳,Ingress、Gateway 和客户端空闲超时均大于两次心跳间隔;
- 支持
Last-Event-ID,在权限允许且保留窗口内重放,超出窗口返回稳定重同步事件; - emitter 注册表按
sid + platform_user_id + topic索引,消费会话撤销、人员停用和关键撤权事件后主动断开; - 主动撤销时先发送
auth-revoked终止事件再断开。原生EventSource.onerror必须主动close(),以去抖方式调用/api/v1/me判定会话;若/me返回 401 则进入登录流程,若会话仍有效则采用带抖动的指数退避重建,禁止浏览器默认无限快速重连; - 即使撤销事件遗漏,连接也设置最大寿命并重新认证;订阅对象变化时再次计算数据权限;
- Gateway 的 SSE 路由
response-timeout=-1,Ingress 关闭响应缓冲和代理缓存;普通 API 仍保留有限超时; - Pod
preStop先停止接收新连接、发送重连提示,再在terminationGracePeriodSeconds内排空;滚动升级验证不中断或可恢复; - HPA 除 CPU/内存外观察活动连接数,避免单实例连接倾斜。
14.4 大文件上传与下载
必须把当前应用限制转换为逐路由的端到端矩阵,Ingress、Gateway、后端、对象存储和临时磁盘任何一层都不能使用更小的隐式默认值:
| 路由类别 | 设计基线 | Gateway/Ingress 要求 |
|---|---|---|
| 普通 JSON/管理 API | 小请求,有限超时 | 严格 body 上限,禁止误用大文件配置 |
| 分片上传 | 单片约 64 MiB,另留 multipart 开销 | 流式转发;每片独立校验、校验和、幂等和会话状态 |
| 证据/业务附件 | 当前业务基线最高约 500 MiB | 只对登记端点放宽;配额、Content-Type 与恶意文件检测 |
| 超大资源导入 | 当前应用配置基线最高 2 GiB/文件、4 GiB/请求 | 优先分片或直传对象存储;进行内存、临时盘和并发容量评估 |
| 下载/视频/制品 | 可能长时传输 | 支持 Range、Content-Range、流式背压和中断续传 |
- Ingress 对上传关闭 request buffering,对 SSE/下载关闭 response buffering;Gateway 不聚合完整 body。
- CSRF Token 放在 Header,并在读取 multipart body 前校验;不得要求解析完整表单后才找到 CSRF 字段。
- 对每个端点设置明确
max-body-size、连接/响应超时和并发配额;413 必须快速、可解释地返回。 - 上传中断、客户端取消、校验失败和会话超时必须回收临时分片;已完成分片通过幂等键避免重复写入。
- 超大文件优先走独立对象数据面直传/直下,不穿过 Gateway 聚合;对象数据面只暴露签名后的 PUT/GET/HEAD,以及用于浏览器 CORS 的受限 OPTIONS 预检,不公开 bucket、List/Delete、管理 Console 或长期凭据。
- 多段直传的 initiate/complete/abort 由平台后端在重新校验权限后调用对象存储,浏览器只获得每个已登记 part 的短期签名 PUT;不能让浏览器自行创建任意 multipart session 或选择最终 Key。
- 原生 S3/MinIO 预签名 URL 绑定对象 Key、动作、大小/校验和、Content-Type 和超短 TTL,但不能承诺在 TTL 内天然一次性。确需“一次核销”的敏感下载必须改走平台下载票据代理,由 Redis/数据库原子消费 ticket 后流式返回;相应用例检查 ticket 复用,而不是错误假设预签名 URL 复用必然失败。
- 直传对象先进入隔离 bucket/prefix,完成大小、SHA-256、真实文件类型、恶意内容扫描和业务归属复核后由服务端发布到正式 Key;扫描前不得被业务列表或下载接口访问。
- 当前 Compose/Helm 把 MinIO root 凭据提供给 backend/artifact-installer,生产必须替换为按工作负载区分的最小权限 service account:backend 仅签名和管理业务前缀,scanner 仅读隔离区/写扫描结果,artifact-installer 只读已发布模型制品;root 仅用于受控初始化且不挂载到应用 Pod。
- 对象数据面 CORS 只允许平台精确 Origin、PUT/GET/HEAD 和必要校验 Header,不允许凭据 Cookie;无签名 OPTIONS 只在
Origin + Access-Control-Request-Method/Headers命中该白名单时返回预检结果且不返回对象数据。浏览器不能自行扩大 Key、动作或 Content-Type。 - 禁止在访问日志记录文件内容或签名 URL 查询参数;测试 2 GiB 单文件、4 GiB 聚合请求、慢上传、主动中断、413、跨实例续传及进程内存/临时盘峰值。
15. 服务间身份
15.1 服务账号
用户身份与服务身份严格分离:
platform-backend调用 UAV、AI、制品安装服务使用 Client Credentials;- 每个服务独立
client_id、受众和 scope; - 每个服务 client 在平台
service_clients以内部 UUID 预注册,(issuer_hash, client_id)唯一映射;未知、归档、缺失当前状态投影或同名重建 client fail closed; - access token 短期有效,密钥保存在 Docker Secret 或外部密钥服务;
- 服务端校验
iss、aud、azp/client_id、scope、jti与 client 当前状态;ACTIVE token 的iat还必须不早于持久化token_valid_after; - 服务账号不能登录管理页面,人员账号不能冒充服务账号。
- 调用方按
client_id + audience + scope缓存短期 token,在到期前加随机抖动提前刷新;并发请求使用 single-flight,避免 IdP 恢复时刷新风暴。 - Secret/私钥轮换使用新旧凭据短暂重叠和明确撤销窗口;配置只引用 Secret,不写入镜像、环境转储或日志。
- 禁用服务 client 的业务事务先把
service_clients.platform_status和撤销账本置为禁用,再由 client-lifecycle worker 停止 IdP 新 token 签发;本机判定立即看到数据库状态,其他 Resource Server 在 control-state/投影传播并 ack 后生效,页面按 12.5 展示DURABLE_ACCEPTED/PROJECTED/GLOBALLY_ENFORCED,不得把事务提交等同全局立即拒绝。重新启用要求RAIL-ACR-3、双人审批、影响预览和 IdP/平台两端确认,普通 user/session worker 无权执行。worker 的数据库角色只能调用带 job/审批/version 前置校验的存储过程,不能任意更新service_clients或安全账本。
建议 scope:
uav.internal.read
uav.internal.command
ai.inference.execute
artifact.install.execute
notification.delivery.execute
当前站内通知仍由 backend 数据库记录;外部短信/邮件/企业通信建议采用 业务事务 + Outbox/Kafka -> notification-delivery worker,backend 不做同步 HTTP 发送。若未来提供重试/管理用内部通知 API,再启用 notification.delivery.execute,并把该 worker 作为独立 Resource Server 验签和检查撤销;不能仅新增地址而不更新默认拒绝网络矩阵。
平台后端是用户授权与用户行为审计的最终边界。后端调用 UAV、AI、制品或通知服务时换用自己的 Client Credentials,不把浏览器 access token、Cookie 或可伪造 X-User-* 身份头继续下传。确需让下游记录原发起人的场景,可附加平台签发、目标 audience 限定、最长 60 秒、带 jti/request_id 的 actor-context JWT;该令牌只用于审计关联,不得作为下游业务授权依据。
Python AI/制品服务至少验证标准服务 JWT 的签名、issuer、audience、client 和 scope,或使用 mTLS;不能仅比较一个静态 Header。现有 X-Access-Token 和其他静态内部令牌在迁移期可双栈支持,但必须设置废弃期限、轮换、来源网段和调用审计,最终移除。
当前 frontend/src/services/videoDemoApi.ts 和 Vite /vision-api 代理只在 Vite 开发模式形成浏览器直连 AI 服务的旁路;生产 frontend/nginx.conf 没有该路由。身份体系实施时应统一改为经平台后端的受权业务 API 调用,或由 Gateway 仅向受保护的适配层路由;浏览器用户令牌不得直接发送给 Python 推理服务。
15.2 厂商回调
- 厂商回调不经过普通用户登录。
- 使用厂商签名、时间戳、nonce、重放窗口、mTLS 或专用客户端凭据。
- 回调路径单独限流,只允许必要 HTTP 方法和 Content-Type。
- 回调生成
SERVICE主体,审计记录厂商、连接、设备和外部事件 ID。 - 回调不能携带
operator_id伪造平台人员。 - 当前
/api/v1/events/flighthub继续校验独立的X-FlightHub-Event-Token;它虽然绕过普通内部 API token 过滤器,但并非匿名。迁移到/integration/callbacks/flighthub时保留旧路径契约测试和明确下线日期。
15.3 Kafka 与异步任务
事件只携带最小审计上下文,不传 access token:
{
"actor_type": "USER",
"actor_user_id": "user-...",
"identity_id": "identity-...",
"request_id": "...",
"trace_id": "...",
"authz_snapshot_id": "..."
}
消费者以服务身份执行,并保留原始发起人。高风险延迟任务在真正执行前重新检查发起人状态或使用已审批的不可变授权票据。
16. 审计与可观测性
16.1 必须审计的事件
- 登录成功、失败、锁定、MFA、登出和会话撤销;
- 本地身份创建、重置、绑定、解绑和禁用;
- 人员导入、同步、冲突、回滚和来源连接测试;
- 组织、角色、权限、数据范围和外部组映射变更;
- 授权拒绝、跨组织访问、直接对象 ID 越权尝试;
- 飞行命令、航线发布、任务审批、告警抑制、工单复核、模型发布;
- 原始资源、证据、模型制品、GIS 专题图和归档下载;
- 应急账号使用和安全配置变更。
对象直传/直下审计区分“平台签发 URL/ticket”和“对象数据面实际访问”。MinIO/S3 数据面审计日志以对象 Key、动作、结果、时间和相关请求标识投递到审计接收器,签名 query 必须脱敏;对必须证明实际下载且一次核销的敏感制品使用平台下载 ticket 代理,不仅记录 URL 签发。
16.2 审计主体
审计同时保存:
- 真实认证主体;
- 业务有效主体(若未来支持合法代理操作);
- 身份来源;
- 会话和认证强度;
- 当时的角色与数据范围快照;
- 对象、动作、结果、原因和请求 ID。
请求体中的操作人字段不能覆盖审计主体。显示名称仅作快照,稳定关联使用 platform_user_id。
16.3 防篡改与可靠写入
“不可修改”不能只靠应用约定,实施机制如下:
- 业务库为安全审计建立独立 owner;运行时写入角色仅有
INSERT/SELECT所需权限,没有UPDATE/DELETE/TRUNCATE。查询 API 使用单独只读角色,普通平台管理员无表级权限。 - 业务状态变更与审计事件在同一数据库事务中写入;需要异步投递到 SIEM/对象存储时使用 Transactional Outbox,消费者幂等并记录投递游标。
- 高风险授权、身份源密钥、人员批量停用、飞行命令、模型发布等事件如果无法形成持久审计,业务事务必须失败;低风险读取日志允许进入本机/消息系统持久缓冲,但必须告警且有容量上限,不能静默丢弃。
- Keycloak 同时启用内置用户/管理员事件持久化和自定义 Event Listener/Exporter。登录、失败、MFA、管理员操作、会话和 broker 事件先在 Keycloak 数据库按容量/保留期形成可补投缓冲,再以稳定
event_id、mTLS/服务身份送入专用审计接收器/SIEM,并用session_id/client_id/request_id与 Gateway、后端事件关联。 - SIEM/网络不可用时 exporter 指数退避并按 watermark 从持久事件库补投;接收端以
event_id幂等。监控最旧未投递时间、积压数、数据库容量和保留窗口,在数据被覆盖前告警/扩容;缓冲逼近上限时关闭非常规 IdP 管理变更入口,但不因同步 SIEM 故障直接拖垮普通登录。投递成功后按保留策略进入 WORM/签名归档,Keycloak 在线事件表只作为有限期缓冲,不是最终不可篡改存储。 before_snapshot/after_snapshot/metadata只允许按事件类型白名单字段,限制深度和大小;密码、令牌、Secret、证件号等敏感字段先剔除,必要 PII 字段列级加密并按权限脱敏展示。- 统一验证反向代理、Gateway、Keycloak、应用、对象存储和 APM 的日志模板;即使请求失败也不得落盘 OAuth code/state、logout token、密码重置/邀请 action token、Cookie、Authorization 或预签名查询串。
- 审计分区按保留制度归档到带保留锁/WORM 能力的存储,或增加批次哈希链与签名清单用于篡改检测。保留期删除只能由隔离的 retention job 按批准策略执行,并为删除批次另留不可变清单。
- 审计导出使用独立
security:audit:export与system:pii:export权限、短期一次性下载、加水印和下载审计。
16.4 指标、探针与告警
| 范围 | 指标/告警 |
|---|---|
| 登录 | 成功率、失败率、锁定、MFA 失败、各身份源延迟 |
| 会话 | 活跃数、刷新失败、强制撤销、异常并发会话 |
| 授权 | 401/403、权限代码、数据范围拒绝、跨组织访问尝试 |
| 同步 | 成功率、延迟、冲突数、停用数、连续失败 |
| IdP | 元数据/JWK 获取、证书到期、Admin API、数据库和集群健康 |
| Gateway | 路由延迟、限流、上游错误、会话 Redis 错误 |
| 安全配置 | 高权限角色变更、应急账号、密钥轮换到期 |
日志和指标禁止使用完整用户名、手机号、证件号或令牌作为高基数字段。
Actuator 使用独立管理端口和独立 Security Chain,不与业务 /api 共享 Cookie 身份。仅精确 /actuator/health/liveness 和 /actuator/health/readiness 在管理端口允许无应用凭据 GET,并只返回 UP/DOWN;该端口不进入公众 Service,NetworkPolicy/主机防火墙仅允许编排器探针。若集群无法稳定表达 kubelet 来源,改用 Pod 内 exec 探针访问 loopback。其余 Actuator 端点仍要求 mTLS/管理凭据。liveness 只反映进程是否需要重启,不依赖 IdP、数据库或外部厂商;readiness 只包含该服务无法承载请求时必需的依赖,例如 Gateway 的 Session Redis、后端业务数据库。IdP 暂时不可达不应让仍可验证已缓存 JWK 的后端反复重启。
Prometheus 通过专用 ServiceAccount/凭据或 mTLS、NetworkPolicy 和管理 Service 抓取,用户网段不能访问;现有仓库只有静态 scrape_configs,尚无 ServiceMonitor/PodMonitor 和上述告警规则。实施时需为登录失败、刷新失败、授权拒绝突增、同步停滞、审计积压、证书到期和撤销事件延迟新增告警,并同步更新健康脚本、Compose 探针和静态抓取地址;若后续引入 Prometheus Operator,再新增相应 ServiceMonitor/PodMonitor。
17. 部署设计
17.1 本地 Docker Compose
新增服务:
gateway Spring Cloud Gateway + Spring Security + Spring Session Redis
identity-provider 参考实现为 Keycloak
identity-continuity-gate IdP 公共入口前的 fail-closed 反向代理;本地对外监听 8180
identity-user-session-worker 复用 backend 镜像的独立 IdP user/session 管理 profile
identity-client-lifecycle-worker 复用 backend 镜像的独立已登记 client 管理 profile
identity-continuity-sentinel 独立读取 IdP 连续性证据并写外部 witness/触发围栏,不承载登录
gateway-session-redis 只承载 Session/OAuth/反向会话索引的专用实例
security-control-plane 复用 backend 安全控制模块的独立 profile,仅内部 mTLS 端口
security-traffic-label-controller 常驻最小 RBAC,只按同 UID ack 把 Pod metadata 从 quarantine 改 normal
security-revocation-projector 复用 backend 镜像的独立 worker profile,无公众端口
security-revocation-redis 独立 ACL + noeviction 的本地撤销投影实例,不与 authz/session 淘汰域混用
kafka-topic-bootstrap 一次性初始化/校验 Topic,完成后退出
security-recovery-controller 仅故障演练/恢复时启动的围栏编排 profile,默认不常驻且无数据/IdP Secret
keycloak-bootstrap-admin-job IdP 管理身份丢失时用官方命令创建一次性 recovery service account,默认不启动
identity-idp-recovery-job 任一新 security epoch 的 IdP 属性同步;IdP 回退时再执行 manifest/key 全恢复,默认不启动
security-session-scrubber-job 恢复时清空专用 Session/OAuth Redis,默认不启动
security-revocation-rebuild-job 恢复时从安全账本重建撤销投影,默认不启动
security-authorization-reattest-job 恢复时按签名清单再证明授权对象,默认不启动
调整流量:
flowchart LR
Browser -->|inspection.rail.test:8088| Gateway
Browser -->|identity.rail.test:8180| IdentityGate
Browser -->|objects.rail.test:9000 签名 URL| ObjectStore
Gateway --> Frontend
Gateway --> Backend
Gateway --> IdentityGate
IdentityGate -->|internal 8181| IdP
IdP -->|Back-channel Logout| Gateway
Backend --> Uav
Backend --> AI
Backend --> ObjectStore
Backend -->|ledger/outbox| Postgres
Projector -->|tombstone/control| SecurityRedis
Projector --> Kafka
- 本地必须发布 Gateway 的
127.0.0.1:8088和 continuity gate 的127.0.0.1:8180,因为 OIDC 授权端点需要浏览器访问身份域;Keycloak 自身仅在 Compose 网络监听identity-provider:8181,不直接发布宿主端口。 - 开发机 hosts 增加
127.0.0.1 inspection.rail.test identity.rail.test objects.rail.test。固定访问入口为http://inspection.rail.test:8088,固定 issuer 为http://identity.rail.test:8180/realms/rail。 - Gateway 容器内部也监听
8088并使用 Docker 网络别名inspection.rail.test,发布映射为127.0.0.1:8088:8088;因此 Keycloak 容器无需继承宿主 hosts 即可访问同一个http://inspection.rail.test:8088/oauth2/backchannel-logout/rail。禁止只把 8088 映射到宿主、却让容器内同名端口不可达。 - continuity gate 监听
8180并持有 Docker 网络别名identity.rail.test,以 mTLS 读取 control-state 后代理到内部identity-provider:8181;Keycloak 虽监听内部 8181,仍设置严格 canonical hostname 为http://identity.rail.test:8180。这样浏览器、Gateway 和后端看到完全相同的 issuer,同时旧/过期 witness 无法绕过 gate 直连 IdP;禁止一边使用localhost、一边使用容器名。 - 需要验证预签名直传时,MinIO API 以网络别名
objects.rail.test监听/发布127.0.0.1:9000:9000,公共签名基址固定为http://objects.rail.test:9000;管理 Console9001不向普通用户发布。若关闭对象数据面,则后端不得签发浏览器不可达的内部minio:9000URL,只能改用 Gateway 流式代理。 - Gateway OIDC 回调精确登记为
http://inspection.rail.test:8088/login/oauth2/code/rail,post-logout redirect 精确登记到同源页面;Web Origin 不使用*。 - Keycloak 客户端的 Back-channel Logout URI 精确登记为
http://inspection.rail.test:8088/oauth2/backchannel-logout/rail,并在 Compose 自测中验证 IdP 容器到 Gateway 的解析和 POST。 frontend、backend、uav-access-service、AI 和 Actuator 只在 Compose 网络中暴露。- 前端 Nginx 只提供静态文件,不再直接代理业务
/api或公开/actuator。 - Keycloak 使用独立数据库或至少独立 schema、数据库用户和备份策略。
- Gateway 会话使用独立 Redis namespace;生产建议使用 ACL 或独立 Redis 实例/集群。
- 本地 projector 使用独立 Redis 用户和 backend worker profile;API 进程环境中不出现 projector/control 写凭据。Topic bootstrap 必须先于 backend/projector readiness 完成;恢复 job 通过显式 Compose profile 启动并输出审计记录。
- 本地提供可重复导入的 realm/client 配置和至少六类测试用户,不使用安全关闭模式冒充登录完成。
建议配置:
RAIL_SECURITY_MODE=enforce
RAIL_AUTH_PROFILE=local-keycloak|production-unified-idp
RAIL_PUBLIC_BASE_URL=http://inspection.rail.test:8088
RAIL_AUTH_ISSUER_URI=http://identity.rail.test:8180/realms/rail
RAIL_AUTH_AUDIENCE=rail-platform-api
RAIL_GATEWAY_CLIENT_ID=rail-gateway
RAIL_GATEWAY_CLIENT_SECRET_FILE=/run/secrets/rail-gateway-client-secret
RAIL_IDP_WITNESS_LEASE=15s
RAIL_IDP_DESIRED_STATE_MANIFEST=/run/config/identity/rail-security-manifest.signed.json
RAIL_SESSION_REDIS_NAMESPACE=rail:gateway:session
RAIL_SECURITY_JIT_MODE=disabled|pending|controlled
RAIL_OBJECT_PUBLIC_BASE_URL=http://objects.rail.test:9000
企业 OIDC/SAML 是统一 IdP 内的 identity_source/broker 配置,不把 Gateway 改成多 issuer 直连;只有建设单位现有 IdP 本身承担唯一内部发行方时,production-unified-idp 才直接指向该 issuer。
disabled 安全模式只能用于迁移前的个人本地故障排查,且不得复用正常平台端口或测试数据。生产只允许 enforce;缺少或出现未知值时启动失败。
17.2 生产入口
推荐使用三个公开访问域名和一个仅内网管理域名:
https://inspection.example Gateway 与巡检平台
https://identity.example IdP Continuity Gate(统一 IdP 公共入口)
https://objects.inspection.example 对象存储签名数据面
https://identity-admin.internal.example IdP 管理面,仅运维网可解析/访问
- 全程 HTTPS,回调 URI 精确登记,不使用宽泛通配符。
- Keycloak 设置 canonical hostname
https://identity.example;若 TLS 在反向代理终止,只信任 Ingress 来源并显式配置受支持的 proxy headers,拒绝用户直接伪造Forwarded/X-Forwarded-*。 - 使用 split-horizon DNS 保持 canonical URL:公网
identity.example指向 continuity-gate Ingress,集群内同名解析到 gate 内部 VIP;gate 再转发 Keycloak public Service,内部端点仍以identity.example的有效证书、SNI 和 Host 提供服务,metadata/JWK/token 返回的 issuer 始终是https://identity.example/...,不得改用.svcissuer 或公网 hairpin。inspection.example对 Back-channel Logout 同样提供到内部 Gateway 的受控解析。 - Gateway 浏览器客户端回调固定为
https://inspection.example/login/oauth2/code/rail,会话 Cookie 使用__Host-前缀且不设置 Domain。 - Gateway 后端端口不对用户网段开放。
objects.inspection.example只反向代理 MinIO/S3 数据 API,阻断 Console、匿名 bucket、List/Delete 和未签名数据操作;仅受限 OPTIONS 预检可无对象签名。精确 CORS 允许https://inspection.example,平台__Host-会话 Cookie 不会发送到对象域。- IdP 数据库、密钥、realm 配置和主题纳入备份恢复。
- JWK/证书轮换提前告警并在预生产环境验证。
- 生产镜像固定版本和摘要,升级先验证协议兼容和回滚。
- Keycloak 使用单独的 admin hostname/listener/Service 暴露只读 digest 与 Admin REST;它不经过 public continuity gate,公共反向代理阻断管理路径,public gate 503/readiness 失败也不得摘除该内部恢复 Service。管理面限制到运维网段并要求 MFA。只有
identity-user-session-worker、identity-client-lifecycle-worker、只读identity-continuity-sentinel和审批窗口内的identity-idp-recovery-job拥有各自精确 egress/凭据;security-recovery-controller只编排围栏,普通 backend API、Gateway 和其他工作负载均不能解析/访问管理域,也不挂载这些 Secret。
17.3 高可用
- Gateway 计算实例无本地会话,使用 Redis 共享 Session 和自定义 Redis Authorized Client,可水平扩展;滚动升级前后序列化版本兼容。
- 生产 Session/OAuth Redis 是只承载 Gateway Session、Authorized Client 和反向会话索引的专用实例/集群,与限流、授权缓存和 Security Revocation Redis 物理分离;采用托管高可用,或跨故障域的 primary+replica 与至少 3 个 Sentinel/受支持 Cluster 拓扑,配置 PDB、反亲和、TLS、ACL、客户端拓扑刷新和受控故障转移,不能以单 Redis 容器支撑多 Gateway。选型必须支持恢复作业以不读取值的方式整库失效并验证完成。
- Session/OAuth 数据和 refresh token 使用应用层信封加密后再持久化,磁盘/快照同样加密并限制恢复人员。撤销事实、全局
security_epoch、三个 token cutoff、authorization_required_epoch_uuid和恢复状态以 PostgreSQL 安全账本为权威,Security Revocation Redis 只做noeviction快速投影;rail:authz:*是可丢弃缓存,不与撤销事实共用恢复语义。 - 登出、强退和撤权先按 6.6 提交 PostgreSQL 账本/Outbox。Redis Session 写与 refresh-token CAS 仍要求主节点确认、配置的副本确认和最大复制延迟;Gateway 记录 Redis primary run-id/config epoch,连接中断、主节点变化或 CAS 结果不确定时不得在新主节点重试/复用旧 refresh token,而是立即把相关流量置为
FENCED并进入恢复流程。 - 安全账本 PostgreSQL 使用受支持 Operator/托管高可用服务,
fsync=on、WAL 持久化,并让撤销/control 事务以及人员停用、成员移除、角色/权限/绑定/范围收紧等身份授权事务以synchronous_commit=remote_apply(或提供方有证据的等价 RPO=0 提交级别)确认到至少一个跨故障域同步副本;同步副本不足时安全变更失败,不能降为本地 commit 后宣称全局成功。普通业务可另行评估可用性策略,但人员与授权表不属于可降级的普通业务。 - OSS Redis Sentinel/Cluster 使用异步复制,即使使用副本确认也不能宣称“已确认写绝不丢”或自动故障转移 RPO=0。本方案对 Redis 或 PostgreSQL 任何提交结果/故障转移不确定的场景选择安全停机而非无损无感切换:恢复控制器先完整执行 17.4.1 的硬隔离;随后以数据库受信时间生成晚于当前整秒与最大时钟漂移的统一未来安全整秒
T_recovery,通过独立security-control-plane在新 primary 上以行锁把状态设为 FENCED、生成随机security_epoch_uuid,并将三个 token cutoff 原子推进到T_recovery。若授权 timeline/版本连续性不能证明,同时生成新的authorization_required_epoch_uuid并进入授权 QUARANTINED。清除 Gateway Session/OAuth,通过 recovery Provider 推进 realm/user not-before、枚举并禁用全部机器 service client、撤销全量用户/offline session/refresh token,并等待受信时钟达到 cutoff;再按 17.4.2~17.4.4 重建撤销投影、授权证明和必要 service client。任何 IdP、数据、授权或凭据步骤未确认时保持 FENCED/PARTIAL,不能按回退数据库自动恢复 ACTIVE。即使数据库回退,所有灾前用户 token 的iat/auth_time、服务 token 的iat均严格早于 cutoff,旧授权图和未再证明机器 client 也因 epoch 不匹配而 fail closed。 - 任何原因只要生成了新
security_epoch_uuid,都必须执行通用 IdP epoch 同步步骤,不能只在 IdP 回退时执行。 control plane 先把 IdP continuity 置为RECOVERING、public continuity gate 保持 503;短期identity-idp-recovery-job使用仍连续的最小 recovery principal,在 Keycloak 单个配置事务/条件 generation 中更新固定 mapper 所读取的 realmrail_security_epoch属性,随后读回新 epoch、mapper、realm digest 和 keyset。sentinel 只有在读回值精确等于 control-state 后才签发新 witness generation,projector 再投影完整 control tuple。若 recovery principal 也不可用,则进入 17.4.5 的官方 bootstrap-admin 路径。跨 PostgreSQL/IdP 不宣称分布式原子性,任一步未知都保持 FENCED;只有此步骤完成后才允许签发合成新 token、执行后续 ack 或恢复身份入口。 - 上述流程保证恢复开放后不复活旧会话,但故障发生到完成 fencing 检测之间仍是 OSS 异步复制的残余风险,因此验收口径不是零停机 RPO=0。若建设单位要求自动切换同时满足已确认写 RPO=0,必须选用有明确强一致/RPO=0 SLA 的会话与撤销存储并单独验证,不能用 Sentinel/Cluster 文案替代能力证明。
security-control-plane生产至少 2 个无状态副本并跨节点/故障域部署,配置 topology spread/强反亲和、PodDisruptionBudget minAvailable: 1、滚动更新maxUnavailable: 0和只选择 ready Pod 的内部 ClusterIP Service;容量与故障域要求更高时采用 3 副本。实例不保存本地恢复状态,所有命令以 PostgreSQL 行锁/幂等 event/全局 Lease 协调;recovery controller、Gateway 和 Resource Server 只访问稳定 Service DNS,禁止绑定单 Pod IP。它的 liveness 只看进程,readiness 要求 mTLS listener 与强 PostgreSQL control-state 读写可用但不依赖 Revocation Redis;Redis 仅影响rebuild-complete的业务前置校验。单 Pod/单节点故障和滚动发布期间 control-state/ack 必须持续可用,否则平台会在 5 秒缓存到期后按设计 fail closed。- IdP 至少双实例、跨节点反亲和并配置 PDB;使用 Keycloak 官方支持的分布式缓存/发现方式、稳定 hostname 和高可用数据库。仅启动两个共享数据库的 Pod 不等于完成 IdP HA。
identity-continuity-sentinel至少双副本且不与全部 IdP Pod 共故障域,使用数据库互斥 lease 选择续签者;15 秒 witness lease 到期即由 IdP admission gate、Gateway 和 Resource Server fail closed。监控同时告警剩余租期、generation 停滞、timeline/digest 漂移,不能把 sentinel 高可用建立在被它验证的同一 IdP 数据库内。identity-continuity-gate生产至少 2 个无状态副本并跨节点/故障域部署,配置 topology spread/强反亲和、PDBminAvailable: 1、滚动maxUnavailable: 0和 ready-only Service;每个副本独立以最多 5 秒缓存核对 control-state,不能由负载均衡器共享一个陈旧 VERIFIED 结果。删除 gate Pod/节点或滚动升级不得绕过 gate 直达 Keycloak。security-traffic-label-controller至少 2 副本、使用 Kubernetes Leader Lease 和幂等 reconcile;非 leader 无 patch 权限生效路径,leader 切换重新核对 Pod UID 与未过期 ack。它故障时新 Pod 只停在 quarantine,已有 normal Pod 不受影响,不允许为了可用性由 Deployment controller 或 init container 自行加 normal 标签。- Keycloak 集群发现、缓存 owner 数、headless Service、节点滚动顺序和会话故障转移必须按选定版本做故障演练;不自定义未经支持的缓存栈。
- JWK 缓存只在 continuity sentinel 仍能续签未过期 VERIFIED witness、keyset digest 未变时,允许业务服务在 Keycloak 进程短时不可用期间继续验证未过期令牌;witness 过期或 keyset 不一致时缓存中的旧公钥不能单独放行。
- Redis 不可用时拒绝新会话和需要会话的请求,不切换到匿名。
- 时间同步是 token、SAML、审计和回调防重放的强依赖。
- IdP 数据库、realm 增量配置、签名密钥、主题和自定义 Provider 定期备份并执行恢复演练;签名密钥轮换保留验证旧令牌所需的重叠窗口。
17.4 Kubernetes / Helm 调整
当前 Helm 主入口为 Ingress -> frontend Nginx -> backend,同时 /api/v1/events/flighthub 存在由 Ingress 精确直达 uav-access-service 的回调分支。目标拓扑统一改为 Ingress -> Gateway:旧路径与新 /integration/callbacks/flighthub 都先进入 Gateway 的精确回调链,再转发 UAV 回调端点;目标版本不再保留 Ingress 直达 UAV 的第二条生产路径。
- Gateway 生产至少 2 个副本,配置 PDB、就绪/存活探针和按流量指标扩缩容;
- backend 多副本前必须设置
RAIL_SSE_DELIVERY_MODE=kafka-broadcast,部署值校验拒绝生产local-memory,并为 SSE/安全事件 Outbox、Topic 和每 Pod 广播 consumer 配置积压告警; - Ingress 强制 TLS,平台域名只路由到 Gateway;对象数据面使用独立 host/Ingress 只路由 MinIO API 端口,绝不暴露 Console;FlightHub 等厂商回调使用明确路径,不使用宽泛前缀;
- 增加默认拒绝的 NetworkPolicy,完整放行边以 17.5 的矩阵为准,不能只配置 Gateway→backend 和 backend→数据服务的摘要子集;
- backend、UAV、AI、PostgreSQL、Redis、Kafka、MinIO 和管理端口均使用 ClusterIP,不使用面向用户网段的 NodePort/HostPort;
- Actuator 使用独立管理端口;仅 liveness/readiness 对受限探针网络免应用认证,其余端点保持认证和网络双重限制;
- Gateway、IdP、Redis Session、JWK、登录和授权拒绝指标加入现有监控采集体系,并新增对应告警规则;
- IdP 和 Gateway 的 Secret 不进入 Helm values 明文,使用 Kubernetes Secret 或外部密钥管理;
- Pod 使用非 root、安全上下文、只读根文件系统和最小网络/文件权限。
17.4.1 可撤销流量围栏
标准 Kubernetes NetworkPolicy 的 allow 规则是叠加关系,创建另一条 default-deny/security-fence 不会撤销已经存在的 allow,也不保证关闭已建立 TCP 连接。因此 security-fence 不是“故障时临时新增一张策略”,而是以下部署期就必须成立并由 Helm/准入策略验证的契约:
- 命名空间级 ingress/egress default-deny 始终以固定
rail.security/workload=true选择所有平台 Pod,该固定标签在围栏时不移除。 - 所有面向常态应用 Pod 的 NetworkPolicy 自身
podSelector、所有数据服务策略中代表常态调用方的from/topeer,以及所有公众/内部业务 Service selector 都必须依赖可撤销的rail.security/traffic=normal;PostgreSQL/Redis/IdP 等安全依赖以固定组件标签被策略选择,但其 normal-client 和 recovery-client 必须是两条分离规则。不允许存在绕过该标签的宽泛 allow、无 selector Service、NodePort、HostPort、hostNetwork 或同命名空间全放行。 security-control-plane、security-recovery-controller、security-traffic-label-controller、identity-continuity-sentinel、identity-continuity-gate和各命名恢复 Job 固定使用rail.security/traffic=recovery,不随普通工作负载缩零或移除标签。identity gate 在围栏中继续存活但公众路由只返回 503/维护响应;traffic-label controller 保留 control-plane/Kubernetes API 最小边,以便新 Pod 完成同 UID 放流。每个 ServiceAccount 只有矩阵列出的 control、数据或 IdP 边,恢复策略不复用normalpeer。- PostgreSQL 安全主从、Security Revocation Redis、Session/OAuth Redis、IdP 和恢复数据 Job 等依赖可以继续运行,但以固定
rail.security/component=*标签和最小 recovery 策略隔离,不进入公众/业务 Service;普通 projector 在FENCED/REBUILDING必须停止写入并释放投影租约。围栏终止的是能够接收或发出用户/业务内部命令的 Gateway、backend、Resource Server 和普通 worker,而不是盲目缩容安全账本及其恢复依赖。 - workload registry 中全部 normal Pod(含 frontend、Gateway/backend/Resource Server、普通 projector、identity/provisioning/notification/异步 worker 与 CronJob)都使用
rail.security/traffic=quarantine启动;quarantine 只允许探针、DNS/NTP/证书校验、按 workload 所需的 Security Revocation Redis 只读和security-control-plane的 control-state/ack,不允许用户入口、Session Redis、业务数据库、Flyway/JPA、Kafka、领取任务或内部命令。 - registry 中每个 normal Pod 都注入固定版本的
security-quarantine-gateinit container、匹配的 ready attester 和 startup barrier;quarantine 时只有 gate 运行,业务/worker 主进程尚未启动。gate 从 Downward API 取得当前 Pod UID,使用只挂载给该 container 的 mTLS 凭据核对 control state/所需 watermark并提交REBUILDINGack;状态切到 NORMAL 后再次核对并提交NORMALack,然后等待自己的 Pod metadata 标签被security-traffic-label-controller改为normal才退出。主进程在同一 Pod UID、normal 网络已可达后启动;此时 readiness 仍为 false,startup barrier 只放行声明的 schema validation/health 等启动动作,拒绝请求、消费、job claim、projector lease、领域写和业务出站。加载最新 control state 后提交 registry 指定的APPLICATION_READY/WORKER_READY,才释放 readiness/consumer/lease;NetworkPolicy 本身不声称区分启动查询与业务命令。 - 正常态 rollout/HPA/CronJob 新 Pod 出生时 control-state 已是 NORMAL,不等待一次不存在的 REBUILDING→NORMAL 跳变:gate 直接核对当前 epoch/watermark、authorization epoch、IdP witness lease 和 keyset digest,提交本 UID 的
NORMALgate ack;traffic-label controller patch 同一 Pod metadata 后 gate 退出,主进程可访问 registry 声明的启动期最小依赖,再提交APPLICATION_READY或WORKER_READY。完成前不得进 Endpoint、消费/领取 job、取得 projector lease 或执行领域写/业务出站。任一检查失败就停在 quarantine 或 readiness=false,controller 失联也只影响扩容/任务可用性,不会把未确认 Pod 放流。 - 所有普通应用的 Deployment/StatefulSet Pod template 永久固定为
quarantine,禁止把 template 改成normal。常驻security-traffic-label-controller仅在 control/ack 条件齐全时 patch 当前同一 Pod UID 的 metadata label 为normal;任何 rollout/HPA/重建产生的新 UID 都重新从 quarantine gate 开始。它不能改 template/scale/Service/Policy,恢复 controller 也不能用 template patch 冒充同 UID 切换。 - Helm 渲染检查和 ValidatingAdmissionPolicy/Gatekeeper/Kyverno 规则阻止普通 ServiceAccount 新增旁路 Service/NetworkPolicy、把 Pod template 或当前 Pod 直接设为
normal、或创建 HostNetwork/HostPort;只有恢复 controller 可操作白名单工作负载的 scale 与入口维护资源,只有 traffic-label controller 可在匹配 ack 后修改当前 Pod 的流量标签,两者 RBAC 不可互换。
故障恢复固定按以下顺序执行,任一步失败都保持入口维护且不进入下一步:
- controller 取得全局 Lease,记录目标 Deployment/StatefulSet/HPA/CronJob 的 generation 与期望副本,启用外部入口维护页并冻结 HPA/CronJob。
- 验证所有目标 Pod template 本来就是
quarantine后,将能够承载用户请求或业务内部命令的普通应用工作负载缩容为零;等待原normalPod UID 全部终止、相关 EndpointSlice 中normalendpoint 清零。若发现 template 为 normal,部署契约已经失效,保持维护并先修复 manifest,不能在本次恢复中临时 patch template 后继续。超过宽限期仍存在的旧 Pod 必须终止,不能仅移除标签后假设 CNI 会切断已有 SSE/HTTP/数据库连接;安全控制平面及列明的恢复依赖保持运行。 - 从 Ingress namespace、代表性普通 ServiceAccount 和外部探针分别测试已有 allow 规则与既有长连接:用户 API、SSE、内部命令、业务数据库和普通 worker egress 必须失败;只有受限 health、control-state、ack 和 recovery 链可达。任何探针仍成功时保持隔离并告警,绝不写入“围栏已完成”。
- 只有上述证据持久化后,controller 才经 recovery mTLS 端点把强数据库状态推进为
FENCED/REBUILDING、生成新 epoch;随后必须先完成 17.3 的通用 IdP epoch 同步、读回 realm digest 和新 witness,再执行 17.4.2 的数据重建与 17.4.3 的授权连续性判定/再证明。即使本次故障只涉及 Redis/平台 PostgreSQL,也不能跳过 IdP 同步。 - 重建完成后,按 registry 和记录的期望副本/Job 模板启动全部 normal 工作负载;只有各自 gate 运行并以当前 Pod UID 提交
REBUILDINGack,业务/worker 主进程、Endpoint、消费者和 job handler 均尚未启动。 - 目标 epoch/watermark 的 gate ack 全齐后切到
NORMAL;gate 再提交同 UID 的 NORMAL ack。security-traffic-label-controller逐个核对后只把这些当前 Pod metadata 改为normal,template 始终不变;gate 观察到自己同一 UID 的标签后退出,主进程在 startup barrier 下启动并独立提交 registry 要求的APPLICATION_READY/WORKER_READY。normal 网络在此阶段已可达,但 Endpoint/readiness、消费、任务、租约、领域写和业务出站仍锁定,只允许启动期最小依赖。只有全部主进程 ready ack 和安全检查通过,才恢复 HPA、CronJob、业务 Endpoint、消费与入口;恢复后的滚动发布/HPA/CronJob 新 Pod 也逐 UID 重走 gate→label→ready,旧 UID、仅有 gate ack、错误 phase、过期 ack 或未通过探针都不能解除围栏。
若建设单位锁定具有 deny 优先级且能经实测终止既有连接的特定 CNI,可用其原生 clusterwide policy 替代第 2 步的 Pod 终止,但产品、版本、优先级语义和故障测试必须写入版本锁;基线方案不依赖该可选能力。
17.4.2 恢复数据作业与临时凭据
围栏 controller 只负责编排和状态推进,本身不持有任何 Redis 数据凭据。Gateway 已缩容为零、普通 projector 已停写后,由两个默认不存在、审批后创建的短期 Job 完成数据恢复:
security-session-scrubber-job固定使用 recovery 标签,只连接专用 Session/OAuth Redis。优先对这个不含其他业务数据的实例/每个 primary 执行受控FLUSHDB ASYNC(托管服务可使用经过验证的等价 namespace/实例轮换),随后以DBSIZE=0和拓扑/副本确认作为证据。JIT ACL 只允许 TLS 连接、必要的主节点发现、PING/DBSIZE/FLUSHDB ASYNC,明确拒绝GET/MGET/HGET/HGETALL/DUMP/KEYS/SCAN、写入新会话以及访问其他 Redis;因此作业不能读取 refresh token 密文。若存储无法提供这种隔离/清空证明,生产预检失败,不能退化为持有全 Redis 管理密码的脚本。security-revocation-rebuild-job使用另一 ServiceAccount/Secret,只读 PostgreSQL 安全控制状态与撤销账本,并持有 Security Revocation Redis 的 recovery-projector 写 ACL;不能连接 Session/OAuth Redis、业务 schema 或 IdP。它必须取得与普通 projector 共用的数据库独占 projection lease,确认普通 projector 已停写,清空专用 Revocation Redis 后按新 epoch 和连续 ledger sequence 重建所有当前有效 tombstone、ACTIVE/DISABLED client 状态与 control 值,最后一步才写 projection watermark。任一 gap/跨 slot/结果不确定均保持 REBUILDING。- 两个 Job 的 Secret 由 CSI/Vault 在获批恢复窗口直接挂载,controller RBAC 不能读取 Secret 内容;Job 成功、失败超时或取消后都立即撤销租约、卸载凭据并保存命令类别、目标实例、前后 watermark/计数和审批审计。两套 ServiceAccount、Redis ACL、NetworkPolicy 不可互换。
security-control-plane只核对 PostgreSQL 状态、Job attestation 与 Revocation Redis watermark,不代替 Job 写数据。重建确认后普通 projector 从已证明的 watermark 取得租约并续投;它不能与 rebuild job 并发写,也不能访问 Session/OAuth Redis。
17.4.3 授权回退隔离与再证明
PostgreSQL timeline/LSN、同步副本确认记录、安全 ledger/control watermark 和授权版本链全部连续时,可以保留原 authorization_required_epoch_uuid。只要任一证据缺失、回退或无法判定,就不得用“用户重新登录”掩盖授权回退,而要在 FENCED 事务生成新的随机授权 epoch、设置 authorization_recovery_state=QUARANTINED 并投影 control 值。此时数据库中全部旧人员状态、组织成员关系、角色、权限边、绑定、范围、外部组映射和委派策略因没有新 epoch attestation而默认不参与授权;回退出来的超级管理员绑定也不例外。
恢复采用默认无权限、逐对象/批次再证明:
- 代码生成的权限目录只可从当前部署镜像内签名 manifest 的 digest 自动再证明;HR/人员权威快照最多证明人员在职状态与组织成员关系,不能自动恢复业务角色、
ALL范围或委派能力。 security-authorization-reattest-job使用 recovery 标签和独立 JIT DB 角色,只读身份/授权规范化表,并只能调用条件存储过程为“当前对象版本 + canonical state hash”写 attestation;不能直接修改业务表、扩大范围或把任意对象标成当前 epoch。角色定义、权限边、绑定、数据范围和委派策略必须展示差异并由两名不同人员以外部审批引用确认。- 恢复用最小 break-glass 身份/角色来自离线签名 bootstrap manifest,稳定主体 ID、权限集合和有效期均固定;recovery Job 只能按该 manifest 幂等 upsert/attest,完成首个受控管理员后立即缩短有效期并审计。manifest 或双人审批不可验证时继续保持全量 FENCED,只能使用 mTLS recovery control plane。
- 正常管理 API 在授权 epoch 已建立后,每次人员状态、成员、角色、权限、绑定或范围变更都必须在同一强事务为新对象版本写当前 epoch attestation;灾后普通管理员只能新建/重新批准期望授权,不能批量复制旧 epoch 证明。
- 授权缓存先整体切换到含新 epoch 的 namespace,再只从已证明图重建。主容器的
APPLICATION_READYack 必须包含其加载的 authorization epoch;不一致即不 ready。
平台可以在最小管理员和业务必需对象完成再证明后把系统 recovery_state 置回 NORMAL、授权状态置为 PARTIAL,此时未证明用户可完成认证但获得零业务权限,并看到“授权恢复中”;也可按建设单位策略保持全量 FENCED 直到批次完成。只有恢复清单中的每个对象均有 ATTESTED 或显式 DENIED/SUPERSEDED 处置后才标记授权状态 NORMAL。若未来建设跨故障域、同步确认且保存完整事件载荷的不可变授权限制镜像,可用它幂等恢复 DENY 后加速再证明;仅有异步 SIEM、最后序号或 hash 而没有完整事件不能作为自动恢复依据。
17.4.4 服务 client 灾后再证明
新 security_epoch 会让所有机器 client 的旧 attested_security_epoch_uuid 自动失配;rebuild job 必须原样重放旧值,禁止依据回退数据库的 ACTIVE 状态写成新 epoch。恢复 principal 同时从 IdP 中枚举全部启用 Client Credentials/service-account 能力的 client,并与离线签名的 service-client inventory 做并集:先全部禁用/隔离,未知或缺 owner 的 client 保持禁用并告警。浏览器 BFF/OIDC client 与机器 client 分类分开,前者的自身 Secret/redirect 恢复使用独立 Gateway 客户端 runbook,不能混成可调用内部 API 的机器身份。
恢复清单中标记为业务启动必需的机器 client,在解除入口维护前逐个执行 12.5 的 RECOVERY_REATTEST:双人审批、撤销全部旧 secret/私钥、生成并部署新凭据、推进 IdP 与平台 per-client cutoff/generation、验证旧凭据不能换 token、新工作负载身份/audience/scope 正确,最后才写当前 attested epoch 的 ALLOW 事件。非必需 client 可在平台进入 PARTIAL 后继续处理,但始终 fail closed。service_token_valid_after 只淘汰灾前 JWT,不能替代旧凭据撤销;IdP disable/rotation、Secret Manager 部署或 attestation 任一步不确定都不得把 client 恢复 ACTIVE。
17.4.5 IdP 连续性围栏与独立恢复
Keycloak 自身是安全状态源,不能因为平台业务 PostgreSQL/Redis 仍为 NORMAL 就把 IdP 单独回退视为普通重启。否则旧 client Secret、旧密码/MFA/WebAuthn 状态、旧认证流程/redirect/mapper/audience 或旧签名私钥都可能重新签出“当前时间”的令牌。目标部署增加 identity-continuity-sentinel 和 IdP 身份入口 admission gate:
- sentinel 至少双副本跨故障域部署,使用 recovery/control 标签和只读 IdP 数据库元数据角色,核对数据库 system identifier、timeline 祖先链、已持久 LSN/同步提交证明、受控
realm_state_generation,再通过只读 Admin Provider 计算 realm 安全配置和公开签名 keyset 的 canonical digest;取得数据库互斥续租权的实例以平台安全库当前时间签发最长 15 秒 witness lease,结果写到 Keycloak 故障域之外的idp_continuity_witnesses。它无用户凭据、私钥和配置写权限,另一副本只在取得 lease 后续写,不能用本地时钟或旧结果延长 expiry。 - 所有 IdP 安全管理变更只允许经受控 worker/GitOps;变更前在平台安全库登记 intent,完成后读回 canonical state 并形成新 witness。用户密码/MFA 等不能导出的材料不复制到 witness,但其数据库提交必须使用受支持的同步 RPO=0 策略,并由 timeline/LSN 连续性证明覆盖。
- 签名 desired-state manifest 独立保存 issuer/hostname、精确 redirect/web origin、scope/audience、Protocol Mapper(含固定
rail_security_epoch)、认证流程、MFA/required action、broker、client inventory、Provider 镜像 digest 和公钥/KMS 引用;只保存 Secret 引用/公钥指纹,不保存密码、client Secret 或私钥。manifest、最后 witness 链头和备份证明位于 IdP 与平台数据库之外的 KMS 签名 + 对象锁/WORM 存储。 - IdP 公共授权、token、device、introspection 和登录路由只有在当前 witness 为
VERIFIED、now < idp_witness_expires_at、generation/digest 与 control-state 一致时才进入 ready Endpoint;Gateway/Resource Server 的业务请求也执行同一 lease 检查。sentinel 全部停更、网络分区或新 primary 切换后最多经过 15 秒就 fail closed,关闭身份入口并触发围栏,最后一次 VERIFIED 不能永久沿用。短时 IdP 进程不可用但 sentinel 仍能证明数据库未切换时,可以继续本地验证既有合格 token;新 primary/PITR 恢复后在 continuity gate 放行前若无法证明包含最后已接受 witness,就触发完整应用围栏,不能靠新节点自报健康开放换 token。
恢复身份本身不能依赖回退 realm 内仍存在正确的 recovery client/角色/Secret。离线 bootstrap root 是“锁定版本的官方 Keycloak 恢复命令 + 外部 Vault/HSM 一次性随机 Secret + 签名 runbook/双人审批”,不在 Keycloak 数据库中预埋万能账号。选择的 Keycloak 版本必须在隔离环境验证官方 bootstrap-admin service 可对已有 master realm 创建临时管理 service account;执行时全部 Keycloak 节点必须停止,并使用与生产相同数据库/优化构建参数。临时账号只用于建立更小权限的 recovery principal,随后立即删除;Secret 租约无论成功、失败或超时都撤销,临时账号缺失删除证明时不得重开任何 IdP ingress。
IdP-only 回退、timeline/LSN 不确定、realm/key digest 漂移或恢复证明缺失时固定执行:
- 外部入口显示维护页,关闭 IdP 身份入口,按 17.4.1 隔离并终止旧 IdP/Gateway/Resource Server Pod 与既有连接,停止全部 Keycloak 节点并冻结所有普通 IdP admin worker;保留 recovery control plane、sentinel 和审批链。
- 在平台强安全库事务把
idp_continuity_state=RECOVERING、recovery_state=FENCED,生成全新随机security_epoch_uuid和authorization_required_epoch_uuid,推进三个未来安全 cutoff;此时可信签名 keyset 为空,所有 token fail closed。即使平台授权库本身连续,也采用新 authorization epoch,避免回退身份重新挂接旧主体/组映射。 - 从一致备份恢复 IdP 数据库后,在所有 Keycloak 服务节点仍停止、仅 IdP DB writer 与 Vault/HSM 可达时运行
keycloak-bootstrap-admin-job;以官方bootstrap-admin service创建随机命名的一次性临时 admin service account。随后启动仅管理内网可达的单个恢复节点,identity-idp-recovery-job使用该账号建立本次 run 专用、操作白名单更小的 recovery principal,并删除 bootstrap account;删除结果、Secret 撤销与数据库读回证明齐全后才继续。 - 以外部签名 manifest 为期望值做增量对账;未知 realm/client/mapper/auth flow/redirect 保持禁用或删除前待人工处置,不能整体导入一个回退 realm 并直接信任其 ACTIVE 状态。
- 所有本地密码、OTP、WebAuthn、恢复码和 MFA flow 只有在同步提交/timeline 证据证明连续时保留;不能证明的
authentication_principal进入RECOVERY_REVERIFY,禁用旧凭据并走一次性密码重置或 MFA 重新注册。外部 OIDC/SAML 身份也必须重新验证上游 metadata/cert、client 凭据、subject 映射和当前一次上游认证;仅凭回退的 broker session 不恢复登录。 - 机器 client 按 17.4.4 全量隔离和逐个再证明;Gateway 浏览器 OIDC client、外部 broker client、IdP 管理 worker 与其他非机器内部 client 使用独立清单撤销全部旧 Secret/私钥、部署新版本,并负向验证旧凭据不能换 token。redirect、web origin、scope、audience、mapper 与认证流程逐项比对,不能因 client 名称相同跳过。
- 从 IdP 数据库快照之外的 HSM/KMS 生成全新签名私钥,激活新
kid;隔离并停用所有恢复出的旧签名私钥。control-state 只登记新公钥集合的 digest,各 Resource Server 清理 JWK 缓存并证明旧kid即使签名数学上正确也被拒绝。疑似回退时不使用正常轮换的旧 key 验证重叠窗。 - 在 IdP 写入新的固定
rail_security_epochmapper/realm 值,清除全部用户、offline、device、broker 和管理员会话,推进 realm/user/client not-before,等待受信时钟越过 cutoff;再用合成用户与服务 client 验证新 token 的 epoch、kid、issuer、audience、azp、ACR/AMR 和 auth_time。删除本次专用 recovery principal,并从管理 API 与数据库两侧证明 bootstrap/recovery 临时身份均不存在;否则保持 FENCED。 - sentinel 读回状态,签署新的 witness generation;恢复作业把 realm/key digest 投影到 Revocation Redis。quarantine gate 和每个主容器 ack 必须绑定该 generation、expiry、keyset digest、security/authz epoch 与 ledger watermark。必要授权、人员和 client 按 17.4.3/17.4.4 再证明后,才可先开放受限身份重验证入口、再进入 PARTIAL/NORMAL;任一证据或负向测试失败都保持 FENCED。
SSE、普通 API 和大文件路由建议拆成独立 Ingress 规则,以便分别设置 buffering、body size 和 timeout,避免为整个站点放宽。身份公共域名/Ingress 只指向 continuity gate,Keycloak public/admin 使用分离的内部 Service 与 NetworkPolicy;生产模式启用 health/metrics,限制管理控制台来源。Gateway、continuity gate 和 IdP 均配置 topology spread/anti-affinity,升级时保持最小可用副本。
生产目标端口固定为下表;当前同端口暴露业务与健康检查的服务必须在迁移后拆分 listener/Service。NetworkPolicy 只引用 named port 并在 Helm 渲染后断言到唯一 targetPort,禁止为了兼容探针直接放行整个 Pod 的所有端口:
| 工作负载 | 业务/内部端口 | 管理端口 |
|---|---|---|
| Gateway | gateway-public 8088;gateway-session-admin 8089 |
gateway-management 9088 |
| frontend | frontend-http 80 |
无;存活检查使用同端口精确 / |
| backend | backend-api 8080 |
backend-management 9080 |
| security-control-plane | security-control-internal 8081 |
security-control-management 9081 |
| identity-continuity-gate | identity-gate-public 8444 |
identity-gate-management 9082 |
| UAV service | uav-api 8091 |
uav-management 9091 |
| vision / pointcloud / artifact-installer | 8101 / 8102 / 8103 | 9101 / 9102 / 9103,仅 health/metrics |
| Keycloak | keycloak-https 8443 |
keycloak-management 9000;集群传输另见 17.5 |
identity.example Service port 443 -> identity-continuity-gate targetPort 8444 -> Keycloak public targetPort 8443,inspection.example Service port 443 -> Gateway targetPort 8088;TLS 与 canonical Host 由锁定的反向代理/Ingress 配置处理。Keycloak 8443 只接受 gate 和明确内部恢复来源,Ingress/用户不能直达。若采用端到端 TLS,Gateway 对外 listener 改为锁定的 HTTPS targetPort,但仍必须在 values/NetworkPolicy 中唯一解析,不能同时放行 HTTP/HTTPS 猜测路径。
17.5 网络访问矩阵
生产 NetworkPolicy 和防火墙按下表建白名单,其余入站/出站默认拒绝;DNS、NTP 和证书吊销服务也必须作为明确依赖登记:
| 来源 | 目标 | 用途 |
|---|---|---|
| 用户浏览器 | Ingress/Gateway 443 | 平台页面、API、SSE |
| 用户浏览器 | IdP 443 | 登录、MFA、登出 |
| 用户浏览器 | 对象数据面 Ingress 443 | 仅短期签名 PUT/GET/HEAD + 受限 CORS OPTIONS;无平台 Cookie、无 bucket 列表 |
| 厂商平台 | 精确 callback Ingress 443 | FlightHub 等签名事件,不访问普通 API |
| Ingress Controller | Gateway gateway-public:8088、identity-continuity-gate identity-gate-public:8444 |
仅声明的业务和身份域名;目标态不直达 backend/UAV/Keycloak |
| 对象数据面 Ingress | MinIO/S3 API Service 9000 | 仅数据 API;Console 9001 和未签名操作不暴露 |
| Gateway | frontend 80、backend API 8080、UAV callback 8091 | 静态页面、业务 API 和精确厂商回调转发;不能访问这些服务的 management port |
| Gateway | security-control-plane:8081 |
耐久撤销命令/control state;独立 mTLS 工作负载身份 + 最小 scope,不走公众路由且不依赖 Redis 认证 |
| Gateway | Session/OAuth Redis、Security Revocation Redis 数据/发现端口(当前 6379;Sentinel 26379) | 前者仅 Gateway 会话凭据可读写;独立 revocation 凭据只读 control,并在账本确认后仅写 sid:* 快速 tombstone;不能写其他 tombstone/control,不访问 Cluster bus |
| backend 认证链/授权模块 | Security Revocation/Authz Redis 数据/发现端口 | 认证链只读撤销/control,授权模块读写 rail:authz:*;不能读 Session/OAuth 区 |
| security-revocation-projector | PostgreSQL 5432、Security Revocation Redis 数据/发现端口、Kafka client TLS listener | NORMAL 时顺序读取安全 Outbox,独立 projector ACL 投影状态/control/watermark并发布安全 Topic;FENCED/REBUILDING 停写并释放租约,不能与 rebuild job 并发,不读取 Session/OAuth |
| security-control-plane | PostgreSQL writer/read VIP 5432、Security Revocation Redis 数据/发现端口 | control-state/ack/recovery 直接使用安全 schema;Redis 只用于 rebuild-complete 核对 watermark,不参与该平面 mTLS 认证或 FENCED 状态读取 |
| identity-continuity-gate | security-control-plane:8081、Keycloak public Service keycloak-https:8443 |
独立 mTLS security.control.read,缓存最多 5 秒;仅 witness VERIFIED/未过期且 generation/realm/keyset digest 一致时转发公众身份路由,未知即 503;无 Admin/DB/Redis/Kubernetes 权限 |
| identity-continuity-sentinel | IdP PostgreSQL 只读 metadata VIP 5432、identity-admin.internal.example:443 只读 digest Provider、security-control-plane:8081、内部 KMS/WORM evidence 443 |
recovery/control 标签 + 独立 SA;只读 system identifier/timeline/LSN/realm public config,以 security.idp-witness.write 续短租约或触发 fence;不能读取用户 credential/Secret/私钥、写 IdP 配置或调用普通业务 API |
| security-session-scrubber-job | Session/OAuth Redis 数据/发现端口 | recovery 标签 + JIT scrub ACL;仅主节点发现、PING/DBSIZE/FLUSHDB ASYNC,不读值、不写新会话、不访问其他 Redis |
| security-revocation-rebuild-job | PostgreSQL 安全 writer/read VIP 5432、Security Revocation Redis 数据/发现端口 | recovery 标签 + 独立 rebuild ACL;DB 角色只读安全账本并可取得投影 lease,按连续账本重建并最后写 watermark,不访问 Session/OAuth/业务 schema;scrubber 凭据在此目标无效 |
| security-authorization-reattest-job | PostgreSQL 身份/授权只读 VIP 5432、security-control-plane:8081、签名 manifest/审批 evidence 只读 443 |
recovery 标签 + 独立 JIT SA/DB 角色/security.recovery.authz-attest;读取 canonical 对象并提交精确 version/hash 证明,不能直接写人员/角色/绑定/范围表、访问 Redis/IdP 或自批 |
| 所有 registry normal Pod 的 security-quarantine-gate/ready attester | Security Revocation Redis 只读数据/发现端口、security-control-plane:8081;主进程启动后仅本地 health |
gate 仅核对 control/watermark、提交绑定当前 UID 的 REBUILDING/NORMAL ack并等待 normal 标签;ready attester 再提交 APPLICATION_READY/WORKER_READY。两者不取得业务数据凭据,gate 凭据不挂载给主进程 |
| UAV/AI/制品/通知等 Resource Server | Security Revocation Redis 数据/发现端口 | 独立只读 ACL,检查 control/global service cutoff、服务 token 的 jti 与 client status/generation/cutoff;不可判定时 fail closed |
| registry 中 frontend、Gateway/backend/Resource Server、普通 projector、identity/provisioning/notification/异步 worker/CronJob | security-control-plane:8081 精确 GET control-state / POST ack;取得 normal label 后另按各行访问启动期最小依赖 |
每类独立 mTLS + security.control.read/security.control.ack 和期望 phase;backend 可直接读 DB 但仍通过该链提交自身 ack。响应缓存最多 5 秒,FENCED 时只允许 gate/自身 ack;normal label 到 ready ack 之间由 readiness + startup barrier 阻止入流、消费、job/lease、领域写和业务出站,不虚报为 NetworkPolicy 断网;ack 只能使用证书绑定的 workload/Pod UID |
| security-recovery-controller | security-control-plane:8081、Kubernetes API 443 |
独立 ServiceAccount + mTLS/security.recovery.execute + 审批引用;只执行白名单 workload scale/freeze、Pod 终止、入口维护、状态编排和创建白名单恢复 Job,不得 patch Pod template/traffic label,且不持有 Redis、IdP Admin、KMS 或数据 Secret |
| security-traffic-label-controller | security-control-plane:8081、Kubernetes API 443 |
常驻双副本/Leader Lease;只读目标 Pod/ack并仅可 patch 白名单 Pod metadata 的 rail.security/traffic 单枚标签,不能改 template/scale/Service/Policy;失联时新 Pod 保持 quarantine |
| keycloak-bootstrap-admin-job | IdP PostgreSQL writer VIP 5432、Vault/HSM JIT Secret 443 | 仅全部 Keycloak 停止且 FENCED 的双人审批窗运行锁定版本官方 bootstrap-admin service;创建随机一次性 admin client,不访问平台/Redis/业务库/公众网络,Secret 到期即撤销且临时 client 必须在开入口前删除 |
| identity-idp-recovery-job | identity-admin.internal.example:443、IdP PostgreSQL 只读恢复验证 VIP 5432、security-control-plane:8081、KMS/HSM/Secret Manager/WORM evidence 443 |
仅 FENCED 审批窗创建的 JIT SA;以 security.recovery.idp-sync 在每次新 security epoch 更新/读回 realm epoch 属性并促成新 witness;IdP 回退时再按签名 manifest 对账、生成全新 signing key/轮换 Gateway 与 broker client、清会话。该 scope 不可用于授权 attestation/Redis,完成、失败或超时即撤销 |
| Gateway | identity.example 内部 VIP 443 |
metadata、授权码换 token、刷新、logout;保持 canonical Host/TLS |
| IdP | inspection.example 内部 Gateway VIP 443 -> 8088 |
精确 Back-channel Logout |
| backend/UAV/AI/制品/通知服务 | identity.example 内部 VIP 443 |
JWK、Client Credentials;所有 Resource Server 均可冷启动验签 |
| identity-user-session-worker | PostgreSQL writer VIP 5432、identity-admin.internal.example:443 |
以仅限 provisioning job/identity 必需表与存储过程的 DB 角色领取/回写任务,并执行 Keycloak 用户/会话白名单操作;独立最小权限 principal,不管理 client/realm/key |
| identity-client-lifecycle-worker | PostgreSQL writer VIP 5432、identity-admin.internal.example:443 |
以仅限 lifecycle job、service_clients allowlist 与审计写入的 DB 角色领取/回写已审批任务;仅可禁用/启用/轮换已登记 service client,独立 client-level principal,不访问用户/realm/key |
| backend 身份模块 | Gateway gateway-session-admin:8089 |
mTLS/Client Credentials + gateway.session.admin;会话查询和强制撤销,不走公众路由 |
| backend provisioning/受控连接器 | 固定 egress proxy 或已批准 HR/SCIM/REST/只读数据库端点 | 人员/组织同步;目标域名、IP、端口、证书和数据库账号白名单 |
| backend | PostgreSQL writer/read VIP 5432、Kafka client listener(当前 9092;生产为锁定的 TLS 端口)、MinIO 9000、UAV 8091、vision 8101、pointcloud 8102、artifact-installer 8103 | 业务数据、SSE/通知 Outbox、对象数据和同步服务调用 |
| notification-delivery worker | Kafka client listener、批准的 HTTPS 443 / SMTP TLS 465 或 STARTTLS 587 / 企业通信 egress | 异步消费通知;外部目标经固定代理/域名端口白名单,失败重试和审计 |
| UAV service | PostgreSQL 5432、Kafka client listener | uav_access 业务状态、Transactional Outbox 和事件消费/发布 |
| UAV service | EMQX 8883(仅生产启用 FlightHub MQTT 时;本地隔离自测可用 1883) | MQTT TLS 订阅;功能关闭时不生成该 egress,Dashboard 18083 不可达 |
| UAV service | 已批准厂商 API | 设备/任务集成,限制域名、IP、端口、TLS 证书和代理 |
| vision-inference、pointcloud-analysis、artifact-installer | MinIO/S3 API 9000 | 读取模型/制品或写入获批前缀;分别使用工作负载级账号,不能使用 root 或跨 bucket/List 权限 |
| IdP | IdP PostgreSQL 5432、LDAPS 636、外部 OIDC/SAML HTTPS 443、SMTP TLS 465/587 | 身份存储、代理和通知;只为已启用能力生成规则 |
| Keycloak 节点 | 同集群 Keycloak Pod 7800、57800 | jdbc-ping 的加密缓存传输与 FD_SOCK2 故障检测;只允许同一 Keycloak ServiceAccount/namespaceSelector |
| Redis 数据节点 | Redis 数据节点 6379、Cluster bus 16379(采用 Cluster 时) | primary/replica 复制、Cluster gossip/故障转移;只允许 Redis Pod 身份和选定拓扑所需的精确端口 |
| Redis Sentinel 与客户端 | Redis 节点 6379、Sentinel 26379(采用 Sentinel 时) | Sentinel 监控/选主;Gateway、Outbox projector 和 Resource Server 仅访问数据 Service 及发现端点 26379,不访问 Cluster bus |
| Kafka broker/controller 节点 | inter-broker 9092、KRaft controller 9093(当前端口基线;生产用锁定的 TLS listener 值) | 分区复制、broker/controller 通信和 controller quorum;client Pod 不可访问 controller listener |
| PostgreSQL replica/HA 控制器 | PostgreSQL primary/peer 5432、选定 Operator 的 Kubernetes API 443 及锁定控制端口 | 流复制、健康检查、选主与故障转移;外部托管数据库由提供方交付等价防火墙矩阵 |
| MinIO 分布式节点 | MinIO 节点 9000 | 对象/纠删码数据和节点健康;只允许同一租户节点,Console 9001 不作为集群数据通道 |
| EMQX 节点 | EMQX peer 4370+节点后缀、Kubernetes 容器 RPC 5369(若选用非容器部署则 5370+后缀) | 仅启用 EMQX HA 时的 Erlang 分布和集群 RPC;实际节点名、端口集合在锁定 values 中展开,不开放到应用外 |
| Kafka Topic bootstrap job | Kafka client TLS listener | 仅建/校验锁定 Topic、分区、RF/minISR/retention;独立 Admin principal,任务结束即失效 |
| Keycloak Event Listener | 内部审计接收器/SIEM HTTPS 443 | 登录、MFA、管理员和 broker 安全事件,mTLS/服务身份 |
| backend 审计 Outbox consumer | SIEM HTTPS 443、带保留锁/WORM S3 443/9000 | 幂等投递、归档和篡改检测清单 |
| MinIO/S3 数据面审计 | 内部审计接收器/SIEM HTTPS 443 | 签名对象实际 PUT/GET/HEAD 事件;查询签名脱敏 |
| Prometheus | Gateway 9088、backend 9080、security-control-plane 9081、UAV 9091、AI/制品 9101/9102/9103、Keycloak 9000 | 仅指标/health,使用专用凭据或 mTLS;不能访问业务 listener |
| 编排器探针/Pod 内 exec | 上述 management health 端口;frontend 80 | 仅 liveness/readiness 精确路径;若 kubelet 来源不可稳定选择则使用 loopback exec |
| 所有需解析的 Pod | CoreDNS/kube-dns UDP/TCP 53 | 只允许集群 DNS Service IP;禁止任意 UDP/TCP 53 egress |
| Kubernetes 节点 | 站点批准的 NTP/NTS 源 UDP 123 或 NTS-KE 4460 | 节点统一校时,Pod 继承节点时钟;普通业务 Pod 不直接访问公网 NTP |
| PKI/证书验证工作负载 | 站点批准的内部 OCSP/CRL HTTPS 443(必要时 HTTP 80) | 证书状态检查;离线站点由受控同步任务更新内部镜像,禁止任意公网 egress |
表中的“当前端口基线”对应仓库现有单机 Chart(PostgreSQL 5432、Redis 6379、Kafka 9092/9093、MinIO 9000、EMQX 1883/8883)。HA Profile 当前关闭内置数据服务,因此上线前必须从选定的 PostgreSQL/Redis/Kafka/MinIO/EMQX Operator 或外部服务生成实例级端点清单,把 client、复制、发现、选主、controller 和 webhook/control 通道逐条渲染为 NetworkPolicy/防火墙规则;同时必须实例化 Keycloak 7800/57800、CoreDNS、节点 NTP/NTS、OCSP/CRL、management named port 和所有外部代理端点。端口、命名空间、服务账号、域名/IP 或协议仍为占位值时部署预检失败。状态组件自己的复制/控制边不能因“不属于应用流量”而遗漏。
17.6 离线交付与版本锁定
铁路现场可能无公网,身份组件必须进入与现有系统一致的离线交付链:
- 在
versions.lock.yaml锁定 Gateway、Keycloak、PostgreSQL/Redis 依赖、主题、自定义 Provider、Helm Chart 和基础镜像的版本与 SHA-256 digest;构建和部署拒绝漂移 tag。 - 离线包使用 OCI layout/归档包含全部镜像、SBOM、签名/校验清单、Helm Chart、realm 增量配置、无密钥模板和数据库迁移;安装前做完整性和架构检查。
- 同包交付经 KMS/建设单位离线根签名的 IdP security desired-state manifest 与 bootstrap/recovery 公钥;生产 Secret、用户 credential 和签名私钥不入包。现场必须把签名 manifest、最后 continuity witness 链头和备份证明复制到独立对象锁/WORM 证据库,不能只留在 Keycloak 或平台 PostgreSQL 中。
- 自定义 Keycloak 主题/Provider 构建为独立可追踪镜像层,版本必须与 Keycloak 兼容矩阵绑定,不能在现场容器内手工复制。
- CI 在断网环境验证
helm template、镜像引用完整性、digest、数据库迁移、realm 配置升级以及最小登录闭环。 - 回滚包保留上一套仍然强制认证的 Gateway 镜像/配置;上一 Keycloak 镜像只能在未发生不兼容数据库 schema 迁移,或已演练“旧镜像 + 一致数据库/密钥备份”整体恢复时使用,不能把单独降级 IdP 容器当作常规回滚。Secret、生产 realm 导出和用户数据不进入通用离线介质。
18. 分阶段实施与迁移
18.1 阶段 0:权限清单与数据准备
- 生成所有后端、UAV、下载、导出和回调接口清单。
- 为每个接口确定公开性、功能权限、数据属性和高风险级别。
- 统一现有权限代码,建立旧代码到新代码映射。
- 盘点所有硬编码
user-*和请求操作人字段。 - 明确企业 IdP、人员来源、组织主键和字段权威来源。
交付物:接口权限矩阵、角色矩阵、字段映射表、迁移清单。
18.2 阶段 1:Gateway、IdP 和真实登录闭环
- 新增 Gateway 和本地 IdP Compose 服务。
- 实现 BFF、Redis 会话、登录、登出、CSRF 和
/api/v1/me。 - 同阶段落地
security_control_state/security_revocations/security_revocation_outbox、独立 projector、内部撤销/control API、Topic bootstrap 和本地恢复演练;真实登出不能先上线后再补耐久撤销。 - 同阶段落地固定
rail_security_epochclaim、可信 signing keyset allowlist、idp_continuity_witnesses、本地 continuity sentinel 和短租约 fail-closed 测试;不能仅校验 JWK 签名而允许回退 IdP 用旧私钥签新 token。 - 内部撤销/control API 以独立
security-control-planeprofile/Deployment 运行,不能与普通 backend API Pod 共进程;本地 Compose 同样通过独立服务验证恢复时自保。 - 同阶段以独立 profile/Secret 启动 user-session worker 与 client-lifecycle worker;backend API 只写 provisioning/lifecycle job,不直接持有 Keycloak Admin 凭据。
- 阶段一即迁移最小
identity_sources、authentication_principals、user_identities、人员状态/授权版本,以及登录所需的角色绑定;没有这些表不能建立确定的内部iss/sub -> platform_user链路。 - 后端加入 Resource Server 并完成认证主体映射;为本地 realm 测试用户生成确定性的人员、principal、identity 和最小角色绑定种子,重复导入保持幂等。
- 前端显示真实人员和会话,处理 401/403。
- 关闭前端 Nginx 对后端和 Actuator 的直接公开代理。
验收:匿名页面导航 302、匿名 API 返回 JSON 401;登录后可访问;令牌不出现在浏览器存储;两台 Gateway 并发刷新和滚动重启后会话仍有效;本机 Cookie 在登出时立即清除,耐久撤销与全局传播状态可查并在 SLA 内完成。
18.3 阶段 2:人员、组织、角色和权限后台
- 完成规范化表迁移和管理 API。
- 实现人员、组织、角色、权限、有效授权、会话和服务 client 生命周期页面/API。
- 完善阶段一的种子绑定和迁移工具,不在此阶段才首次解决主体映射。
- 建立安全审计和授权缓存。
验收:管理员可在页面创建人员、赋角色、限制组织范围、停用并强制下线。
18.4 阶段 3:业务接口权限收口
- 按领域逐批加入方法权限和数据范围。
observe只在集成/预发环境或与强制结果并行的影子计算中记录“若强制将拒绝”的结果,不能作为生产放行模式。- 系统管理、身份源/Secret、角色授权、人员停用、飞行控制、审批/发布、原始媒体和 PII/审计导出等敏感接口从首次上线即
enforce。其余接口按业务域迁移,每个域同时收口列表、详情、写入、附件、统计和导出,不能把高风险接口留到最后。 - 从前端和 DTO 移除硬编码操作人。
- 下载、导出、统计和附件单独验证。
- 在 backend 扩为多副本前,把现有进程内 SSE 发布器迁移为“数据库事件 + Transactional Outbox + Kafka 每 Pod 广播 + 数据库 replay”,移除静默忽略 Kafka 发送失败的路径。
验收:每个接口都有匿名、无权限、有权限、跨组织四类测试。
18.5 阶段 4:外部人员导入和同步
- 实现身份来源、字段映射、预检、差异、提交、回滚和冲突中心。
- 先接文件导入,再接 REST/数据库/SCIM。
- 建立停用、恢复、组织变化和外部组映射策略。
验收:重复同步幂等;冲突不静默覆盖;离职人员按 SLA 停用;历史业务关联不丢失。
18.6 阶段 5:企业 OIDC/SAML 与生产加固
- 在统一 IdP 配置企业 OIDC/SAML 身份代理。
- 完成 Claims、NameID、组织、组映射和登出策略。
- 验证证书/JWK 轮换、IdP 故障、MFA 和应急账号。
- 完成 IdP 同步 RPO=0/timeline 证明、双 continuity sentinel、签名 desired-state/WORM 根、IdP-only PITR/旧 key/旧 credential 围栏与再验证演练。
- 服务间静态令牌迁移为 Client Credentials。
- 完成安全扫描、越权测试、压力测试和灾备演练。
- 普通平台应用灰度不同时升级 Keycloak 二进制;IdP 版本升级使用独立维护窗,先验证数据库 schema 兼容、完整备份/恢复、密钥一致性、RPO/RTO 和会话影响。
18.7 迁移开关
| 模式 | 行为 | 允许环境 |
|---|---|---|
disabled |
不执行认证,仅迁移前故障排查 | 个人本地,禁止生产 |
observe |
完成登录并计算权限,记录潜在拒绝但暂不阻断指定旧接口 | 集成/预发,禁止作为生产主模式 |
enforce |
默认拒绝并完整执行方法与数据权限 | 验收/生产 |
生产配置固定为 enforce。若个别旧接口确需短期兼容,只能使用“HTTP 方法 + 精确路径 + 责任人 + 到期时间 + 工单”的显式 legacy allowlist;默认最长 14 天,到期自动拒绝,且不能包含系统管理、身份、权限、飞行、审批、下载/导出、原始媒体、PII 和审计读取。未知模式、空 allowlist 责任人或无法解析的规则使服务启动失败。
18.8 回滚原则
- 数据迁移只新增表和字段,不删除旧权限 JSON,直到稳定版本后再清理。
- Gateway 回滚到上一套经过验证且仍为
enforce的版本;生产不能用observe、匿名或扩大 allowlist 作为回滚手段。 - Keycloak realm 不是可安全整体覆盖回滚的普通配置文件。采用增量、幂等配置变更;客户端迁移使用新旧 client/redirect/签名密钥重叠窗口,变更前备份数据库和密钥,失败优先前向修复。
- 正常、已证明连续的 key 轮换可使用有界验证重叠;一旦 IdP timeline、realm 或 key 连续性不确定,禁止继续信任旧 key/Secret 的重叠窗口,必须先 fence 并按 17.4.5 生成新 epoch、新签名 key 和新客户端凭据。
- Keycloak 二进制若已升级并执行数据库 schema 迁移,不直接启动旧镜像;失败按该版本官方支持路径前向修复,或在维护窗内恢复同一时点的数据库、签名/加密密钥和旧镜像,并按已确认 RPO 通知会话/配置影响。
- 授权策略保留上一份已强制执行的版本和版本号;回滚仍默认拒绝,并同步失效缓存。
- 人员同步使用批次和来源标记,只回滚本批次可逆变更,不删除已产生业务引用的人员。
- 权限变更和回滚都必须审计。
19. 测试与验收方案
19.1 自动化测试分层
| 层级 | 内容 |
|---|---|
| 单元测试 | JWT 主体映射、绑定内 AND/绑定间 OR、里程数值区间、组织树版本、数据谓词、职责分离、字段权威和冲突规则 |
| 组件测试 | Gateway 各 Security Chain、Redis Session/Authorized Client、Resource Server、IdP Admin Provider、审计 Outbox、Flyway 迁移 |
| 契约测试 | OIDC Metadata/JWK、SAML Metadata/断言、SCIM/人员 API、Client Credentials |
| API 安全测试 | 每个接口的 302/401/403、Cookie+Bearer 混用、跨组织 IDOR、下载、导出、分页统计一致性 |
| 前端测试 | 登录、step-up、登出、会话超时、菜单、按钮、403、人员/角色/导入页面和浏览器存储无令牌 |
| 集成测试 | 创建人员—IdP 预配—绑定身份—赋角色—执行业务—撤权—断开 SSE—强制下线完整链路 |
| 部署测试 | 多 Gateway 刷新/重启、SSE 滚动升级、探针、Prometheus、NetworkPolicy、离线镜像完整性 |
| 故障测试 | IdP、Redis、Gateway、JWK、数据库、审计投递、证书过期和时钟偏差 |
| 性能测试 | 登录/刷新峰值、授权缓存、组织树过滤、SSE 连接、大文件流式传输、同步批次和审计写入 |
现有 scripts/smoke_test.ps1、scripts/navigation_pages_test.ps1、scripts/capability_completion_test.ps1 和全链路脚本目前按匿名 API 调用设计。迁移时应统一增加测试客户端登录或服务账号取令牌能力,并新增明确的 401/403、跨组织和伪造操作人负向断言;不能通过在测试环境关闭 Security 来维持旧脚本通过。
19.2 本地页面自测角色
本地 IdP 至少准备:
local-identity-admin 身份与人员管理员
local-role-admin 角色授权管理员
local-service-client-admin 服务 client 生命周期申请人
local-service-client-approver 服务 client 审批人
local-recovery-reviewer 安全恢复差异与审批流程测试;不持有生产 recovery mTLS 权限
local-dispatcher 任务计划与调度
local-approver 任务/航线审批
local-field 仅能处理分派给自己的工单
local-auditor 安全审计只读
所有角色都使用真实登录流程,不再通过前端写死 user-dispatcher 等值模拟。
19.3 身份与同步验收
- 同一人员可绑定本地、OIDC 和 SAML 身份,业务人员 ID 不变。
- 无登录身份的人员可以作为责任人存在,但不能登录。
- 同一批次重复同步不新增重复人员、组织关系或角色绑定。
- 冲突行可查询、修复和重放,不能覆盖其他人员。
- 两个同优先级权威源给出
ACTIVE/LEFT冲突时人员进入SUSPENDED/CONFLICT并撤销会话;主组织冲突不扩大范围,人工裁决和规则版本完整审计。 - 外部人员停用后在约定 SLA 内禁止新登录并撤销会话。
- 历史任务、审批、告警和工单仍显示已停用人员。
19.4 OIDC/SAML 验收
- OIDC Authorization Code 正常登录、登出和密钥轮换通过。
- 本地浏览器、Gateway 和后端都以
http://identity.rail.test:8180/realms/rail为 issuer,identity.rail.test:8180只解析/发布到 continuity gate,Keycloak 内部 8181 不对宿主或普通容器直达;重启后 issuer 仍一致,redirect URI 只有精确登记值可用。 - Keycloak 容器可通过 Docker alias
inspection.rail.test:8088调用 Gateway Back-channel Logout,不能依赖宿主 hosts 或不可达的 host-only 端口。 - 错误 issuer、audience、state、nonce、过期或错误签名令牌全部拒绝。
- 页面未认证返回 302、API/SSE 未认证返回 JSON 401;同一请求同时带平台 Cookie 和 Bearer 返回
AMBIGUOUS_CREDENTIALS。 - SAML 无签名、错误 Audience/Recipient、过期和重放断言全部拒绝。
- 未映射用户不能因任意外部组声明获得平台角色。
- JIT 默认无业务权限,审核后才可进入业务页面。
- IdP 不可用时的既有会话、新登录和应急账号行为符合设计。
- 两台 Gateway 同时刷新同一会话时只产生一次有效刷新;refresh token rotation、Gateway 重启和滚动发布不导致会话随机丢失。
- 同一人员两个浏览器会话分别持有隔离的 Authorized Client;注销其中一个不删除另一个,过期会话对应的 OAuth 键被清理。
- 加密 keyring 轮换期间旧/新
key_id记录可被所有滚动版本读取,新写只用当前 key;回滚不造成集中掉线,仍被引用或仍在回滚窗口内的旧 key 不允许退役。 - Back-channel Logout 仅接受精确 POST 端点和有效
logout_token;错误iss/aud/events、过旧iat、重复jti、缺失sid/sub、普通 access token 和伪造签名全部拒绝。
19.5 权限与数据范围验收
- 匿名用户不能调用业务 API。
- 每个页面、按钮和 API 均对应稳定权限代码。
ASSIGNEE_SELF用户只能访问分派给自己的工单及附件。ORG/ORG_TREE用户不能通过直接构造 ID、下载地址或导出绕过范围。- 同一绑定的组织、线路、里程范围按 AND,多个绑定按 OR;兼职组织未显式授权时不可访问。
- 角色管理员请求的授权范围必须满足
R ⊆ (D ∩ P);越出授予人组织/线路/项目、空 scope 伪装ALL、没有 privileged 权限或缺少双人审批均拒绝。 - 外部组映射与
EXTERNAL_SYNC派生绑定同样执行子集校验;越界模板、上游 Claim 注入范围、同步任务直接写绑定和任何外部派生ALL均拒绝,映射变更必须重新审批。 - 未经可信回填的历史
created_by不能触发CREATOR_SELF放行;前端伪造创建人无效。 - 列表、详情、修改、附件、下载、导出、统计使用一致的数据范围。
- 创建者不能审批自己的任务、航线版本或模型发布。
- 服务 client 列表/详情、禁用、启用、轮换、审批分别执行精确权限;伪造 client UUID、未知/归档 client、陈旧
expected_client_version、重复 Idempotency-Key 不产生越界或重复任务。禁用按策略审批,事务提交只返回DURABLE_ACCEPTED;注入 projector 延迟、某 Resource Server 5 秒旧缓存和失联但仍在 Endpoint 的场景时不得提前标记全局成功,只有投影与全部目标 ack/摘流证据齐全才为GLOBALLY_ENFORCED。启用/轮换缺少RAIL-ACR-3、由申请人自批或跳过影响预览均拒绝,IdP/数据库任一步不确定时保持禁用或旧凭据有效状态。 - 角色撤销、人员停用和会话强退在目标时限内生效。
- 前端提交伪造
operator_id不改变真实审计主体。
19.6 审计验收
- 登录、登出、失败、同步、身份绑定、角色变更、授权拒绝和高风险业务动作完整留痕。
- 审计包含主体、身份源、角色/数据范围快照、对象、动作、结果、原因、请求 ID、时间和客户端信息。
- 审计不含密码、密钥、Cookie 或完整令牌。
- 普通管理员不能修改、删除或绕过安全审计。
- 可按人员、对象、动作、时间、身份源和请求 ID 查询。
- 直接使用运行时数据库账号执行
UPDATE/DELETE/TRUNCATE审计表必须失败;保留任务之外的分区删除必须失败。 - 高风险授权在审计写入失败时整体回滚;Outbox 重放不产生重复审计或重复 SIEM 事件。
- 快照字段白名单、大小限制、脱敏/加密和一次性审计导出均通过负向测试。
- 中断 Keycloak 到 SIEM/审计接收器网络后,登录/MFA/admin 事件进入持久缓冲并产生积压告警;恢复后按 watermark 补投且
event_id不重复,最终进入 WORM/签名归档,容量阈值策略不静默丢事件。 - Ingress、Gateway、Keycloak、应用、MinIO 和 APM 的成功/失败日志均不包含 OAuth
code/state/logout_token、密码重置/邀请 token、Cookie、Authorization 或预签名 query;对象实际访问与 URL/ticket 签发可按请求标识关联。
19.7 性能和可用性参考指标
以下是首期设计目标,正式值需结合部署规模压测确认:
- 授权缓存命中时新增后端耗时 P95 不高于 5 ms;
- Gateway 转发新增耗时 P95 不高于 20 ms(不含 IdP 登录跳转);
- 普通角色/组织变更和高风险停用的全局传播在 60 秒内完成;本机状态/本机 Cookie 立即改变,但跨 Resource Server 不虚报零时延;
- 10 万人员全量同步可断点恢复,失败行不阻塞无冲突行;
- IdP 单节点故障不影响未过期令牌的本地签名验证;
- 安全审计失败时,高风险授权变更不得静默成功。
19.8 Gateway、SSE、文件与部署验收
- SSE 每 15~30 秒心跳;连接在 Pod B、事件由 Pod A 产生时仍实时到达;
Last-Event-ID可从数据库保留窗口按序重放。专项注入“新 Pod group assignment/E0 seek/首轮 poll、provisional buffer、W1/W2 barrier”各窗口事件;并发启动同一 stream 的事件事务 N/N+1,分别制造前者延迟提交、回滚和进程崩溃,验证 head 只表示最高连续已提交序号且回滚号可安全复用。再让两个 Outbox worker 竞争发送 N/N+1、向初始 replay 和 live consumer 分别注入 gap/重复;任何 replay gap 都不能切 live,最终必须通过数据库补洞保持不丢、不重、不乱序。会话撤销、人员停用和权限撤回会主动断连;前端onerror通过/me区分 401 与瞬时故障,不产生无限快速重连。 - Kafka 中断时事件留在 Outbox、产生积压告警并在恢复后补投;客户端重连仍可从 PostgreSQL replay。生产缺少
kafka-broadcast或误配local-memory必须启动失败。 - Gateway/后端滚动升级期间 SSE 能收到重连提示并恢复;当前进程内 emitter 未改造前不得把 SSE 标为高可用。
- 分片、500 MiB 证据、2 GiB 单文件和 4 GiB 聚合请求按端点矩阵执行;慢上传、客户端中断、413、校验失败后无孤儿临时文件,进程内存不随文件线性增长。
- 下载支持 Range/Content-Range;范围外对象、过期预签名 URL、复用一次性下载 ticket 和越权附件均拒绝。原生预签名 URL 只按其短 TTL/签名语义验收,不伪造“一次性”能力。
- 本地
objects.rail.test:9000和生产对象数据面在真实浏览器中通过精确 CSP 与 CORS preflight 后可完成签名 PUT/GET/HEAD;正确 OPTIONS 返回受限预检,错误 Origin/Header、未签名数据请求、List/Delete、Console、越权 Key 和扩大 Content-Type/size 均拒绝。直传文件在扫描通过前不可见,失败样本不会发布到正式 Key。 - liveness 在 IdP/数据库短时故障时不触发重启风暴;readiness 对真正关键依赖失效能摘流。探针网络访问精确 health 返回 200/503,访问其他 Actuator 未认证返回 401/403,管理端口从用户网段不可达。
- Prometheus 只通过受控管理 Service 抓取;关闭旧端口后 Compose/Helm 探针和脚本仍通过,关键告警可用测试事件触发。
- 默认拒绝 NetworkPolicy 下,白名单链路全部可用,browser/backend 不能直连 AI/UAV 管理端口,未登记 egress 被阻断。
- 默认拒绝 NetworkPolicy 下,CoreDNS UDP/TCP 53、节点 NTP/NTS、内部 OCSP/CRL、Keycloak 7800/57800、所有 app/internal/management named port 均按最终值可达;普通 Pod 不能访问任意 DNS/NTP、公网证书端点或其他服务管理口。
security-recovery-controller可访问security-control-plane:8081/Kubernetes API 443 并执行围栏,Gateway 不能取得恢复 scope。 - 默认拒绝 NetworkPolicy 下,
identity-continuity-sentinel只能读 IdP continuity metadata/公开安全摘要、向 control plane 续 witness 并写批准的 KMS/WORM evidence;security-authorization-reattest-job只能读身份授权对象和提交精确 attestation;keycloak-bootstrap-admin-job只能在 Keycloak 全停的 JIT 窗口访问 IdP writer/Vault;identity-idp-recovery-job只能访问 IdP recovery、KMS/HSM/Secret Manager/manifest。四者互换 ServiceAccount、证书、DB role 或 Secret 后必须全部失败;sentinel 不能读 credential/Secret/私钥或写 IdP,reattest 不能直接改业务表/访问 Redis/IdP,bootstrap job 不能访问 Admin API/平台库,recovery job 不能伪造授权证明。 - 围栏专项必须在常态 allow Policy、已建立 SSE/HTTP/数据库长连接和 HPA 均存在时触发:验证 namespace default-deny 始终选择固定 workload 标签,所有 normal Service/Policy 都依赖可撤销标签;controller 冻结扩缩容、清空 normal EndpointSlice、终止全部围栏前 Pod UID 和既有连接后,用户 API/内部命令/业务数据库均不可达,仅 recovery health/control/ack 链可用。
identity-continuity-gate与security-traffic-label-controller不被缩零,前者稳定返回 503 且仍可读 control plane,后者仍可读 ack/Kubernetes API但不能在 FENCED 时放 normal。分别隔离它们的 recovery 边应安全失败而非绕行;故意保留旁路 allow、无 selector Service、HostPort/HostNetwork 或仍存活旧 Pod 时探测必须失败且状态不能推进;准入策略必须阻止普通 ServiceAccount 恢复normal标签。 - 在 Session Redis、业务 PostgreSQL、Kafka 和应用 JWK egress 全部对 quarantine Pod 关闭时,
security-quarantine-gate仍能以当前 UID 完成 REBUILDING/NORMAL ack,且 Spring/Flyway/JPA 主容器尚未启动。traffic-label controller 只 patch 同一 Pod metadata 后 gate 才退出,主容器必须保持同一 UID、独立加载最新 control state、提交APPLICATION_READY并通过 readiness 后才进入 Endpoint;Pod template 从始至终保持 quarantine。故意让 recovery controller 改 template/label、traffic controller 改 scale/Service/Policy、仅有 gate ack、重建 Pod 导致 UID 变化或主容器安全初始化失败都必须被 RBAC/准入或状态机拒绝。 - NORMAL 状态执行 HPA 扩容和 Deployment/StatefulSet/CronJob 滚动:每个新 UID 从永久 quarantine template 创建,gate 直接验证当前 NORMAL control/watermark/witness/keyset并提交本 UID NORMAL ack,无需等待 REBUILDING 转换;traffic-label controller 只改该 Pod metadata,主进程随后提交 registry 指定的 APPLICATION_READY/WORKER_READY。controller leader 在此间切换仍幂等且不换 Pod UID;controller 全停时新 Pod 安全停在 quarantine,恢复后继续,不能由 init container/Deployment 自行放流。
- 对 frontend、普通 security/SSE projector、identity user-session/client-lifecycle worker、provisioning/sync、notification worker 和一个 CronJob 逐类做 NORMAL rollout 与 FENCED 恢复:gate ack 后、ready ack 前允许建立 registry 明确的 schema validation/health 等启动依赖连接,但 Pod 不进入 ready Endpoint,直连 Pod IP 请求由 startup filter 返回 503,且不得消费 Kafka/数据库 job、取得 projector lease、提交领域写或发起业务出站;审计中只能出现白名单启动动作。ready 后保持同 UID 才释放上述 barrier。删除 gate/attester/barrier、提前启动 listener/scheduler、伪造 workload 类别/phase、让 CronJob template 直接 normal 或把 worker 漏出 registry 时 Helm/准入/集成测试必须失败;不能只验证 HTTP Resource Server 而遗漏后台执行器。
security-control-plane至少双副本测试:持续轮询/提交 ack 时删除一个 Pod、驱逐其节点并执行maxUnavailable=0滚动升级,稳定 Service DNS 始终命中 ready 副本,5 秒窗口内 control-state/ack 不间断;随后在单副本故障期间完整执行一次 fence 的 gate ack/状态推进。客户端直连 Pod IP、PDB 允许同时中断全部副本、readiness 依赖故障中的 Revocation Redis 均必须被部署预检或负向测试拒绝。identity-continuity-gate至少双副本测试:删除一个 gate Pod、驱逐节点和执行maxUnavailable=0滚动升级时身份域仍经 ready gate 工作且无直达 Keycloak Endpoint;隔离任一 gate 到 control plane 后该副本在 5 秒内 503/摘流,不能使用共享陈旧 cache。隔离全部 gate 时公众登录/token 全部关闭,但独立identity-admin.internaldigest/recovery Service 仍可由 sentinel/JIT recovery 身份访问,避免恢复自锁。- 默认拒绝 NetworkPolicy 下,UAV 可访问 PostgreSQL/Kafka;仅在 FlightHub MQTT 开启时可访问 EMQX 8883,关闭时对应 egress 不存在。vision、pointcloud 和 artifact-installer 只能以各自工作负载身份访问 MinIO 9000 的获批 bucket/prefix,不能使用 root 或读取其他工作负载前缀。
- 按最终 HA values 逐条模拟故障:Redis replica/Sentinel 或 Cluster bus、Kafka inter-broker/controller、PostgreSQL 复制/选主、MinIO peer,以及启用时的 EMQX cluster 通道均能在默认拒绝策略下组群和自动切换;删除任一必需规则会使专项网络测试失败。应用 client 不能访问 Redis Cluster bus、Kafka controller、MinIO Console 或 EMQX Dashboard。
- 集群内通过 split-horizon DNS 访问 canonical
identity.example,metadata/JWK/token 的 issuer、TLS/SNI 与浏览器一致;AI/制品服务可冷启动取 JWK,IdP 可调用内部解析的 Gateway 后台登出端点。 - backend 认证 reader 可读取但不能写 Security Revocation 区;普通身份 writer 只写 PostgreSQL;NORMAL 时只有独立 Outbox projector、恢复窗口只有取得互斥 lease 的 rebuild job 可按连续序号写撤销投影/control。它们读取 Session/OAuth Key 都必须失败。Gateway、各 Resource Server、projector、rebuild job 和授权缓存使用不同 Redis ACL,完整前缀契约与轮换不扩大权限。
- Gateway revocation 凭据尝试写
jti/principal/user/client/control必须失败,backend 普通身份 writer 尝试直写任一 Redis 撤销/control key 必须失败;普通 projector 与 recovery rebuild job 不能同时取得 lease,只有当前持有者可按顺序写 control/watermark。两者凭据均不得挂载到普通 API 容器可读路径。 - 在 Gateway 和普通 projector 副本均为零/停写时,
security-session-scrubber-job能把专用 Session/OAuth Redis 每个 primary 清零并以 DBSIZE 证明,不读取任何 token 值;security-revocation-rebuild-job能从强数据库账本重建新 epoch 投影并最后写 watermark。交换两套 ServiceAccount/Secret/ACL 后,两者访问对方 Redis、业务 schema、任意 GET/SCAN 或越权写都必须失败;作业完成/失败/超时后 JIT Secret 均撤销。 - Redis primary 在 logout、强退或 refresh CAS 附近故障时,Gateway 检测 run-id/config epoch 变化后立即 fencing,不能在新主节点重放旧 refresh token。恢复演练在 PostgreSQL
security_control_state生成全新随机security_epoch_uuid,以统一未来T_recovery推进 user-token、user-auth、service-token 三个 cutoff,清除 Gateway Session/OAuth、推进 IdP not-before、撤销用户/offline session 与 refresh token,再从撤销账本重建 noeviction Redis并核对(security_epoch, ledger_sequence)watermark;NORMAL前全部用户请求和内部命令 fail closed。验收明确记录 OSS 异步复制不是零停机 RPO=0。 - 对“仅 Session Redis 故障”“仅平台 PostgreSQL 不确定切换”分别执行通用恢复:每次生成新 security epoch 后,IdP public gate 必须保持 503,
identity-idp-recovery-job更新固定 mapper 的 realm epoch 属性并读回,sentinel 生成匹配的新 realm digest/witness。随后用真实用户和 Client Credentials 各签一枚新 token,断言rail_security_epoch == control.security_epoch且可在满足授权/client再证明后访问;旧 epoch token 必须拒绝。故意跳过 IdP 更新、返回旧 mapper 值或让读回/witness 失败时系统必须保持 FENCED,不能出现“恢复成功但新 token 永久 401”。 - 安全账本事务只有在 PostgreSQL
synchronous_commit=remote_apply/等价 RPO=0 提交确认后才能返回“耐久撤销已接受”;同步副本不足必须失败,投影/目标 Pod ack 未完成前不能返回“全局撤销成功”。模拟 PostgreSQL 不确定 failover/回退时先应用网络 fence,再生成随机 epoch + token cutoff并全量重建;恢复前的浏览器 Session 和移动 JWT 全部失效,不能因数据库序号回退复活。 - 分别把 PostgreSQL 恢复到人员停用、组织成员移除、角色绑定撤销、role-permission 删除和数据范围收窄之前的快照;连续性不可证明时必须生成新
authorization_required_epoch_uuid,切换授权缓存 namespace。没有当前 epoch attestation 的人员/边即使显示 ACTIVE 也获得零业务权限;版本/hash 不符证明被拒绝,回退出来的超级管理员不能签自己的证明,只能按离线签名 bootstrap manifest 和外部双人审批恢复最小 break-glass。恢复清单存在未证明、未显式 DENIED/SUPERSEDED 对象时不得把授权状态标为 NORMAL。 APPLICATION_READY与运行时 ack 必须精确携带 security/authz epoch、IdP witness generation/expiry、keyset digest 和 ledger watermark;缺字段、旧 generation、expiry 已过、hash 不同或用 gate ack 冒充主容器均不 ready。PARTIAL 下未证明用户可以重新认证但业务权限为零,页面明确显示授权恢复中。FENCED/REBUILDING时普通 API、内部命令、消费/任务领取和 Redis 依赖认证全部拒绝,但独立 mTLS control-state/ack/recovery 精确端点仍可直接访问 PostgreSQL,不发生恢复自锁。只有 registry 中当前 Kubernetes 期望 Pod UID 的 gate 对目标 control/watermark 提交 REBUILDING/NORMAL ack,且同 UID 主进程随后提交匹配类别的APPLICATION_READY/WORKER_READY,恢复 Job 才能解除 fence;伪造/过期/错误 phase/已终止 Pod ack 或 gate 冒充主进程无效。- 仅有
security.control.read的证书不能提交 ack;带 ack scope 的 Pod 也不能替其他 workload/Pod UID、旧 epoch 或更高 watermark 确认。重复自身 ack 幂等,证书撤销后立即拒绝。 - Outbox projector 多副本竞争、单实例崩溃、Redis Cluster 跨 slot 写失败和故意制造 sequence gap 时,只有持有数据库锁的 NORMAL projector 或互斥 recovery rebuild job 处理;状态/tombstone 先于 watermark,watermark 只推进到最高连续已确认序号。可信 control-state 与 Redis watermark 不一致时 Gateway、UAV、AI、制品和通知服务均 fail closed,不能用陈旧
NORMAL自证健康。 - 普通登出和 Back-channel Logout 在 Kafka/Redis 中断时仍先把同一
event_id提交到 PostgreSQL 撤销账本/Outbox;现存 SSE 在一个 15~30 秒心跳周期内通过账本/control 检查断开。恢复后 Redis projector 和 Kafka producer按ledger_sequence补投,各 Pod 按event_id去重;模拟 Gateway 在账本提交后的任意一步崩溃,会话不能在恢复开放后复活或无限保持 SSE。 - Kafka 生产 listener 必须使用 TLS + mTLS 或 SASL/SCRAM;业务/安全 Outbox producer、各 Pod 广播 consumer、UAV 和通知 worker 的 Topic/Group ACL 均按最小权限验证,幂等 producer 只有额外
IdempotentWrite而无ClusterAction。错误 principal 不能读写rail.sse.events.v1/rail.security.session-events.v1,不能加入未授权 Group,旧轮换凭据在重叠期结束后失效。 - Kafka 生产关闭自动建 Topic;bootstrap job 预创建并验证 RF≥3、minISR≥2、retention 和 unclean leader election 禁用。producer 必须以
acks=all + enable.idempotence=true通过故障测试;少于 minISR 时 Outbox 保持未发布,不能先提交 offset/状态再丢事件。 - IdP 管理权限做逐身份负向测试:
identity-user-session-worker只能使用自己的 DB job/identity 最小角色且不能管理 realm/client/key/管理员角色,identity-client-lifecycle-worker只能使用自己的 lifecycle job/service_clientsallowlist DB 角色,不能访问用户、未知 client、realm/mapper/key,且无双人审批不能重新启用或轮换;两者不能互换 DB 角色、IdP principal 或 Secret。默认拒绝 NetworkPolicy 下,普通 backend API Pod、Gateway 和其他工作负载均不存在 Admin Secret 且不能连管理域,两个 worker 只能访问 PostgreSQL writer 与各自精确 IdP admin egress。security-recovery-controller永不取得 realm/KMS Secret;identity-idp-recovery-job的 recovery principal 在正常态不可取得,只能在已审批 FENCED 窗口通过短期租约挂载,完成或超时后自动撤销并留下审批、调用与卸载审计。 - 杀死全部 continuity sentinel 或隔离其 IdP DB/control-plane 网络,强数据库签发的 15 秒 witness 到期后,IdP 授权/token 入口和所有 Gateway/Resource Server 必须 fail closed,不能继续沿用最后一次 VERIFIED。再把 IdP 切到低于 witness LSN 的旧 primary 或未知 timeline,即使 Keycloak readiness 为 UP 也必须触发完整 fence;双 sentinel 并发续租只能有一个有效 generation,过期调用方不能延长 lease。
- 做 IdP-only 回退演练:平台安全/业务 PostgreSQL 保持正常,把 Keycloak 数据库恢复到旧密码/OTP/WebAuthn、旧 service/Gateway/broker Secret、旧 redirect/mapper/audience/auth-flow 和旧 signing key 仍有效的快照。系统必须生成新 security/authz epoch 与 cutoff、清全部会话、默认隔离人员授权和机器 client;旧 Secret 不能换 token,旧本地凭据进入
RECOVERY_REVERIFY,旧kid即使仍在进程 JWK 缓存且签名正确也被拒绝。只有新 HSM/KMS key、固定新 epoch claim、签名 desired-state digest、凭据/MFA 再验证、必要 client/授权再证明和全目标 ack 都通过才进入 PARTIAL/NORMAL;任何 timeline/LSN、manifest、key inventory 或负向证明缺失都保持 FENCED。 - 分别把 IdP 恢复到“recovery client 不存在、仅旧 Secret 可用、recovery 角色被删/降权”三类快照;所有 Keycloak 节点和公众入口未完全停止时
keycloak-bootstrap-admin-job必须拒绝,满足围栏后官方 bootstrap service 才能用 Vault/HSM 一次性随机 Secret 建立临时 admin。常规 worker Secret 不能冒充;建立最小 recovery principal 后 bootstrap account 必须删除。故意让删除、租约撤销或两侧不存在证明失败,系统保持 FENCED 且网络上无法使用残留账号。 /mobile-api/v1/**、/partner-api/v1/**经认证后正确改写到既有/api/v1/**;Bearer 直打公众浏览器/api/v1/**被拒绝,错误客户端 audience/scope 不因改写而放行。- 恢复 cutoff 后,旧移动 refresh token、OIDC offline token 和旧 Gateway refresh token 即使换到新
iat的 access token,也因旧auth_time、IdP not-before 或已撤销 session 被拒绝;只有 cutoff 后重新完成认证的新会话可访问。专项在故障发生的同一 epoch 秒签发用户 token并建立auth_time,验证统一未来T_recovery使两者都严格小于 cutoff,不能因>=比较和时钟宽容放行。另模拟 PostgreSQL 回退丢失近期CLIENT DENY/凭据轮换事实:灾前全部 Client Credentials JWT 必须因iat < service_token_valid_after在 backend、UAV、AI、制品和通知端拒绝,只有 IdP service-client not-before 与受信时钟到达新 cutoff 后重新签发的 token 可用。 - 现有
/api/v1/events/flighthub和新回调别名在迁移期都通过 HTTPS、签名/Token、防重放测试,普通内部令牌 bypass 不能绕过回调专用校验。 - Client Credentials 缓存、提前刷新、并发 single-flight、新旧 Secret 轮换、错误 audience/scope 及 Python 服务验签全部通过。
- 禁用服务 client 或写入服务 token
jti后,backend、UAV、AI、制品等所有接收方均在目标时限内拒绝已签发 token;重新启用时把 IdP not-before 和平台token_valid_after/generation推进到未来安全整秒,禁用前 token 在重新启用后仍拒绝,只有iat >= cutoff的新 token 可用;同秒边界、负向 clock-skew 和投影缺失不能放行。撤销 Redis 不可用时内部命令 fail closed。 - 服务 client 凭据轮换分别验收:
PLANNED撤销旧 Secret 后,页面在旧 JWT 最大exp前显示SECRET_ROTATED_TOKEN_DRAINING且不宣称 token 已撤销;EMERGENCY_REVOKE必须先让 CLIENT DENY 全局生效,再轮换、推进 future cutoff/generation 并写 ALLOW,旧 Secret 和旧 JWT 都拒绝。向 IdP、Secret Manager、数据库提交和投影/ack 的每个边界注入超时/未知结果,按 request/version 对账且 client 保持 DISABLED/可重试,任何部分成功不得显示 SUCCEEDED。 sid/jti的 HMAC hash keyring 轮换按“全服务双读→新写→所有旧版本撤销事实超过 token/session/refresh/offline 最长有效期并确认清理水位→退役旧 key”执行;数据库账本、Redis key 和 Kafka payload 的hash_version/key_id一致,原始 claim 不落日志。服务 client 禁用使用稳定内部 UUID,不受 HMAC 退役影响;长期禁用跨多次 key rotation 后仍拒绝。旧/新 token 在窗口内均能命中撤销,错误 key/version 或未知 client fail closed。- 默认拒绝网络下 backend 可用 Client Credentials 调用 artifact-installer;外部通知仅经 Outbox/Kafka 到 notification-delivery worker,再访问批准的 egress,未批准目标被阻断且不丢重试状态。
- Gateway 内部
/internal/gateway/v1/sessions/**仅从内部 Service 使用 mTLS/Client Credentials +gateway.session.admin可达;公众端口、Cookie、错误 scope 和未知内部路径全部拒绝,人员页面仍可查询/强退会话。 - 离线环境不访问公网即可完成镜像校验、Helm 渲染、数据库迁移、realm 增量配置和真实页面登录。
- Keycloak schema 升级失败演练不能仅启动旧镜像;按已演练的一致数据库/密钥恢复或支持的前向修复达到 RPO/RTO,会话和配置影响与预案一致。
20. 待建设单位确认的外部输入
以下信息不阻塞本地参考实现,但会影响生产接入:
- 企业统一身份平台类型、OIDC/SAML 元数据、issuer、客户端、证书和退出能力。
- 人员主系统、稳定外部人员 ID、增量机制、字段字典和停用语义。
- 正式组织树、组织编码、岗位、专业、线路和责任区关系。
- 哪些字段分别由 HR、统一身份和巡检平台权威维护。
- 外部组到平台角色的映射、审批人和委派边界。
- 是否允许多组织归属、兼职和临时借调。
- MFA、高风险二次认证、密码、会话和并发登录制度。
- 离职停用 SLA、审计保留期和个人信息脱敏要求。
- 生产域名、TLS 证书、内外网边界、运维网段和密钥管理方式。
- 移动端、第三方 API、企业微信等后续客户端的 OAuth 客户端类型。
未提供上述材料时,平台可以完成本地 IdP、人员角色页面、权限执行、模拟外部 OIDC/SAML 和导入流程的自测;不能据此宣称已通过企业统一身份的生产验收。
21. 预期代码与配置落点
| 范围 | 建议落点 |
|---|---|
| Gateway | platform/gateway/ |
| 后端认证配置 | platform/backend/src/main/java/com/ai/trackwalker/identity/config/ |
| 当前主体 | platform/backend/src/main/java/com/ai/trackwalker/identity/authentication/ |
| 授权与数据范围 | platform/backend/src/main/java/com/ai/trackwalker/identity/authorization/ |
| 人员与同步 | platform/backend/src/main/java/com/ai/trackwalker/identity/provisioning/ |
| 安全审计 | platform/backend/src/main/java/com/ai/trackwalker/identity/audit/ |
| 安全控制/撤销账本 API | platform/backend/src/main/java/com/ai/trackwalker/identity/securitycontrol/ |
| Security Control Plane 部署 | 同一 identity/securitycontrol/ 代码的独立 security-control-plane profile;deploy/charts/trackwalker/templates/security-control-plane.yaml 与 PDB/ready-only Service,独立 ServiceAccount/DB 角色/mTLS Secret,生产副本与故障域约束按 17.3 |
| IdP continuity 与恢复 | platform/backend/src/main/java/com/ai/trackwalker/identity/idpcontinuity/;deploy/charts/trackwalker/templates/identity-continuity-gate.yaml、identity-continuity-sentinel.yaml、keycloak-bootstrap-admin-job.yaml、identity-idp-recovery-job.yaml,独立 public gate、只读/JIT ServiceAccount、witness lease、官方 bootstrap service 与签名 manifest |
| Workload 启动屏障 | platform/backend/src/main/java/com/ai/trackwalker/identity/securitycontrol/startup/ 及各非 Java 服务的共享 attester/wrapper;security-workload-registry.yaml 声明 ready phase、启动期依赖、listener/job/lease/outbound barrier |
| 撤销 Outbox projector | 同一 identity/securitycontrol/ 代码,使用独立 Spring profile/启动类和独立 Deployment,不复用 API Pod 凭据 |
| IdP 管理 Provider/worker | platform/backend/src/main/java/com/ai/trackwalker/identity/idpadmin/;user-session、client-lifecycle、recovery 三个独立 profile/ServiceAccount/Secret |
| 数据迁移 | platform/backend/src/main/resources/db/migration/Vxx__identity_and_authorization.sql |
| 前端认证状态 | frontend/src/security/ |
| 人员权限与 PARTIAL 恢复页面 | frontend/src/views/system-identity/、frontend/src/views/system-security-recovery/ |
| Gateway/IdP Compose | infra/docker-compose.identity.yml,稳定后合入主 Compose |
| 本地 IdP 配置 | infra/identity/,仅提交无密钥模板和测试 realm |
| Kafka Topic bootstrap | deploy/charts/trackwalker/templates/security-topic-bootstrap-job.yaml 与本地 Compose init profile |
| IdP 管理 worker 部署 | deploy/charts/trackwalker/templates/identity-admin-workers.yaml;两个常驻 worker 分离 ServiceAccount/Secret/NetworkPolicy,recovery Job 不包含在常规 Deployment 中 |
| Quarantine gate、ready attester 与恢复数据 Job | deploy/charts/trackwalker/templates/security-quarantine-gate-config.yaml、security-workload-registry.yaml、security-recovery-data-jobs.yaml;gate/attester 注入所有 registry normal Pod(含 frontend/projector/worker/CronJob),准入拒绝漏注入;scrubber/rebuild/authorization-reattest Job 默认不创建且使用不同 JIT Secret |
| 恢复控制与 NetworkPolicy | deploy/charts/trackwalker/templates/security-recovery-rbac.yaml、security-traffic-fence-controller.yaml、security-traffic-label-controller.yaml、security-network-policies.yaml;Pod template 永久 quarantine,独立 controller 分别负责 scale/fence 与同 UID metadata label,恢复 Job 默认禁用,审批后按 runbook 创建 |
| 接口权限矩阵 | docs/security-endpoint-permission-matrix.md 或 CSV |
22. 实施完成定义
只有同时满足以下条件,身份权限建设才可标记为“完成”:
- 平台有真实登录、登出、会话和当前用户,而不是固定测试用户。
- 人员、组织、角色、权限、身份来源、同步和服务 client 生命周期均可从页面管理。
- 本地账号、外部人员导入和至少一种标准 SSO 均完成可重复测试。
- 所有业务 API 默认认证,功能权限和数据权限均由后端执行。
- 前端不再提交或硬编码可决定审计身份的用户字段。
- 下载、导出、统计、附件和异步任务不存在旁路权限。
- 用户停用、角色撤销和强制下线达到约定生效时限。
- 服务间静态共享令牌完成替换或有明确、受控的迁移期限。
- 登录、同步、授权、拒绝和高风险动作均有不可修改的安全审计。
- 匿名、无权限、有权限、跨组织和职责冲突测试全部通过。
- 撤销账本、Redis 投影、Kafka 广播、独立 control plane 与 normal/quarantine/recovery 标签 fencing 演练通过;workload registry 覆盖 frontend、所有 Resource Server、projector、worker 和 CronJob,任何新 UID 未经 gate + 正确 ready phase 不可入流/消费/取 lease;既有连接和旁路 allow 已被故障注入证明可阻断,不以 additive NetworkPolicy 或异步 Redis 的未证明能力宣称硬隔离/RPO=0。
- 授权数据库回退默认无权限并完成对象再证明;IdP-only 回退、sentinel 租约过期、旧签名 key/Secret/credential 和配置漂移均通过故障演练,未满足外部 witness、签名 manifest、新 epoch/key、凭据/MFA/client/authz 再证明及全 Pod ack 时系统不能进入 NORMAL。
23. 参考资料
23.1 当前仓库
platform/backend/pom.xmlplatform/backend/src/main/resources/db/migration/V13__capability_completion_foundation.sqlplatform/backend/src/main/java/com/ai/trackwalker/foundation/service/FoundationCompletionService.javauav-access-service/src/main/java/com/ai/trackwalker/uavaccess/config/InternalApiTokenFilter.javafrontend/src/router/index.tsfrontend/src/services/api.tsfrontend/src/services/videoDemoApi.tsfrontend/src/layouts/AppShell.vuefrontend/nginx.confinfra/docker-compose.ymldeploy/charts/trackwalker/templates/ingress.yamldeploy/charts/trackwalker/values.yamldocs/railway-uav-capability-gap-closure-design.mddocs/frontend-left-sidebar-tab-division-design.md
23.2 官方技术资料
- Spring Security OAuth 2.0
- Spring Security OAuth 2.0 Resource Server JWT
- Spring Cloud Gateway WebFlux Token Relay
- Spring Cloud Gateway HTTP Timeouts
- Spring Security WebFlux CSRF
- Spring Session Redis 配置
- OpenID Connect Back-Channel Logout 1.0
- Keycloak Server Administration Guide
- Keycloak Admin REST API
- Keycloak Production Configuration
- Keycloak Bootstrapping and Recovering an Admin Account
- Keycloak Hostname v2
- Keycloak Distributed Caches
- Redis Sentinel High Availability
- Redis Cluster Scaling and Cluster Bus
- PostgreSQL Synchronous Replication
- Apache Kafka Listener Configuration
- EMQX Cluster Security and Port Mapping