|
|
|
|
# 认证与第三方登录(crm-auth)
|
|
|
|
|
|
|
|
|
|
围绕三方扫码登录、本地用户定位与身份绑定的领域语言。
|
|
|
|
|
|
|
|
|
|
## Language
|
|
|
|
|
|
|
|
|
|
**孤儿身份**:
|
|
|
|
|
三方身份绑定(identity)仍存在,但其指向的本地用户已被物理删除的状态。持有孤儿身份的人再次登录时,按手机号匹配或首登注册重新定位用户,并把原绑定重定向到新用户,避免下次登录再次命中孤儿。
|
|
|
|
|
_Avoid_: 脏数据、失效绑定
|
|
|
|
|
|
|
|
|
|
**主部门**:
|
|
|
|
|
一个用户唯一的正式归属部门,代表其组织身份与汇报线,用户信息展示、组织架构归位均以主部门为准。每个用户至多一个主部门。
|
|
|
|
|
_Avoid_: 归属部门、默认部门
|
|
|
|
|
|
|
|
|
|
**兼职部门**:
|
|
|
|
|
用户除主部门之外额外挂靠的部门,代表其参与该部门业务但组织身份不在其中。一个用户可有多个兼职部门,也可以没有。
|
|
|
|
|
_Avoid_: 副部门、附属部门
|
|
|
|
|
|
|
|
|
|
**只读挂靠**:
|
|
|
|
|
兼职部门当前的权限语义:用户能读到兼职部门范围内的数据,但其自身产出的业务数据恒归属主部门,不进兼职部门。因此兼职部门的管理者看不到该成员在本部门产出的数据。
|
|
|
|
|
_Avoid_: 双向兼职、完全平等
|
|
|
|
|
|
|
|
|
|
**部门集合**:
|
|
|
|
|
一个用户的主部门与全部兼职部门的并集,数据范围计算以该集合为输入,集合内各部门地位平等。
|
|
|
|
|
_Avoid_: 权限部门、可见部门
|
|
|
|
|
|
|
|
|
|
**名称回显**:
|
|
|
|
|
把已保存的部门 ID / 用户 ID 换成中文名称在展示层显示。批量入口是 `ISysDeptService#getDeptNames(deptIds)` → `id → deptName` 与 `IAuthUserService#getUserNames(userIds)` → `id → username`,供 crm-lead / crm-opportunity / crm-rule 等调用方回填 DTO;「填哪些字段」留在各调用方,认证侧只负责「ID → 名称」这一段(与 crm-dict 字典名回显同型)。
|
|
|
|
|
_Avoid_: 名称翻译、label 回填
|
|
|
|
|
|
|
|
|
|
**一律不可见**:
|
|
|
|
|
数据范围档位要求按部门过滤,但该用户的部门集合为空时的判定结果:按部门归属的数据一行都不可见。无归属不等于全放行,此时查询注入永假条件而非放弃过滤。它是权限判定的结论,不是查询恰好没有命中数据。
|
|
|
|
|
_Avoid_: 空结果、查无数据、不过滤
|
|
|
|
|
|
|
|
|
|
**在职状态**:
|
|
|
|
|
用户的雇佣状态:在职 / 离职。来源于 OA 系统,CRM 不产生、只消费;离职用户仍保留在用户列表中(名下可能仍挂有业务数据),而非删除。与“账号是否可登录”是两回事——离职账号应禁止登录,被禁用的在职账号仍是在职。
|
|
|
|
|
_Avoid_: 员工状态、在职/离职员、启用状态
|
|
|
|
|
|
|
|
|
|
**用户**:
|
|
|
|
|
本系统中来自 OA 的人员统一称“用户”,不使用“员工/用工”。用户基本信息(姓名/部门/职务/手机号/在职状态)唯一来源为 OA,CRM 不产生、只消费。
|
|
|
|
|
_Avoid_: 员工、用工、人员
|
|
|
|
|
|
|
|
|
|
**待分配用户**:
|
|
|
|
|
在职且未持有任何角色的用户。该指标提醒管理员“有新人尚未配权限”;离职用户不计入,无论其是否持有角色。
|
|
|
|
|
_Avoid_: 无角色用户、未授权用户
|
|
|
|
|
|
|
|
|
|
**免登**:
|
|
|
|
|
用户在浏览器侧已登录钉钉后访问 CRM 时,前端重定向到钉钉 OAuth 授权页,钉钉检测到已登录态后自动重定向回 CRM 并携带 authCode,前端将 authCode POST 到现有扫码登录端点完成登录。免登与扫码登录在后端走同一套 OAuth2 userAccessToken 换取逻辑,区别仅在前端获取 authCode 的方式(重定向 vs 扫码),后端无感知。
|
|
|
|
|
_Avoid_: 自动登录、SSO、静默登录
|
|
|
|
|
|
|
|
|
|
**端类型**:
|
|
|
|
|
单端在线唯一性判定的维度,取值 pc / mobile(枚举可扩展,未来可加 miniapp 等)。作为登录入参 clientType 传入(大小写不敏感,不传默认 pc,老前端兼容);只存 Redis 侧(会话 JSON 与反向索引 key 段),JWT payload 不带。检测与顶号同端:PC 登录只查/只踢 PC 会话,不碰移动端。
|
|
|
|
|
_Avoid_: 设备类型、终端、设备指纹(明确不做物理设备指纹)
|
|
|
|
|
|
|
|
|
|
**单端在线**:
|
|
|
|
|
核心不变量:任意时刻每个 (userId, clientType) 至多一个活会话。一次登录态 = 一个有效 token:{jti} Redis 键;反向索引 session:{userId}:{clientType} → jti 只是线索,任何判定都回查 token 键确认活着才算在线(脏索引无害,命中顺手删)。索引与 token 键 TTL 对齐并跟随滑动续期。
|
|
|
|
|
_Avoid_: 单点登录、互踢、同账号互斥
|
|
|
|
|
|
|
|
|
|
**顶号**:
|
|
|
|
|
同一用户在新设备登录同一端类型时,把该端已有在线会话踢下线并接管登录态的动作。交互为两接口:主登录检测到设备在线时不签正式 token,返回 61013 + 短命 preLoginToken;用户确认后凭 preLoginToken 调 confirm 接口完成踢旧签新。已知取舍:confirm 的「踢旧」与「签新覆写索引」之间存在毫秒级窗口,若同端主登录恰好插入,可产生一个不被索引追踪的孤儿活会话(最多 1 个,靠自然过期自愈)——同端并发竞态的专项并发控制已明确排除在范围外。
|
|
|
|
|
_Avoid_: 强制下线(管理员按用户/端批量踢会话是另一回事,已明确排除)、挤下线
|
|
|
|
|
|
|
|
|
|
**预登录票**:
|
|
|
|
|
短命一次性凭证 preLoginToken(UUID,TTL 默认 2 分钟可配),仅代表「已认证、待确认顶号」。存于 prelogin:{token} → {userId, clientType},confirm 接口 GETDEL 原子消费——拿到即烧,防并发双签/重放;过期/已消费/伪造统一报 61014,用户重走主登录。
|
|
|
|
|
_Avoid_: 临时 token、授权码(与钉钉 authCode 是两回事)
|
|
|
|
|
|
|
|
|
|
**被顶下线**:
|
|
|
|
|
旧设备会话被顶号踢除后的识别状态:踢除时埋标记 kicked:{旧jti}="1"(TTL 对齐原会话有效期),旧设备下次请求 token 校验失败时查该标记命中,返回专门的 61015 错误码(HTTP 仍 401),前端据此弹「已在其他设备登录」而非「登录过期」。标记留到 TTL 自然过期,不读后即焚。
|
|
|
|
|
_Avoid_: 踢下线、强制退出、账号冲突
|
|
|
|
|
|
|
|
|
|
**组织准入**:
|
|
|
|
|
登录流程中对新用户的组织归属校验:当用户在本地 DB 中查不到时,用企业级 access token 调钉钉组织通讯录 API(按 unionId 查询),确认该用户属于本企业后才允许自动注册。不属于组织的用户拒绝登录并返回「不属于该组织」错误。已在 DB 中的用户不受此校验影响(登录时不重复校验)。
|
|
|
|
|
_Avoid_: 组织校验、企业校验、准入检查
|
|
|
|
|
|
|
|
|
|
**企业级 token**:
|
|
|
|
|
用 clientId + clientSecret 向钉钉换取的组织级 access token,用于调用组织通讯录等需要企业身份的 API。与登录流程中的用户级 token(userAccessToken)不同:企业级 token 代表应用/组织身份,用户级 token 代表个人身份。缓存于 Redis,TTL 略低于钉钉颁发的 7200 秒有效期。由 `CorpAccessTokenProvider`(`service/client`)统一提供:`get()` 读缓存/未命中换取回写,`forceRefresh()` 弃旧取新(调用方收到 40014/42001 失效时调用);登录组织校验与组织同步共用这一模块,缓存 key 不再被调用方直接触碰。
|
|
|
|
|
_Avoid_: corpToken、应用 token、组织 token
|
feat(auth): role management — ancestor completion, CRUD, permission assignment, cascade delete (ADR-0012)
Implements ADR-0005 (backend ancestor completion) and ADR-0012 (role management
architecture) across 5 tracer-bullet tickets:
- ISysMenuService.getAncestorIds: batch query full table, in-memory parentId
traversal, returns complete ancestor set (ADR-0005)
- SysRole.builtin field + DataInitializer idempotent refactor + 5 button
permission seeds (crm:role:list/detail/save/delete/assign) bound to ADMIN
- RoleController (/api/roles/*): page, saveOrUpdate, detail, assign-resources,
delete — pure hasAuthority, POST + action suffix, form params
- SysRoleServiceImpl.saveRole: roleCode uniqueness, builtin protection,
dataScope 1-4 range, create can't set builtin=true
- assignResources: calls getAncestorIds for ancestor completion, then
full replacement (delete + insert) of sys_role_menu
- getRoleDetail: returns RoleDetailVO with basic info + resourceIds
- deleteRoleCascade: cleans sys_role_menu + sys_user_role, builtin rejection,
three-table transaction
- SystemController: all role endpoints removed, unused deps cleaned
- Dead code removed: assignMenus, getMenuIdsByRoleId (superseded by
assignResources, getRoleDetail)
- H2 test schemas updated with builtin column
- 150 tests pass (16 new in SysRoleServiceImplTest, 7 in SysMenuServiceImplTest)
1 month ago
|
|
|
|
|
|
|
|
**权限资源树**:
|
|
|
|
|
RBAC 的统一可授权资源模型:菜单分组(catalog)、菜单页面(menu)、按钮/权限点(button)三类节点在同一张 sys_menu 表中扁平存储,通过 parentId 组成树。catalog 是收纳容器不承载页面,menu 是可导航的页面,button 是挂在其下的叶子权限点。三层固定层级:根→catalog/menu→menu→button,分组不嵌套、按钮必挂页面下。
|
|
|
|
|
_Avoid_: 菜单表、资源表、权限树
|
|
|
|
|
|
|
|
|
|
**权限码并集**:
|
|
|
|
|
用户登录后,其所有角色授权的按钮节点(type=button 且 status=enabled)的 perms 字符串收集而成的集合,与 roleCode 一起注入 Spring Security authority,使 @PreAuthorize(hasAuthority('crm:xxx:yyy')) 生效。停用的权限点不进并集(全局断路),无需逐角色取消授权。
|
|
|
|
|
_Avoid_: 权限集合、按钮权限列表
|
|
|
|
|
|
|
|
|
|
**权限点停用**:
|
|
|
|
|
button 节点的 status=disabled 状态:即使已被角色授予,权限解析引擎也不将其 perms 纳入权限码并集——相当于全局断路。重新启用后原授权自动恢复,无需管理员重新勾选。停用/启用只改 status 字段,不涉及角色授权关系变动。
|
|
|
|
|
_Avoid_: 权限删除、授权撤销
|
|
|
|
|
|
|
|
|
|
**角色授权集合**:
|
|
|
|
|
一个角色被授予的全部资源节点 ID,存储于 sys_role_menu 表。保存时由后端沿 parentId 向上遍历补全祖先 catalog/menu 节点(ADR-0005),因此存储态始终是一棵完整子树——用户勾选了某 button,其所属的 menu 和 catalog 一定也在集合中。角色详情接口返回此完整集合,前端直接用于渲染勾选状态,无需二次补全。API 层称 resourceIds,存储层为 sys_role_menu.menu_id。
|
|
|
|
|
_Avoid_: 菜单ID列表、menuIds(API 层术语)、勾选项
|
|
|
|
|
|
|
|
|
|
**祖先补全**:
|
|
|
|
|
角色权限保存时的后端操作:对前端传入的每个勾选节点 ID,沿 parentId 递归向上收集祖先 catalog/menu ID,合并入集合后再入库。保证菜单可见性链路完整——若只存叶子 button 而不补全其祖先 menu/catalog,权限解析引擎建菜单树时祖先不在授权集合中,用户虽有按钮权限却看不到所在页面。补全发生在存储时而非读取时,读取返回的就是已补全的完整集合。
|
|
|
|
|
_Avoid_: 前端补全、读取时补全
|
|
|
|
|
|
|
|
|
|
**内置角色**:
|
|
|
|
|
SysRole 表中 builtin=true 的角色,代表系统运行所必需、不可删除、不可改 roleCode 的角色。ADMIN 是典型内置角色——若被删除,持有它的用户瞬间失去全部管理权限,系统不可恢复。builtin 字段由数据初始化器设置,创建角色接口不允许设 builtin=true。其他字段(roleName、dataScope、sort、remark)可正常编辑。
|
|
|
|
|
_Avoid_: 系统角色、固定角色、超级管理员
|
|
|
|
|
|
|
|
|
|
**数据权限模块**:
|
|
|
|
|
数据权限按业务模块分别配置时的「业务模块」概念(线索/商机/客户/项目,可扩展),存在于 `sys_data_scope_module` 注册表中,用 `code` 标识(稳定标识,不引用 `id`)。一个角色对每个模块各有一档数据范围档位,多角色对每个模块各自取最宽(ADR-0008 的规则下沉到每模块)。模块列表数据驱动(管理员可 CRUD),受控实体通过 `@DataScope(module = "...")` 注解声明所属模块,启动时与注册表交叉校验,未注册的 module code 拒绝启动(ADR-0006)。
|
|
|
|
|
_Avoid_: 业务域、功能模块、权限域
|