☰
用户中心到底管什么?账号、组织成员、角色和会话的设计与验收
2026/10/26 1:58:31 网站建设 项目流程

企业系统里经常先建一张user表,后来又在上面陆续加部门、公司、角色、岗位、停用状态和“当前租户”。起初能用,一旦同一个人加入两家公司,就会出现奇怪的现象:切换公司后菜单还是上家公司的;员工转岗了,旧浏览器仍能操作;离职时为了“删账号”,历史审批记录也找不到人了。问题通常不在登录框,而在“账号是谁”和“此刻代表哪个组织做什么”被保存成了同一个状态。

这篇先画清用户中心的边界,再用一个虚构案例把登录、选择组织、形成会话、读取单据和停用成员连起来。账号u07同时加入甲公司orgA和乙公司orgB:在甲公司是管理员,在乙公司是普通查看者。文章给出最小表结构、可直接运行的 Java 11 教学示例、输出与异常测试,以及上线前应该查的 SQL。示例不对应任何现成产品的内部实现。

这里所说的“用户中心”负责稳定账号身份、组织成员关系、账号/成员状态与会话身份的建立;业务单据仍由业务模块管理,组织树可以由组织模块维护,角色对应的业务动作由权限模块定义。模块可以部署在同一服务中,但职责不能靠一张大表混在一起。

目录

  1. 一个账号加入两家公司会暴露什么问题
  2. 账号、成员、组织、角色和会话分别是什么
  3. 从登录到请求授权怎样走
  4. 最小数据模型和约束
  5. 用可运行代码验证身份边界
  6. 转岗、停用和历史记录如何处理
  7. SQL 核对与上线验收
  8. 适用边界与参考资料

一、一个账号加入两家公司会暴露什么问题

先固定输入,避免泛泛讨论“多租户用户”。u07在甲公司有管理员成员身份,可以读取甲公司的单据A101,即便单据创建者是u88;在乙公司只有查看者身份,只能读取自己创建的B101,不能读同公司别人创建的B102。甲、乙是两个组织边界,本文的权限规则只是教学用例,并非通用的管理员规则。

当前会话访问目标期望结果原因
u07@orgA管理员甲公司A101允许当前成员有甲公司内的管理范围
u07@orgB查看者乙公司自有B101允许属于当前组织且在本人范围内
u07@orgB查看者乙公司他人B102拒绝同组织不等于可读所有单据
u07@orgB查看者甲公司A101拒绝账号加入甲公司也不能替乙会话越界
乙公司成员停用后的旧会话乙公司B101拒绝旧浏览器不能延长已撤销的成员资格

如果系统把角色直接写在账号上,u07的“管理员”就容易泄漏到乙公司;如果把org_id只作为前端请求参数,用户可改参数猜其他公司的记录。用户中心要解决的是每次请求的有效组织身份从哪里来,而非替所有业务表统一加一个user_id。

图1:账号相同,成员、角色与可读单据不同;乙公司会话不能借甲公司管理员身份读取甲单据。

二、账号、成员、组织、角色和会话分别是什么

这五个概念回答不同的问题:

对象回答的问题建议保留的关键字段不应混入的内容
账号谁完成登录?稳定账号 ID、认证状态某公司的唯一角色
组织当前代表哪家公司/单位?组织 ID、状态某个人的登录凭据
组织成员账号是否加入该组织?账号 ID、组织 ID、成员状态、版本全局不变的账号身份
角色授予该成员在本组织可执行哪些动作?成员 ID、角色 ID、有效状态套餐购买与业务单据
会话身份这次请求当前代表谁、在哪个组织?账号 ID、当前组织 ID、成员版本永不失效的角色快照

“组织成员”是连接账号与组织的边。一个账号可以有多条成员记录;一个组织可以有多名成员。职务、部门与角色也不要天然画等号:某人调入新部门后可能继续担任同一角色,也可能立刻撤销某些数据范围;这是两项独立变化。业务表若保留创建人,应保存稳定账号或成员标识,显示名称可随人事信息变化,但历史操作人不能因为成员停用而消失。

会话并不必然是一张数据库表。它可以由服务器端会话或受控令牌承载,但客户端不能自行宣称“我现在是orgA管理员”。Spring Security 的认证模型将当前请求的Authentication放在SecurityContext中;它回答认证主体是谁。从已认证账号到当前组织成员、再到业务资源授权,是应用仍需实现的判断。Spring Security 认证架构

图2:成员连接账号与组织;角色授予附着在成员上,会话只持有当前有效身份的引用。

三、从登录到请求授权怎样走

登录成功只确定账号u07,尚未确定它要代表甲还是乙。系统先查询此账号的有效成员关系:只有一个可用组织时,可按产品规则自动进入;有多个时,让用户选择。选择orgB时,服务端根据已认证账号u07查orgB成员是否有效,再建立或更新当前会话。不能只接收前端传来的orgB和角色名。

