LDAP是什么

一、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 自带 userPasswordaccountExpirespwdMaxAge 等字段,开箱即用
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 属性(字段) uidmailmemberOf
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:389ldaps://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:389ldaps://...: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 认证的唯一正确方式。

相关推荐
一念一花一世界5 个月前
sourcefare零基础学习 - 集成LDAP,使用LDAP用户登录sourcefare
ldap·sourcefare·代码扫描工具
tornadoami6 个月前
51CTO_开源的密码自助平台Self Service Password
docker·开源·ad·安全可控·ssp·ldap·ltb
openHiTLS密码开源社区7 个月前
【密码学实战】openHiTLS passwd命令行:专业密码哈希生成工具
linux·密码学·哈希算法·ldap·密码策略·随机盐值
yangmf20408 个月前
LDAP 认证系列(四):Gateway LDAP 认证
大数据·elasticsearch·搜索引擎·gateway·ldap
Johny_Zhao10 个月前
Docker 一键安装部署 JumpServer 堡垒机
linux·网络安全·信息安全·云计算·shell·jumpserver·ldap·yum源·系统运维
斯普信专业组1 年前
Ldap高效数据同步- Delta-Syncrepl复制模式配置实战手册(上)
ldap·delta-syncrepl
斯普信专业组1 年前
LDAP高效数据同步:Syncrepl复制模式实战指南
ldap·syncrepl
开着拖拉机回家1 年前
【Ambari】使用 Knox 进行 LDAP 身份认证
大数据·hadoop·gateway·ambari·ldap·knox
余生H2 年前
ToB项目身份认证AD集成(完):利用ldap.js实现与windows AD对接实现用户搜索、认证、密码修改等功能 - 以及针对中文转义问题的补丁方法介绍
javascript·windows·typescript·node.js·身份认证·ldap·windowsad