Identity 产品化重构规划
本文档不是一次性“大改目录”的任务清单,而是基于当前 Identity 代码现状,为认证、账号、组织、权限和会员能力建立可演进产品的分阶段重构方案。
1. 目标与边界
Identity 的产品目标是提供统一的身份平台:
- 用户可以注册、登录、绑定外部身份、管理设备和安全设置;
- 管理员可以管理用户、组织、职位、角色和权限;
- 业务模块只依赖稳定的身份契约,不感知 OpenIddict、FreeSql 或具体存储;
- Member 是用户域上的业务扩展,而不是第二套用户体系;
- BasicIdentity 是轻量认证能力,不能与完整 Identity 形成隐式重复实现。
本规划覆盖:FreeKit.Identity、FreeKit.Identity.Infrastructure、FreeKit.Member 及 Identity 自身的 Host/ApiClient。FreeKit.BasicIdentity 是独立的简化版项目,不属于本次产品和代码重构范围,也不与完整 Identity 设计兼容层。认证协议实现、管理后台 API、数据迁移和前端配套均在范围内;不在第一阶段拆分微服务。
2. 当前结构与主要问题
当前项目已经具备较完整的业务面,但边界按历史功能增长,存在以下结构性问题:
FreeKit.Identity同时承担身份核心、管理后台、OAuth/OpenIddict、日志、设置、在线用户和基础资料,应用层职责过宽。PermissionService已改为通过IAuditBaseRepository<Permission>、IAuditBaseRepository<PermissionGrant>和IPermissionManager访问权限数据;ProfileService已改用IUserRepository,MemberService也已移除userRepository.Orm子查询。当前裸 ORM 债务主要集中在EmployeeService、RoleService、TenantService、TokenHandler和SerilogService的.Orm/ADO 调用,Identity 仍缺少类似 CmsKit 的架构守护测试来阻止债务回流。Identity、Member的职责边界不够产品化,容易造成登录、用户资料和会员资料重复维护。- 权限、角色、组织和职位的授权模型需要统一为“资源权限 + 作用域”,否则前端和业务模块会继续出现特例判断。
- 设置、Token、设备验证、邮件和外部登录等横切能力散落在多个目录,缺乏明确的安全策略和审计边界。
- 许多历史代码使用
DateTime.Now、直接 ORM 查询和非统一异步命名,重构需要分阶段收敛,不能一次性重命名或替换。
3. 业务流程诊断
这次重构的重点不是把目录重新命名,而是把用户从“进入系统”到“成为可运营会员”的路径变短、变清晰、可追踪。
3.1 注册与首次登录
当前能力分散在 AccountService、邮件验证码、OAuth2、Profile 和 Member 的事件处理器中。产品上应收敛为一条明确状态流:
需要优化的业务规则:
- 注册成功只创建一次身份,Member 资料由用户创建事件幂等初始化;
- 验证码发送、重发、过期和失败次数使用同一策略,不能由各登录入口分别实现;
- 登录结果明确区分“账户不存在、密码错误、待验证、锁定、需要二次验证”,前端据此展示下一步动作;
- OAuth 首次登录必须进入“绑定已有账户或创建新账户”的确认流程,不能静默创建重复用户;
- 注册、登录、绑定外部账号、改密都写入同一安全事件链。
3.2 登录与会话安全
TokenHandler、RefreshTokenService、DeviceService、UserTokenService 和 OnlineUserService 当前分别提供能力,产品上应表现为一个“会话中心”:
- 登录成功创建会话并记录设备、客户端、IP、风险结果;
- 刷新令牌采用轮换,旧令牌立即失效;
- 用户可以查看设备列表、单独退出设备或退出全部设备;
- 改密、启用/关闭二次验证、管理员锁定时全部会话失效;
- 风险登录进入二次验证,不重复创建半有效 Token。
3.3 账户资料与安全资料
当前 ProfileService 同时处理公开资料、账户资料和安全操作。目标流程应拆开:
- 公开资料:头像、昵称、简介,修改后立即可被业务模块读取;
- 账户资料:邮箱、手机号、登录名,修改前必须验证旧凭证或二次验证;
- 安全资料:密码、二次验证密钥、恢复码,只能通过安全操作接口修改,不能复用普通 Profile DTO;
- 每次敏感资料变更都撤销高风险会话并通知用户。
3.4 组织、职位与授权
组织、职位、角色和权限不能只作为后台 CRUD。管理员真正需要的是“把人放进组织并立即获得/回收工作权限”的闭环:
重构要求:
- 用户、组织、职位、角色分配支持批量操作和预览差异;
- 权限变更显示“谁、对什么资源、获得了什么权限、何时生效”;
- 离职/禁用/移出组织必须自动回收职位和组织授权;
- 权限缓存按用户和角色精确失效,不能等待固定 TTL;
- 所有授权决策能解释原因,便于后台排查 403。
3.5 Member 业务流程
Member 不应只是 Identity 用户表的附属 CRUD。建议把它定义为用户生命周期上的运营域:
- 身份用户激活后,幂等创建 MemberUser;
- 业务行为产生积分流水,而不是直接覆盖积分余额;
- 积分变化触发等级评估,等级变化记录原因和生效时间;
- 标签、分组和等级用于运营筛选、权益和内容推荐;
- 身份禁用不删除会员历史,会员状态变为不可用;
- 身份硬删除由事件处理器清理可删除数据,但保留合规要求的账务/审计记录。
当前 MemberUser、积分日志、等级、分组和标签已经具备雏形,下一步重点是补齐状态流、幂等和事件失败重试,而不是继续增加管理接口。
3.6 AccessKey 生命周期
AccessKey 已收敛为独立的凭证生命周期,当前流程为:
- 创建成功只在响应中返回一次 Secret,列表和详情仅返回末四位掩码;
- 数据库沿用原
access_key_secret列名保存sha256:版本化摘要,启动迁移会先提取历史 Secret 末四位,再把历史明文原位转换为摘要; - Host 鉴权先按 AccessKeyId 查找,再固定时间校验摘要,并统一检查启用、过期、撤销状态以及绑定用户仍为 Active;
- 轮换会原子替换摘要,旧 Secret 立即失效;创建、轮换、状态/过期变更、使用和撤销均进入 AccessKey 审计日志;
- 撤销记录撤销人、UTC 时间、原因并永久禁用凭证;旧 DELETE 路由仅作为兼容入口执行撤销,不再删除审计记录;
- 鉴权成功时条件更新最后使用时间,可阻止鉴权与轮换或撤销并发时继续放行旧 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 查询通过审计仓储完成,授权读写委托给 IPermissionManager;ProfileService 通过 IUserRepository 和 IdentityUserManager 访问用户数据。两者下一阶段的重点分别是统一授权语义,以及拆分公开资料、账户资料和安全操作,而不是再次迁移 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 周)
- 抽出
IAccountService、ICredentialService、IExternalLoginService、ISessionService; - 统一注册、验证、登录、锁定、验证码、刷新令牌和注销语义;
- 将首次 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 按能力域整理;
- 保持
ProfileService经IUserRepository/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 路由兼容”的顺序:
- 先冻结并记录现有 API 契约、权限和数据字段;
- 新边界直接替换旧 Application Service,避免保留重复服务;
- 数据库新增字段先允许为空,并提供回填脚本;
- 回填完成后增加唯一约束/非空约束;
- 仅在前端尚未切换时保留短期路由/DTO 兼容映射,切换完成后删除。
生产环境不依赖 FreeSql CodeFirst 自动完成破坏性变更;每个阶段提供显式 DDL、回滚脚本和校验 SQL。
8. 测试与验收矩阵
| 层级 | 必须覆盖 |
|---|---|
| Domain | 账户状态、锁定、凭证、角色作用域、会员关联 |
| Application | 普通用户/管理员权限、租户隔离、设备撤销、AccessKey 轮换/过期/撤销、幂等 |
| API | 登录、刷新、注销、改密、权限管理、资料脱敏、AccessKey Secret 一次性返回 |
| Integration | OpenIddict、Redis 缓存失效、邮件、数据库事务 |
| Architecture | 禁止裸 ORM、依赖方向、Controller 薄层、敏感 DTO |
| Performance | 登录 P95、授权 P95、分页列表 P95、并发刷新 Token |
9. 风险与停止条件
- 如果旧前端依赖未记录的字段或错误码,不直接删除旧契约,先添加兼容映射;
- 如果数据库表由多个宿主同步,迁移必须先确认所有宿主版本;
- 如果 OpenIddict 版本升级改变 Token 行为,必须单独灰度,不与目录重构混在同一发布;
- 如果权限缓存无法证明失效正确,宁可暂时降低缓存时间,也不要返回过期授权结果。
10. 第一批可执行任务
- 画出并确认注册、登录、绑定、改密、设备撤销、组织授权和会员升级状态图。
- 新增 Identity 架构测试项目和依赖扫描规则。
- 直接建立
IAccountService新边界并迁移用户生命周期,不增加旧服务 facade。 - 实现会话中心:设备列表、单设备退出、全设备退出和敏感操作撤销。
- 将组织成员变更与授权刷新绑定,增加权限差异预览和审计记录。
- 将 Member 积分、等级、标签变更改为幂等事件和可追踪流水。
- 为
PermissionService、ProfileService的现有仓储边界增加架构回归测试,并将EmployeeService、RoleService、TenantService、TokenHandler、SerilogService中的.Orm/ADO 调用迁入专用仓储、Query Service 或 Infrastructure Adapter。 - 建立登录、刷新、注销、权限变更、设备撤销和会员升级的集成测试。
- 为每个阶段补充 DDL、回滚脚本和前端兼容说明。
11. 相关源码
src/Services/Identity/FreeKit.Identitysrc/Services/Identity/FreeKit.Identity.Infrastructuresrc/Services/Identity/FreeKit.Membersrc/Services/Identity/FreeKit.Identity.ApiClientbasic/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 债务 |