后续每次访问按顺序处理:

  1. 认证当前账号,确认会话本身有效。
  2. 读取会话里受信的当前组织 ID,确认组织可用、成员仍有效。
  3. 检查成员的授权版本或重新加载其当前角色,避免转岗后继续使用旧权限。
  4. 将业务资源 ID 与当前组织一起查找,再判断动作权限和数据范围。
  5. 失败时对外返回适当拒绝,不暴露另一个组织的单据是否存在;对内记录原因码。

这不是说每个请求都必须全量查五张表。可用短期缓存、版本号和失效通知优化,但缓存的键与有效性必须包含当前组织和成员版本。如果登录时把“甲公司管理员”永久写进一个长期令牌,乙公司切换、转岗或停用都可能留下旧能力。OWASP 的授权指南强调默认拒绝并对每次请求验证权限;菜单隐藏不能代替服务器端检查。OWASP 授权检查清单

图3:登录仅得到账号;选择组织后验证成员,再在业务请求中核对状态、动作与资源归属。

四、最小数据模型和约束

以下是 MySQL 8.x 的教学模型,只保留解释案例需要的对象。生产系统还会有凭据、邀请、部门、角色定义、审计等表;它们不是本文为了凑“完整系统”而硬加进来的。这里有两个关键约束:同一账号在同一组织最多一条成员关系;业务资源必须明确归属组织。角色授予用成员 ID,而不是只用账号 ID。

CREATETABLEuc_account(account_idVARCHAR(32)PRIMARYKEY,statusVARCHAR(16)NOTNULL);CREATETABLEuc_org(org_idVARCHAR(32)PRIMARYKEY,statusVARCHAR(16)NOTNULL);CREATETABLEuc_member(member_idBIGINTPRIMARYKEY,account_idVARCHAR(32)NOTNULL,org_idVARCHAR(32)NOTNULL,statusVARCHAR(16)NOTNULL,auth_versionBIGINTNOTNULLDEFAULT1,UNIQUEKEYuk_account_org(account_id,org_id),UNIQUEKEYuk_member_org(member_id,org_id),FOREIGNKEY(account_id)REFERENCESuc_account(account_id),FOREIGNKEY(org_id)REFERENCESuc_org(org_id));CREATETABLEuc_member_role(member_idBIGINTNOTNULL,role_codeVARCHAR(40)NOTNULL,PRIMARYKEY(member_id,role_code),FOREIGNKEY(member_id)REFERENCESuc_member(member_id));CREATETABLEbiz_document(doc_idVARCHAR(32)PRIMARYKEY,org_idVARCHAR(32)NOTNULL,owner_account_idVARCHAR(32)NOTNULL,FOREIGNKEY(org_id)REFERENCESuc_org(org_id),FOREIGNKEY(owner_account_id)REFERENCESuc_account(account_id));INSERTINTOuc_account(account_id,status)VALUES('u07','ACTIVE'),('u88','ACTIVE');INSERTINTOuc_org(org_id,status)VALUES('orgA','ACTIVE'),('orgB','ACTIVE');INSERTINTOuc_member(member_id,account_id,org_id,status,auth_version)VALUES(101,'u07','orgA','ACTIVE',1),(102,'u07','orgB','ACTIVE',1),(103,'u88','orgA','ACTIVE',1),(104,'u88','orgB','ACTIVE',1);INSERTINTOuc_member_role(member_id,role_code)VALUES(101,'ADMIN'),(102,'VIEWER');INSERTINTObiz_document(doc_id,org_id,owner_account_id)VALUES('A101','orgA','u88'),('B101','orgB','u07'),('B102','orgB','u88');

auth_version是让旧会话失效的一种可解释办法:改变成员状态或角色授予时,在同一事务中递增版本;请求若持有旧版本就必须重新建立授权上下文。实际系统也可以选择服务器端会话撤销、短期令牌加版本查询等机制,目标都是确保“权限已经撤销”可被后续请求观察到。版本本身不能代替组织状态检查,也不能自动撤销已经发出的长期下载链接。

对业务表的查询至少要带当前组织条件:

SELECTdoc_id,org_id,owner_account_idFROMbiz_documentWHEREdoc_id=:docIdANDorg_id=:currentOrgId;

随后还要判断本人、部门或管理员数据范围。WHERE org_id = ?防止跨组织误读,却不会自动解决乙公司查看者读取B102的同组织越权。附件、导出和后台任务也必须采用相同的身份与资源归属边界,不能只保护列表页。

五、用可运行代码验证身份边界

Java 11 教学示例用内存对象代替数据库和框架,只测试最关键的合同:从已认证账号选择组织时验证成员;读单据时校验成员状态、授权版本、资源归属和本例的数据范围。下段是核心方法,完整文件可直接运行java tools/Stage8UserCenterExample.java。

SessionselectOrganization(StringauthenticatedAccountId,StringrequestedOrgId){Membermember=members.get(key(authenticatedAccountId,requestedOrgId));if(member==null||!member.active){thrownewSecurityException("MEMBER_NOT_ACTIVE");}returnnewSession(authenticatedAccountId,requestedOrgId,member.version);}booleancanRead(Sessionsession,Documentdocument){Membermember=members.get(key(session.accountId,session.orgId));if(member==null||!member.active||member.version!=session.memberVersion){returnfalse;}if(!session.orgId.equals(document.orgId)){returnfalse;}return"ADMIN".equals(member.role)||document.ownerId.equals(session.accountId);}

