跳到主要内容

Identity 产品化重构规划

本文档不是一次性“大改目录”的任务清单,而是基于当前 Identity 代码现状,为认证、账号、组织、权限和会员能力建立可演进产品的分阶段重构方案。

1. 目标与边界

Identity 的产品目标是提供统一的身份平台:

  • 用户可以注册、登录、绑定外部身份、管理设备和安全设置;
  • 管理员可以管理用户、组织、职位、角色和权限;
  • 业务模块只依赖稳定的身份契约,不感知 OpenIddict、FreeSql 或具体存储;
  • Member 是用户域上的业务扩展,而不是第二套用户体系;
  • BasicIdentity 是轻量认证能力,不能与完整 Identity 形成隐式重复实现。

本规划覆盖:FreeKit.IdentityFreeKit.Identity.InfrastructureFreeKit.Member 及 Identity 自身的 Host/ApiClient。FreeKit.BasicIdentity 是独立的简化版项目,不属于本次产品和代码重构范围,也不与完整 Identity 设计兼容层。认证协议实现、管理后台 API、数据迁移和前端配套均在范围内;不在第一阶段拆分微服务。

2. 当前结构与主要问题

当前项目已经具备较完整的业务面,但边界按历史功能增长,存在以下结构性问题:

  1. FreeKit.Identity 同时承担身份核心、管理后台、OAuth/OpenIddict、日志、设置、在线用户和基础资料,应用层职责过宽。
  2. PermissionService 已改为通过 IAuditBaseRepository<Permission>IAuditBaseRepository<PermissionGrant>IPermissionManager 访问权限数据;ProfileService 已改用 IUserRepositoryMemberService 也已移除 userRepository.Orm 子查询。当前裸 ORM 债务主要集中在 EmployeeServiceRoleServiceTenantServiceTokenHandlerSerilogService.Orm/ADO 调用,Identity 仍缺少类似 CmsKit 的架构守护测试来阻止债务回流。
  3. IdentityMember 的职责边界不够产品化,容易造成登录、用户资料和会员资料重复维护。
  4. 权限、角色、组织和职位的授权模型需要统一为“资源权限 + 作用域”,否则前端和业务模块会继续出现特例判断。
  5. 设置、Token、设备验证、邮件和外部登录等横切能力散落在多个目录,缺乏明确的安全策略和审计边界。
  6. 许多历史代码使用 DateTime.Now、直接 ORM 查询和非统一异步命名,重构需要分阶段收敛,不能一次性重命名或替换。

3. 业务流程诊断

这次重构的重点不是把目录重新命名,而是把用户从“进入系统”到“成为可运营会员”的路径变短、变清晰、可追踪。

3.1 注册与首次登录

当前能力分散在 AccountService、邮件验证码、OAuth2、Profile 和 Member 的事件处理器中。产品上应收敛为一条明确状态流:

需要优化的业务规则:

  • 注册成功只创建一次身份,Member 资料由用户创建事件幂等初始化;
  • 验证码发送、重发、过期和失败次数使用同一策略,不能由各登录入口分别实现;
  • 登录结果明确区分“账户不存在、密码错误、待验证、锁定、需要二次验证”,前端据此展示下一步动作;
  • OAuth 首次登录必须进入“绑定已有账户或创建新账户”的确认流程,不能静默创建重复用户;
  • 注册、登录、绑定外部账号、改密都写入同一安全事件链。

3.2 登录与会话安全

TokenHandlerRefreshTokenServiceDeviceServiceUserTokenServiceOnlineUserService 当前分别提供能力,产品上应表现为一个“会话中心”:

  1. 登录成功创建会话并记录设备、客户端、IP、风险结果;
  2. 刷新令牌采用轮换,旧令牌立即失效;
  3. 用户可以查看设备列表、单独退出设备或退出全部设备;
  4. 改密、启用/关闭二次验证、管理员锁定时全部会话失效;
  5. 风险登录进入二次验证,不重复创建半有效 Token。

3.3 账户资料与安全资料

当前 ProfileService 同时处理公开资料、账户资料和安全操作。目标流程应拆开:

  • 公开资料:头像、昵称、简介,修改后立即可被业务模块读取;
  • 账户资料:邮箱、手机号、登录名,修改前必须验证旧凭证或二次验证;
  • 安全资料:密码、二次验证密钥、恢复码,只能通过安全操作接口修改,不能复用普通 Profile DTO;
  • 每次敏感资料变更都撤销高风险会话并通知用户。

3.4 组织、职位与授权

组织、职位、角色和权限不能只作为后台 CRUD。管理员真正需要的是“把人放进组织并立即获得/回收工作权限”的闭环:

