2. 用户与组织管理:用户表设计、组织架构树、用户-组织关联、LDAP集成思路
权限系统说到底,核心就两件事:谁能访问什么。而「谁」这个字,落地到数据库里,就是用户和组织架构。这一章我们就把这个基础打牢。
我见过不少团队,一上来就设计复杂的角色和权限模型,结果用户和组织的关系没理清,后面改得痛不欲生。嗯,咱们先把地基夯实。
2.1 用户表设计:别只想着存账号密码
用户表是权限系统的起点。很多人觉得用户表不就是 id、username、password 嘛?其实不然。在企业级场景下,用户表要承载的信息远不止这些。
我个人习惯把用户表分为三个区域:身份区、认证区、状态区。
| 区域 | 字段 | 说明 |
|---|---|---|
| 身份区 | user_id, username, real_name, email, phone | 唯一标识用户,不可变或极少变 |
| 认证区 | password_hash, salt, mfa_secret, last_login_ip | 密码存储必须加盐哈希,别存明文 |
| 状态区 | status, locked_until, failed_attempts, last_login_at | 控制账号是否可用、是否被锁定 |
另外,我建议在用户表里加一个 user_type 字段。为什么?因为企业里除了「自然人」用户,还可能有「机器人」账号、服务账号。不同类型的账号,认证方式和权限策略完全不同。
-- 用户表核心结构(简化版)
CREATE TABLE sys_user (
user_id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) NOT NULL UNIQUE,
password_hash VARCHAR(256) NOT NULL,
real_name VARCHAR(64),
email VARCHAR(128),
phone VARCHAR(20),
user_type TINYINT NOT NULL DEFAULT 0 COMMENT '0:自然人 1:机器人 2:服务账号',
status TINYINT NOT NULL DEFAULT 1 COMMENT '0:禁用 1:启用 2:锁定',
failed_attempts INT DEFAULT 0,
locked_until DATETIME,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
2.2 组织架构树:无限层级怎么存?
组织架构是企业权限绕不开的坎。部门、子公司、项目组……层级关系复杂。怎么存?我见过三种主流方案。
方案一:邻接表(Adjacency List)
最简单,每个节点存一个 parent_id。查询子树要递归,MySQL 8.0 以上可以用 CTE。
-- 邻接表结构
CREATE TABLE sys_org (
org_id BIGINT PRIMARY KEY,
org_name VARCHAR(128),
parent_id BIGINT DEFAULT NULL,
sort_order INT DEFAULT 0
);
-- 递归查询所有子节点(MySQL 8.0+)
WITH RECURSIVE org_tree AS (
SELECT * FROM sys_org WHERE org_id = 1
UNION ALL
SELECT o.* FROM sys_org o JOIN org_tree t ON o.parent_id = t.org_id
)
SELECT * FROM org_tree;
方案二:物化路径(Materialized Path)
每个节点存一个路径字符串,比如 /1/2/5/。查询子树用 LIKE,写起来简单,但移动节点时要更新所有子节点的路径。
LIKE '/1/2/%' 就能拿到整个子树,多痛快。
方案三:闭包表(Closure Table)
用一张单独的表存所有祖先-后代关系。查询快,但数据量大,插入和删除要维护多行记录。
| 方案 | 查询子树 | 移动节点 | 存储空间 |
|---|---|---|---|
| 邻接表 | 需要递归,慢 | 改 parent_id 即可 | 小 |
| 物化路径 | LIKE 查询,快 | 要更新所有子路径 | 中等 |
| 闭包表 | JOIN 查询,极快 | 要维护多行 | 大 |
我的建议是:中小规模用物化路径,大规模用闭包表。邻接表?除非你确定层级不超过三层,否则别碰。
2.3 用户-组织关联:多对多还是多对一?
一个用户只能属于一个部门吗?现实情况是:一个人可能同时在多个项目组,或者兼任两个部门的职务。所以,用户和组织的关系,绝大多数场景是多对多。
关联表的设计很简单,但有一个坑:主岗和兼岗。
-- 用户-组织关联表
CREATE TABLE sys_user_org (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
org_id BIGINT NOT NULL,
is_primary TINYINT DEFAULT 0 COMMENT '1:主岗 0:兼岗',
job_title VARCHAR(64) COMMENT '岗位名称',
entered_at DATE COMMENT '入职/调入日期',
UNIQUE KEY uk_user_org (user_id, org_id)
);
为什么要区分主岗?因为权限计算时,主岗往往决定了用户的默认权限范围。比如一个用户主岗在「研发部」,兼岗在「架构委员会」,那他的默认菜单权限应该按研发部来,而不是按委员会来。
2.4 LDAP 集成思路:别重复造轮子
中大型企业一般都有 LDAP(比如 Active Directory、OpenLDAP)。如果你的权限系统要对接 LDAP,记住一个原则:LDAP 负责认证,你的系统负责授权。
说白了,LDAP 就是告诉你「这个人是不是我们公司的员工」,至于他能干什么,还是你的权限系统说了算。
集成 LDAP 的核心步骤,我总结为三步:
- 配置连接:LDAP 服务器地址、端口、Base DN、管理员账号。
- 认证流程:用户输入密码,系统用 LDAP 的 bind 操作验证。验证通过后,从 LDAP 拉取用户属性(姓名、邮箱、部门等)。
- 同步机制:定时或实时同步 LDAP 中的用户和组织到本地数据库。
// LDAP 认证核心逻辑(伪代码)
public LdapUser authenticate(String username, String password) {
// 1. 连接 LDAP
LdapContext ctx = new InitialLdapContext(env, null);
// 2. 用用户账号 bind
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, "uid=" + username + ",ou=people,dc=example,dc=com");
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
ctx.reconnect(null); // 验证失败会抛异常
// 3. 拉取用户属性
Attributes attrs = ctx.getAttributes("uid=" + username + ",ou=people,dc=example,dc=com");
LdapUser user = new LdapUser();
user.setUsername(attrs.get("uid").get());
user.setEmail(attrs.get("mail").get());
user.setDepartment(attrs.get("department").get());
return user;
}
关于同步策略,我建议用增量同步 + 定时全量对账。平时通过 webhook 或消息队列处理变更,每天凌晨跑一次全量对账,把 LDAP 中已离职的用户自动禁用。
嗯,用户和组织这块,说白了就是搞清楚「谁是谁」和「谁归谁管」。基础打好了,后面做角色和权限才会顺手。下面这张图可以帮你理清整体关系。
这张图把用户、组织、LDAP 和关联表的关系串起来了。下一层就是角色,但那是后面的事。先把今天的内容消化掉,尤其是用户-组织关联表的设计,我建议你动手建个表试试,跑几个查询感受一下。