这里的requestedOrgId是用户选择的候选目标,不是被信任的身份;只有服务端确认authenticatedAccountId在该组织有有效成员记录,才能写入会话。canRead也不能因为账号在甲公司是管理员,就在乙公司套用这个角色。教学代码用布尔返回,真实接口还需把“未认证”“当前成员无效”“资源不可见”“动作无权”等原因映射到统一、不过度泄露的信息。

实际运行得到:

A admin reads A101=true B viewer reads own B101=true B viewer denied B102=false B session denied A101=false disabled member denied=false unjoined organization denied=true

这些断言覆盖正常路径和三个重要反例:同组织他人记录、跨组织记录、成员停用后的旧会话。代码并不声称“内存 Map 就能做用户中心”;它把最容易被混淆的授权边界缩成了可运行的判定,再由实际系统接入认证、数据库与缓存。

图4:甲管理员、乙查看者、跨组织访问和停用旧会话使用同一组虚构数据核对。

六、转岗、停用和历史记录如何处理

转岗不是直接修改账号上的role。先明确变化属于哪层:部门变化、组织成员状态变化、角色授予变化,还是业务数据交接。若u07在乙公司转岗导致查看范围变化,应在成员授权变更事务里更新授予与版本,让已缓存的乙公司会话失效;甲公司成员关系不应跟着变化。多个组织共享一个账号,正是要防止“改一处影响所有公司”。

离职同样不等于物理删除账号。若只退出乙公司,就停用u07@orgB;甲公司仍可保留有效成员。若账号整体被停用,则所有组织会话都不能再发起新操作。历史单据和审批日志仍保留稳定操作者 ID,查询时可显示“已离职成员”或当时的快照名称。是否允许历史数据读取、如何留存个人信息,应服从业务与合规规则,不能用“删除用户就一起级联删除单据”来代替产品政策。

会话回收也应看见时间边界。正在处理中的请求、已经发出的异步任务、缓存的菜单、旧下载链接可能继续产生动作;不同通道要逐一验收。最少要做到:角色变更后的下一次受保护请求使用新版本,成员停用后的旧会话不能再读取或提交;后台任务执行时重新核对必要的身份或把授权时的可审计范围固定在任务里,不能随便复用浏览器旧上下文。OWASP 的会话管理指南可作为失效和重新认证策略的参考。OWASP 会话管理

七、SQL 核对与上线验收

上线时先用只读查询核对数据事实。以下 SQL 对应本例的三个问题:u07是否真的有甲、乙两条成员记录;角色是否授予了正确的成员;历史单据是否仍能按创建账号追溯。

SELECTm.account_id,m.org_id,m.status,m.auth_version,GROUP_CONCAT(r.role_codeORDERBYr.role_code)ASrolesFROMuc_member mLEFTJOINuc_member_role rONr.member_id=m.member_idWHEREm.account_id='u07'GROUPBYm.member_id,m.account_id,m.org_id,m.status,m.auth_versionORDERBYm.org_id;-- 预期:orgA / ADMIN,orgB / VIEWER;各自独立的成员和版本。SELECTd.doc_id,d.org_id,d.owner_account_idFROMbiz_document dWHEREd.doc_idIN('A101','B101','B102')ORDERBYd.doc_id;-- 预期:A101 属于 orgA;B101、B102 属于 orgB。SELECTd.doc_id,a.account_id,a.statusASaccount_statusFROMbiz_document dJOINuc_account aONa.account_id=d.owner_account_idWHEREd.doc_id='B101';-- 停用成员后仍应查到 B101 的历史创建账号;不能级联删业务单据。

这几条查询只是数据核对,不能证明接口没有越权。接口验收应以同一组输入执行登录、切换甲乙、查看列表/详情、修改、附件下载与导出;再测试乙公司角色变更、成员停用后刷新旧浏览器。预期不仅是返回拒绝,还要确认拒绝请求没有生成导出任务、文件或审计上看似成功的副作用。若使用缓存,测试跨节点、会话过期和版本更新传播;若有移动端,测试后台恢复后是否仍使用旧组织。

图5:正常访问、同组织越权、跨组织访问、转岗和停用旧会话构成最小验收集。

八、适用边界与参考资料

用户中心不应无限膨胀成“所有带用户 ID 的表都归它”。它提供可识别、可切换、可撤销的当前组织身份;权限模块解释动作和数据范围,业务模块负责自己的记录,审计模块保存历史证据。小系统完全可以先用一个服务实现这些职责,但数据语义和请求检查不能省略。

本文的 Java 11 示例、MySQL 8.x 表与虚构数据说明了一个可验证的最小边界,不涵盖单点登录、外部身份映射、密码和 MFA、复杂组织继承或真实会话分布式存储。后续再分别讨论多组织成员、身份切换、转岗回收、离职留痕与端到端验收;每篇只深入一个问题。

参考资料:

  1. Spring Security:Servlet 认证架构
  2. OWASP:授权检查清单
  3. OWASP:会话管理检查清单
  4. OpenID Connect Core:主体标识与声明

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询