重构要求:

  • 用户、组织、职位、角色分配支持批量操作和预览差异;
  • 权限变更显示“谁、对什么资源、获得了什么权限、何时生效”;
  • 离职/禁用/移出组织必须自动回收职位和组织授权;
  • 权限缓存按用户和角色精确失效,不能等待固定 TTL;
  • 所有授权决策能解释原因,便于后台排查 403。

3.5 Member 业务流程

Member 不应只是 Identity 用户表的附属 CRUD。建议把它定义为用户生命周期上的运营域:

  1. 身份用户激活后,幂等创建 MemberUser;
  2. 业务行为产生积分流水,而不是直接覆盖积分余额;
  3. 积分变化触发等级评估,等级变化记录原因和生效时间;
  4. 标签、分组和等级用于运营筛选、权益和内容推荐;
  5. 身份禁用不删除会员历史,会员状态变为不可用;
  6. 身份硬删除由事件处理器清理可删除数据,但保留合规要求的账务/审计记录。

当前 MemberUser、积分日志、等级、分组和标签已经具备雏形,下一步重点是补齐状态流、幂等和事件失败重试,而不是继续增加管理接口。

3.6 AccessKey 生命周期

AccessKey 已收敛为独立的凭证生命周期,当前流程为:

  1. 创建成功只在响应中返回一次 Secret,列表和详情仅返回末四位掩码;
  2. 数据库沿用原 access_key_secret 列名保存 sha256: 版本化摘要,启动迁移会先提取历史 Secret 末四位,再把历史明文原位转换为摘要;
  3. Host 鉴权先按 AccessKeyId 查找,再固定时间校验摘要,并统一检查启用、过期、撤销状态以及绑定用户仍为 Active;
  4. 轮换会原子替换摘要,旧 Secret 立即失效;创建、轮换、状态/过期变更、使用和撤销均进入 AccessKey 审计日志;
  5. 撤销记录撤销人、UTC 时间、原因并永久禁用凭证;旧 DELETE 路由仅作为兼容入口执行撤销,不再删除审计记录;
  6. 鉴权成功时条件更新最后使用时间,可阻止鉴权与轮换或撤销并发时继续放行旧 Secret。

本次存储升级不允许新旧后端滚动共存:新版本把原列原位哈希后,旧实例仍按明文比较,会拒绝全部 AccessKey。发布顺序必须是暂停 AccessKey 创建/轮换等管理操作、摘流全部旧实例、备份目标表、启动单个新实例完成并校验迁移、部署配套前端后再扩容和恢复流量。旧前端会忽略创建响应,也不能在这段窗口继续创建 Key。迁移失败不得记录版本号。客户端必须在创建或轮换响应后立即安全保存 Secret,因为后端无法恢复或再次展示。

4. 目标产品模型

目标模型将 Identity 拆成五个稳定能力域,仍部署为模块化单体:

能力域核心职责代表对象
Account注册、登录、凭证、外部身份、找回和二次验证User、Credential、ExternalLogin
Access角色、权限、组织作用域和授权决策Role、Permission、Policy、Scope
Directory用户资料、设备、在线状态和组织目录Profile、Device、OrgUnit、Position
Membership等级、积分、标签、地址等业务会员扩展MemberProfile、Level、Integral
Security Operations登录风险、审计、Token、通知和安全事件LoginRecord、SecurityEvent、AccessToken

依赖方向:

业务模块只能引用 Contracts 或应用层接口;不能直接引用 OpenIddict 实体、Identity.Infrastructure 仓储实现或 IFreeSql

5. 关键设计决策

4.1 保留模块化单体

第一阶段不拆分认证服务。认证、权限和资料在同一数据库事务内协作,能降低 Token、权限缓存和用户状态一致性成本。只有当登录流量、组织规模或部署隔离成为明确瓶颈时,才评估独立 Identity Provider。

4.2 统一用户身份

FreeKit.Identity 提供唯一 UserId 和账户生命周期;Member 通过 UserId 关联身份用户,不再复制登录名、密码、Token 或权限。FreeKit.BasicIdentity 作为独立项目拥有自己的简化模型和发布节奏,不参与本系统的依赖关系。

4.3 权限采用显式作用域

权限判断统一表达为:Subject + Permission + ResourceScope。管理员、租户、组织和资源所有权在 Application 层完成解析,Controller 只转发。现有 GetEffectiveUserIdAsync 继续作为兼容入口,逐步替换散落的手写判断。

4.4 仓储隔离

新增代码不得在 Application/Controller 注入 IFreeSql,也不得通过仓储的 .Orm 属性绕过边界。复杂查询、批量删除和原子 Token 操作通过 Domain Manager、Query Service、专用仓储方法或 Infrastructure Adapter 封装。为 Identity 增加架构测试,扫描构造函数、字段、属性及方法体中的直接 ORM 使用;对现有债务建立明确基线,阻止新增调用。

