一、LDAP vs 数据库:本质区别
一句话概括
LDAP 是"查人的电话簿",数据库是"存业务数据的仓库"。
LDAP 不是数据库的替代品,它是一种专门为"查人/查组织结构"优化的轻量级目录协议。
核心区别对比
| 维度 | LDAP | 关系型数据库(MySQL/PostgreSQL) |
|---|---|---|
| 设计目的 | 查人、查组织、查权限 | 存业务数据、做交易、写报表 |
| 数据模型 | 树形结构(DN 路径) | 表 + 行 + 列 |
| 读写比例 | 读多写少(99% 读 / 1% 写) | 读写均衡,甚至写多读少 |
| 查询方式 | 按 DN 路径精确查找(如 cn=zhangsan,ou=IT,dc=company,dc=com) |
SQL 任意条件查询(SELECT * WHERE age > 30) |
| 复杂查询 | ❌ 不支持 JOIN、不支持聚合 | ✅ 任意 JOIN、GROUP BY、子查询 |
| 事务支持 | ❌ 无 ACID 事务 | ✅ 完整 ACID |
| 并发写入 | 差(设计上就不鼓励频繁改) | 强(行级锁、MVCC) |
| 扩展性 | 天然支持分布式(目录树天然可分片) | 分布式复杂(需要分库分表) |
| Schema | 严格,改结构麻烦 | 灵活,ALTER TABLE 随时改 |
| 典型用途 | 账号认证、组织架构、权限管理 | 订单、支付、库存、日志 |
数据模型对比(最直观的差异)
数据库(表结构)
users 表:
+----+-------+--------+-------+
| id | name | dept | email |
+----+-------+--------+-------+
| 1 | 张三 | IT部 | zs@.. |
| 2 | 李四 | 财务部 | ls@.. |
| 3 | 王五 | IT部 | ww@.. |
+----+-------+--------+-------+
查询:SELECT * FROM users WHERE dept = 'IT部'
→ 简单、灵活、任意条件
LDAP(树形结构)
dc=company,dc=com
├── ou=People
│ ├── cn=张三,ou=IT
│ │ ├── uid: zhangsan
│ │ ├── mail: zs@company.com
│ │ └── memberOf: cn=IT-Admins,ou=Groups
│ ├── cn=李四,ou=Finance
│ │ ├── uid: lisi
│ │ └── mail: ls@company.com
│ └── cn=王五,ou=IT
│ ├── uid: wangwu
│ └── mail: ww@company.com
└── ou=Groups
├── cn=IT-Admins
│ └── member: cn=张三,ou=IT
└── cn=Finance-Users
└── member: cn=李四,ou=Finance
你看,LDAP 把人和组织的层级关系天然表达出来了------这是关系型数据库很难做到的。
为什么认证系统偏爱 LDAP 而不是数据库?
| 原因 | 说明 |
|---|---|
| 1. 专门为"查人"优化 | LDAP 的索引结构天然适合按 DN 路径查找(cn=zhangsan,ou=IT,dc=company,dc=com),O(1) 级别 |
| 2. 读性能极高 | 认证场景 99% 是读(你登录时查你的密码/权限),LDAP 的读性能远超 MySQL |
| 3. 层级结构天然匹配组织 | 公司有部门→子部门→人,LDAP 树形结构完美表达,数据库要 JOIN 半天 |
| 4. 标准协议 | LDAP 是 RFC 标准,所有语言/框架都有现成库(Java JNDI、Python ldap3、Go ldap) |
| 5. 分布式天然友好 | 多台 LDAP 服务器可以按 ou=IT / ou=Finance 分片,查询时自动路由 |
| 6. 密码策略内置 | LDAP Schema 自带 userPassword、accountExpires、pwdMaxAge 等字段,开箱即用 |
| 7. 不需要复杂查询 | 认证只需要"查这个人存不存在、密码对不对、属于哪个组",不需要 SQL 那种复杂条件 |
什么时候该用 LDAP,什么时候该用数据库?
| 场景 | 选 LDAP | 选数据库 |
|---|---|---|
| 用户登录认证 | ✅ | ❌(能做但重) |
| 组织架构查询(部门/层级) | ✅ | ❌(要递归 JOIN) |
| 权限/角色管理(RBAC) | ✅ | ❌(树形天然) |
| 员工通讯录(公司黄页) | ✅ | ❌ |
| 订单/支付/库存 | ❌ | ✅ |
| 日志/监控数据 | ❌ | ✅ |
| 复杂报表/分析 | ❌ | ✅ |
| 高频写入(如 IoT 传感器) | ❌ | ✅ |
典型产品对比
| 类型 | 产品 | 本质 |
|---|---|---|
| LDAP 服务器 | OpenLDAP、389 Directory Server、Microsoft AD(AD 底层也是 LDAP) | 专门存人/组织/权限 |
| 数据库 | MySQL、PostgreSQL、Oracle | 存业务数据 |
| 两者结合 | 企业通常 AD/LDAP 存账号 + MySQL 存业务 | 各司其职 |
Microsoft Active Directory 就是最典型的例子:它底层是 LDAP + Kerberos,但微软在上面加了一堆东西(GPO、DNS、DHCP),让它看起来像个"操作系统",本质上还是个 LDAP 目录。
一句话总结
LDAP 是"专门查人的轻量级电话簿",数据库是"万能的业务数据仓库"。认证、组织架构、权限管理用 LDAP(快、准、天然匹配);订单、支付、日志用数据库(灵活、强事务、支持复杂查询)。企业里通常两个都用------LDAP 管"你是谁",数据库管"你干了什么"。
二、如何使用 LDAP------从零到实战
一、LDAP 的核心概念(先搞懂再用)
LDAP 目录树:
dc=company,dc=com ← 树根(域名)
├── ou=People ← 部门/组织单元
│ ├── cn=张三,ou=IT
│ │ ├── uid: zhangsan
│ │ ├── mail: zs@company.com
│ │ └── memberOf: cn=IT-Admins,ou=Groups
│ └── cn=李四,ou=Finance
└── ou=Groups ← 组
└── cn=IT-Admins
└── member: cn=张三,ou=IT
| 术语 | 含义 | 举例 |
|---|---|---|
| DN | 唯一标识路径 | cn=张三,ou=IT,dc=company,dc=com |
| Base DN | 搜索起点 | dc=company,dc=com |
| RDN | DN 中最左边的部分 | cn=张三 |
| Attribute | 属性(字段) | uid、mail、memberOf |
| ObjectClass | 条目类型 | inetOrgPerson(人)、organizationalUnit(部门) |
二、安装 LDAP 服务器(OpenLDAP)
CentOS/RHEL
bash
yum install -y openldap openldap-clients openldap-servers
# 初始化配置
cp /usr/share/openldap-servers/DB_CONFIG.example /var/lib/ldap/DB_CONFIG
chown -R ldap:ldap /var/lib/ldap/DB_CONFIG
# 设置管理员密码
slappasswd -s MyPassword123
# 输出: {SSHA}xxxxxxxxxx
# 创建密码配置文件
cat > changepwd.ldif << EOF
dn: olcDatabase={0}config,cn=config
changetype: modify
add: olcRootPW
olcRootPW: {SSHA}刚才生成的哈希值
EOF
# 应用配置
ldapadd -Y EXTERNAL -H ldapi:/// -f changepwd.ldif
# 启动服务
systemctl enable slapd
systemctl start slapd
Ubuntu/Debian
bash
apt install -y slapd ldap-utils
# 安装时会弹窗让你设置管理员密码
dpkg-reconfigure slapd # 后续可重新配置
三、添加数据(LDIF 文件)
创建 users.ldif:
ldif
# 1. 创建 IT 部门
dn: ou=IT,dc=company,dc=com
objectClass: organizationalUnit
ou: IT
# 2. 创建 Finance 部门
dn: ou=Finance,dc=company,dc=com
objectClass: organizationalUnit
ou: Finance
# 3. 创建 IT-Admins 组
dn: cn=IT-Admins,ou=Groups,dc=company,dc=com
objectClass: groupOfNames
cn: IT-Admins
member: cn=张三,ou=IT,dc=company,dc=com
# 4. 添加用户张三
dn: cn=张三,ou=IT,dc=company,dc=com
objectClass: inetOrgPerson
cn: 张三
sn: 张
uid: zhangsan
mail: zs@company.com
userPassword: Password123
ou: IT
# 5. 添加用户李四
dn: cn=李四,ou=Finance,dc=company,dc=com
objectClass: inetOrgPerson
cn: 李四
sn: 李
uid: lisi
mail: ls@company.com
userPassword: Password456
ou: Finance
导入数据:
bash
ldapadd -x -D "cn=admin,dc=company,dc=com" -W -f users.ldif
# -x 简单认证
# -D 绑定管理员 DN
# -W 提示输入密码
四、查询数据(最常用操作)
命令行查询
bash
# 1. 查所有用户
ldapsearch -x -b "dc=company,dc=com" "(objectClass=inetOrgPerson)"
# 2. 查 IT 部门的人
ldapsearch -x -b "ou=IT,dc=company,dc=com" "(ou=IT)"
# 3. 查某个用户
ldapsearch -x -b "dc=company,dc=com" "(uid=zhangsan)"
# 4. 查用户所在的组
ldapsearch -x -b "dc=company,dc=com" "(&(objectClass=inetOrgPerson)(uid=zhangsan))" memberOf
# 5. 查所有部门
ldapsearch -x -b "dc=company,dc=com" "(objectClass=organizationalUnit)" ou
参数说明
| 参数 | 含义 |
|---|---|
-x |
简单认证(不用 SASL) |
-b |
Base DN(搜索起点) |
-D |
绑定 DN(管理员) |
-W |
提示输入密码 |
-H |
服务器地址,如 ldap://192.168.1.100:389 或 ldaps://192.168.1.100:636 |
(filter) |
搜索过滤器 |
五、Python 操作 LDAP
安装
bash
pip install ldap3
完整示例:连接 + 查询 + 认证
python
from ldap3 import Server, Connection, ALL, SUBTREE, NTLM
# 1. 连接服务器
server = Server('ldap://192.168.1.100:389', get_info=ALL)
conn = Connection(
server,
user='cn=admin,dc=company,dc=com',
password='MyPassword123',
auto_bind=True
)
# 2. 查询组织架构
conn.search(
search_base='dc=company,dc=com',
search_filter='(objectClass=organizationalUnit)',
attributes=['ou', 'distinguishedName']
)
for entry in conn.entries:
print(f"部门: {entry.ou.value}")
print(f" DN: {entry.distinguishedName}")
# 3. 查询所有用户
conn.search(
search_base='ou=IT,dc=company,dc=com',
search_filter='(objectClass=inetOrgPerson)',
attributes=['cn', 'uid', 'mail', 'department']
)
for entry in conn.entries:
print(f"用户: {entry.cn.value}, 工号: {entry.uid.value}, 邮箱: {entry.mail.value}")
# 4. 验证用户登录(认证)
def authenticate(username, password):
user_dn = f'uid={username},ou=IT,dc=company,dc=com'
user_conn = Connection(
server,
user=user_dn,
password=password,
auto_bind=True
)
if user_conn.bound:
# 查该用户的组
user_conn.search(
search_base='dc=company,dc=com',
search_filter=f'(member={user_dn})',
attributes=['cn']
)
groups = [e.cn.value for e in user_conn.entries]
user_conn.unbind()
return True, groups
user_conn.unbind()
return False, []
ok, groups = authenticate('zhangsan', 'Password123')
print(f"登录成功: {ok}, 所属组: {groups}")
# 5. 关闭连接
conn.unbind()
六、Java/Spring Boot 操作 LDAP
Maven 依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>
application.yml
yaml
spring:
ldap:
urls: ldap://192.168.1.100:389
base: dc=company,dc=com
username: cn=admin,dc=company,dc=com
password: MyPassword123
查询示例
java
@Repository
public class LdapUserRepository {
@Autowired
private LdapTemplate ldapTemplate;
// 查询所有用户
public List<User> findAll() {
return ldapTemplate.search(
"ou=IT,dc=company,dc=com",
"(objectClass=inetOrgPerson)",
new UserAttributesMapper()
);
}
// 验证用户登录
public boolean authenticate(String uid, String password) {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectClass", "inetOrgPerson"));
filter.and(new EqualsFilter("uid", uid));
boolean exists = ldapTemplate.search(
"dc=company,dc=com",
filter.toString(),
new ContextMapper<Boolean>() {
@Override
public Boolean mapFromContext(Object ctx) {
return ((DirContext) ctx) != null;
}
}
);
if (!exists) return false;
// 尝试用用户密码绑定
try {
LdapContext ctx = new InitialLdapContext(
env, null
);
ctx.close();
return true;
} catch (NamingException e) {
return false;
}
}
}
七、把 LDAP 接入认证系统(最常见场景)
场景:应用接 LDAP 做登录认证
用户输入用户名/密码
↓
应用服务器
↓
LDAP 服务器:uid=xxx,ou=IT,dc=company,dc=com ?密码对吗?
↓
返回:通过 + 组列表(IT-Admins, VPN-Users)
↓
应用根据组分配权限
配置参数(通用)
| 参数 | 值 | 说明 |
|---|---|---|
| LDAP URL | ldap://192.168.1.100:389 或 ldaps://...:636 |
生产必须用 ldaps |
| Base DN | dc=company,dc=com |
搜索起点 |
| Bind DN | cn=admin,dc=company,dc=com |
应用用来查用户的账号 |
| Bind Password | xxxx |
上面账号的密码 |
| User Filter | (uid={0}) |
{0} 会被用户名替换 |
| Group Filter | (member={0}) |
查用户所属组 |
| User ID Attribute | uid |
用户名对应的字段 |
| Group Name Attribute | cn |
组名字段 |
接入示例(Dropwizard 风格)
java
// Dropwizard 配置
ldap:
url: ldaps://ldap.company.com:636
baseDn: dc=company,dc=com
bindDn: cn=svc-app,ou=service,dc=company,dc=com
bindPassword: ${LDAP_BIND_PW}
userSearch: (uid={0})
groupSearch: (member={0})
userIdAttribute: uid
java
// 认证过滤器
@Provider
@Priority(Priorities.AUTHENTICATION)
public class LdapAuthFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext req) {
String auth = req.getHeaderString("Authorization");
String[] parts = auth.substring(6).split(":"); // Basic xxx
String username = parts[0];
String password = parts[1];
if (!ldapService.authenticate(username, password)) {
throw new WebApplicationException(401);
}
// 把用户和组注入请求上下文
List<String> groups = ldapService.getUserGroups(username);
req.setProperty("user", username);
req.setProperty("groups", groups);
}
}
八、常用命令速查表
| 操作 | 命令 |
|---|---|
| 测试连接 | ldapsearch -x -H ldap://host:389 -b "" -s base |
| 查所有用户 | ldapsearch -x -b "dc=company,dc=com" "(objectClass=inetOrgPerson)" cn uid mail |
| 查用户组 | ldapsearch -x -b "dc=company,dc=com" "(&(objectClass=inetOrgPerson)(uid=zhangsan))" memberOf |
| 查部门树 | ldapsearch -x -b "dc=company,dc=com" "(objectClass=organizationalUnit)" ou |
| 添加用户 | ldapadd -x -D "cn=admin,dc=company,dc=com" -W -f user.ldif |
| 删除用户 | ldapdelete -x -D "cn=admin,dc=company,dc=com" -W "uid=zhangsan,ou=IT,dc=company,dc=com" |
| 修改密码 | ldappasswd -x -D "cn=admin,dc=company,dc=com" -W -S "uid=zhangsan,ou=IT,dc=company,dc=com" |
| 导出数据 | slapcat -b "dc=company,dc=com" > backup.ldif |
| 导入数据 | slapadd -l backup.ldif |
九、安全建议
| 要求 | 做法 |
|---|---|
| 必须用 LDAPS | 生产环境用 ldaps://:636,不要用明文 389 |
| Bind DN 最小权限 | 应用只给只读账号,不要用 cn=admin |
| 密码加密存储 | LDAP 内部用 SSHA/SSHA256,不要明文 |
| 网络隔离 | LDAP 服务器放内网,不暴露公网 |
| 定期备份 | slapcat 定期导出 LDIF |
一句话总结
LDAP 使用三步走:①装服务器(OpenLDAP) → ②导入数据(LDIF 文件) → ③客户端连接查询(ldapsearch / Python ldap3 / Spring LDAP)。最常见的场景是把 LDAP 当"统一账号本",让所有应用都去查它验证用户、取组、分权。
三、是ldap服务器判断密码是否正确,还是应用从ldap查询密码,应用自己判断密码是否正确? 答案:LDAP 服务器判断密码是否正确
应用程序永远不应该自己取密码出来比对。这是 LDAP 认证的核心原则。
两种方式对比
❌ 错误做法:应用自己查密码比对
应用 → LDAP: 帮我查 zhangsan 的密码
LDAP → 应用: {SSHA}a1b2c3d4e5...(加密哈希)
应用 → 自己: 把用户输入的密码哈希,跟 {SSHA}a1b2c3d4e5 比对
为什么这是错的?
| 问题 | 说明 |
|---|---|
| 密码不是明文存的 | LDAP 存的是 SSHA 哈希,你拿到的也是哈希,没法"解密"出原文 |
| 哈希算法可能不同 | LDAP 用 SSHA,你应用可能用 MD5,算法不一致根本比不了 |
| 暴露密码哈希 | 密码哈希一旦泄露,离线暴力破解就能还原密码 |
| 违反安全原则 | 密码验证必须在 LDAP 服务器内部完成,应用不该碰密码 |
✅ 正确做法:让 LDAP 服务器判断
应用 → LDAP: 请验证 uid=zhangsan, 密码=Password123 是否正确
LDAP → 内部: 取出该用户的密码哈希 → 用同样算法哈希用户输入 → 比对
LDAP → 应用: 返回 true/false(只告诉你对不对,不告诉你密码是什么)
这叫"Bind 认证"------应用把用户名密码交给 LDAP,LDAP 内部完成验证,只返回成功/失败。
代码层面的区别
错误写法(自己查密码)
java
// ❌ 错误:应用自己取密码哈希出来比对
public boolean authenticate(String username, String password) {
// 1. 查用户的密码属性
DirContext ctx = ldapTemplate.getContext(
"uid=" + username + ",ou=IT,dc=company,dc=com"
);
String storedHash = ctx.getAttributes("uid=" + username)
.get("userPassword").get().toString();
// storedHash = {SSHA}a1b2c3d4...
// 2. 应用自己哈希用户输入的密码
String inputHash = hashPassword(password);
// 3. 自己比对 ← ❌ 完全错误
return storedHash.equals(inputHash);
}
正确写法(Bind 认证)
java
// ✅ 正确:把验证交给 LDAP
public boolean authenticate(String username, String password) {
try {
// 尝试用用户的 DN 绑定(bind)
String userDn = "uid=" + username + ",ou=IT,dc=company,dc=com";
LdapContext ctx = new InitialLdapContext(env, null);
ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDn);
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
// 尝试绑定 ← LDAP 内部完成密码验证
ctx.reconnect(null); // 真正触发认证
ctx.close();
return true; // 绑定成功 = 密码正确
} catch (AuthenticationException e) {
return false; // 绑定失败 = 密码错误
} catch (NamingException e) {
return false; // 用户不存在等
}
}
Spring LDAP 的正确用法
java
@Service
public class LdapAuthService {
@Autowired
private LdapContextSource contextSource;
public boolean authenticate(String username, String password) {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectClass", "inetOrgPerson"));
filter.and(new EqualsFilter("uid", username));
// 先查用户是否存在
boolean exists = ldapTemplate.search(
"ou=IT,dc=company,dc=com",
filter.toString(),
new AbstractContextMapper<Boolean>() {
@Override
protected Boolean doMapFromContext(DirContextOperations ctx) {
return ctx.getNameInNamespace() != null;
}
}
);
if (!exists) return false;
// ✅ 用 Bind 方式验证密码
String userDn = "uid=" + username + ",ou=IT,dc=company,dc=com";
try {
DirContext authContext = contextSource.getContext(userDn, password);
authContext.close();
return true;
} catch (AuthenticationException e) {
return false;
}
}
}
Python ldap3 的正确用法
python
from ldap3 import Server, Connection, NTLM
def authenticate(username, password):
user_dn = f'uid={username},ou=IT,dc=company,dc=com'
try:
# ✅ 用 Bind 方式验证
conn = Connection(
Server('ldap://192.168.1.100:389'),
user=user_dn,
password=password,
auto_bind=True # 真正触发认证
)
conn.unbind()
return True
except Exception as e:
return False
一张图看懂
❌ 错误流程:
应用 ──查密码──→ LDAP ──返回哈希──→ 应用自己比对
↑
密码哈希暴露给应用了!
✅ 正确流程:
应用 ──Bind(用户名+密码)──→ LDAP ──内部验证──→ 返回 true/false
↑
密码永远不出 LDAP
为什么必须这样做?
| 原因 | 说明 |
|---|---|
| 安全 | 密码哈希不出 LDAP 服务器,应用拿不到任何密码相关信息 |
| 正确 | LDAP 内部知道用什么哈希算法(SSHA/SSHA256),应用不一定知道 |
| 标准 | RFC 4513 规定密码验证必须通过 Bind 操作完成 |
| 扩展 | LDAP 可以配置密码策略(过期、复杂度),Bind 方式自动遵守;自己比对就绕过了这些策略 |
一句话
ldapService.authenticate(username, password)底层做的是 Bind 操作------把用户名密码交给 LDAP 服务器,LDAP 内部验证后只返回"对/错",应用永远碰不到密码原文和密码哈希。这是 LDAP 认证的唯一正确方式。