当前基线中,PermissionService 已符合该方向:CRUD 查询通过审计仓储完成,授权读写委托给 IPermissionManagerProfileService 通过 IUserRepositoryIdentityUserManager 访问用户数据。两者下一阶段的重点分别是统一授权语义,以及拆分公开资料、账户资料和安全操作,而不是再次迁移 ORM。

4.5 安全默认值

  • Token、刷新令牌、二次验证和外部登录必须有明确过期、撤销和重放保护策略;
  • 所有安全敏感操作产生结构化审计事件;
  • 写入时间使用 DateTime.UtcNow
  • 外部邮件/HTTP 使用统一 HttpClient resilience 管线;
  • 密码、密钥和验证码只在专用安全服务中处理,DTO 不返回敏感字段。

6. 分阶段实施

Phase 0:基线与保护(1 周)

  • 建立 Identity 架构测试:禁止 Application/Controller 直接依赖 IFreeSql 或调用仓储 .Orm,并为当前存量建立可逐项消除的基线;
  • 盘点所有路由、权限常量、Token 流程和数据库表;
  • 对登录、刷新 Token、改密、禁用用户、权限变更补充契约测试;
  • 记录当前性能基线:登录 P95、权限校验 P95、用户列表 P95、Token 刷新成功率。

验收:测试能在 CI 中阻止新增架构债务,且现有 API 契约有可回归样例。

Phase 1:打通用户生命周期(2-3 周)

  • 抽出 IAccountServiceICredentialServiceIExternalLoginServiceISessionService
  • 统一注册、验证、登录、锁定、验证码、刷新令牌和注销语义;
  • 将首次 OAuth 登录改为绑定确认流程;
  • 将 OpenIddict 细节封装到 Infrastructure Adapter;
  • 删除重复的旧 Application Service,不保留双写流程。

验收:密码登录、外部登录、刷新、注销、改密和设备验证均通过同一账户状态机。

Phase 2:会话与安全闭环(1-2 周)

  • 将设备、Token、在线用户和二次验证收敛为会话中心;
  • 实现改密、锁定、禁用、二次验证变更时的全会话撤销;
  • 增加风险登录、验证码、外部登录绑定的安全事件;
  • 提供用户端“设备管理”和管理员端“强制下线”。

验收:任意敏感操作都能确定影响哪些会话,并可在后台审计追踪。

Phase 3:统一 Access(2-3 周)

  • 建立权限注册、角色分配、组织作用域和授权决策接口;
  • 保持 PermissionService 的仓储 + IPermissionManager 边界,为权限树、授权复制和权限变更补充契约测试;复杂查询新增专用 Query Service,不回退到 .Orm
  • 统一管理员权限与资源所有权判断;
  • 增加权限缓存版本号,权限变更后按用户/角色精确失效;
  • 为 CMS、Platform 等模块提供只读授权契约。

验收:同一权限在 API、应用服务和缓存三处语义一致;普通用户、租户管理员、平台管理员有明确测试覆盖。

Phase 4:组织授权业务闭环(2-3 周)

  • 将 Profile、Device、OnlineUsers、OrgUnits、Positions 按能力域整理;
  • 保持 ProfileServiceIUserRepository / IdentityUserManager 访问数据,并补充事务、事件发布和会话撤销集成测试;
  • ProfileService 的公开资料、账户资料和安全操作拆成清晰的应用边界与 DTO;
  • 设备注销、异常登录和在线状态统一进入 Security Operations。

验收:资料接口不返回安全字段,设备撤销能立即阻止会话继续使用。

Phase 5:Member 运营闭环(1-2 周)

  • Member 仅通过 Identity Contracts 获取用户身份;
  • 会员等级、积分、标签和地址保留在 Member 域;
  • 删除 Member 中重复的用户生命周期逻辑;
  • 为会员事件定义稳定事件契约,避免反向依赖 Identity 内部实现。

验收:删除/禁用身份用户时,会员状态有明确策略,不产生孤儿数据或重复账户。

Phase 6:性能与运维(持续)

  • 用户、权限、角色和组织列表统一分页和投影,禁止加载无关导航;
  • 为高频查询补充复合索引并记录 SQL 执行计划;
  • Token、登录、授权和审计增加指标与结构化日志;
  • 对邮件、外部 OAuth、验证码服务配置超时、重试和熔断;
  • 将迁移脚本、回滚脚本和数据校验纳入发布流水线。

7. 数据与兼容策略

Identity 内部重构不保留旧 Application Service。采用“新边界一次切换,必要时保留短期 API 路由兼容”的顺序:

  1. 先冻结并记录现有 API 契约、权限和数据字段;
  2. 新边界直接替换旧 Application Service,避免保留重复服务;
  3. 数据库新增字段先允许为空,并提供回填脚本;
  4. 回填完成后增加唯一约束/非空约束;
  5. 仅在前端尚未切换时保留短期路由/DTO 兼容映射,切换完成后删除。

生产环境不依赖 FreeSql CodeFirst 自动完成破坏性变更;每个阶段提供显式 DDL、回滚脚本和校验 SQL。

8. 测试与验收矩阵

层级必须覆盖
Domain账户状态、锁定、凭证、角色作用域、会员关联
Application普通用户/管理员权限、租户隔离、设备撤销、AccessKey 轮换/过期/撤销、幂等
API登录、刷新、注销、改密、权限管理、资料脱敏、AccessKey Secret 一次性返回
IntegrationOpenIddict、Redis 缓存失效、邮件、数据库事务
Architecture禁止裸 ORM、依赖方向、Controller 薄层、敏感 DTO
Performance登录 P95、授权 P95、分页列表 P95、并发刷新 Token

9. 风险与停止条件

  • 如果旧前端依赖未记录的字段或错误码,不直接删除旧契约,先添加兼容映射;
  • 如果数据库表由多个宿主同步,迁移必须先确认所有宿主版本;
  • 如果 OpenIddict 版本升级改变 Token 行为,必须单独灰度,不与目录重构混在同一发布;
  • 如果权限缓存无法证明失效正确,宁可暂时降低缓存时间,也不要返回过期授权结果。

10. 第一批可执行任务

  1. 画出并确认注册、登录、绑定、改密、设备撤销、组织授权和会员升级状态图。
  2. 新增 Identity 架构测试项目和依赖扫描规则。
  3. 直接建立 IAccountService 新边界并迁移用户生命周期,不增加旧服务 facade。
  4. 实现会话中心:设备列表、单设备退出、全设备退出和敏感操作撤销。
  5. 将组织成员变更与授权刷新绑定,增加权限差异预览和审计记录。
  6. 将 Member 积分、等级、标签变更改为幂等事件和可追踪流水。
  7. PermissionServiceProfileService 的现有仓储边界增加架构回归测试,并将 EmployeeServiceRoleServiceTenantServiceTokenHandlerSerilogService 中的 .Orm/ADO 调用迁入专用仓储、Query Service 或 Infrastructure Adapter。
  8. 建立登录、刷新、注销、权限变更、设备撤销和会员升级的集成测试。
  9. 为每个阶段补充 DDL、回滚脚本和前端兼容说明。

11. 相关源码

  • src/Services/Identity/FreeKit.Identity
  • src/Services/Identity/FreeKit.Identity.Infrastructure
  • src/Services/Identity/FreeKit.Member
  • src/Services/Identity/FreeKit.Identity.ApiClient
  • basic/src/FreeKit.BasicIdentity.Host
  • .github/guidelines/architecture.md
  • .github/guidelines/permissions.md
  • .github/guidelines/conventions.md

当前 ORM 边界状态

位置当前状态处理建议
FreeKit.Identity/Application/Permissions/PermissionService.cs已使用审计仓储和 IPermissionManager,未直接注入 IFreeSql 或访问 .Orm保持当前边界;复杂权限查询进入专用 Query Service,补充架构与授权契约测试
FreeKit.Identity/Application/Profiles/ProfileService.cs已使用 IUserRepository / IdentityUserManager,未直接访问 .Orm保持仓储访问;重点拆分资料与安全职责,补充事务、CAP 事件和会话撤销测试
FreeKit.Member/Application/Members/MemberService.cs已通过用户与会员审计仓储分别查询,不再使用 userRepository.Orm保持双仓储边界;后续复杂筛选下沉到 Member Query Service
FreeKit.Identity/Application/Employees/EmployeeService.cs通过 userRoleRepository.Orm.Delete<IdentityUserRole>() 执行跨实体批量删除在用户角色仓储或 Domain Manager 中提供显式批量解绑方法
FreeKit.Identity/Application/Roles/RoleService.cs通过 userRoleRepository.Orm.Delete<IdentityUserRole>() 执行跨实体批量删除与 Employee 共用显式的用户角色批量解绑能力,避免重复裸 ORM
FreeKit.Identity/Application/Tenants/TenantService.cs多处通过 .Orm 跨用户、租户、角色实体查询和删除,并显式关闭租户过滤器提取租户成员 Query Service 与成员关系仓储;将跨租户访问封装为可审计的显式接口
FreeKit.Identity/Application/Account/Token/TokenHandler.cs通过 userTokenRepository.Orm 协调 Token 持久化操作将原子轮换和会话写入封装到 Token Persistence Adapter,并保持并发安全测试
FreeKit.Identity/Application/Logs/SerilogService.cs通过 serilogBaseRepository.Orm.Ado 执行日志表 TRUNCATE将表名白名单和清理命令下沉到 ILogRepository 实现
FreeKit.Identity/Application/OpenIddict/OAuth2AdminService.cs使用 ICompositeRepository当前不属于 ORM 债务