搭建ldap并进行java连接测试
搭建了ldap, 测试了命令行操作ldap, 测试了java连接ldap.
docker 搭建 openldap
https://hub.docker.com/r/bitnami/openldap/
注意只用于开发测试, 正式环境需要正确配置各种安全参数, 包括账号密码
version: '2'
services:
openldap:
image: docker.io/bitnami/openldap:2.6
ports:
- '1389:1389'
- '1636:1636'
environment:
- LDAP_ADMIN_USERNAME=admin
- LDAP_ADMIN_PASSWORD=adminpassword
- LDAP_USERS=user01,user02
- LDAP_PASSWORDS=password1,password2
volumes:
- /data/openldap:/bitnami/openldap'
注意通过这种方法搭建的ldap,baseDn 为dc=example,dc=org, 主账号(principle)为: cn=admin,dc=example,dc=org, 密码(credential)为adminpassword.
没有用户组和用户的定义, 需要进行设置才能进行用户组操作.
官方安装文档里默认提供的一些配置默认参数
https://hub.docker.com/r/bitnami/openldap/
The Bitnami Docker OpenLDAP can be easily setup with the following environment variables:
- LDAP_PORT_NUMBER: The port OpenLDAP is listening for requests. Priviledged port is supported (e.g. 1389). Default: 1389 (non privileged port).
- LDAP_ROOT: LDAP baseDN (or suffix) of the LDAP tree. Default: dc=example,dc=org
- LDAP_ADMIN_USERNAME: LDAP database admin user. Default: admin
- LDAP_ADMIN_PASSWORD: LDAP database admin password. Default: adminpassword
- LDAP_ADMIN_PASSWORD_FILE: Path to a file that contains the LDAP database admin user password. This will override the value specified in LDAP_ADMIN_PASSWORD. No defaults.
- LDAP_USERS: Comma separated list of LDAP users to create in the default LDAP tree. Default: user01,user02
- LDAP_PASSWORDS: Comma separated list of passwords to use for LDAP users. Default: bitnami1,bitnami2
- LDAP_USER_DC: DC for the users' organizational unit. Default: users
- LDAP_GROUP: Group used to group created users. Default: readers
- ...
java 连接 ldap 进行测试
不使用 spring ldap的传统方法, 有很多非常冗余的操作.
下面代码并没有完整关闭链接, 详细需要参考: https://docs.spring.io/spring-ldap/docs/current/reference/
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.util.Hashtable;
import java.util.List;
public class OpenLdapTest {
@Test
@Disabled
public void simpleLdapConnTest() {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://ldap.example.com:1389/dc=example,dc=org");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "cn=admin,dc=example,dc=org"); // replace with user DN
env.put(Context.SECURITY_CREDENTIALS, "adminpassword");
// 防止测试连接超时
env.put("com.sun.jndi.ldap.read.timeout", "5000");
env.put("com.sun.jndi.ldap.connect.timeout", "5000");
DirContext ctx;
try {
ctx = new InitialDirContext(env);
} catch (NamingException e) {
System.out.println("exception" + e);
return;
}
try {
SearchControls controls = new SearchControls();
controls.setSearchScope( SearchControls.SUBTREE_SCOPE);
NamingEnumeration<SearchResult> rv = ctx.search( "", "(objectclass=person)", controls);
int i = 0;
while (rv.hasMore()) {
System.out.println("rv:" + rv.next());
i += 1;
}
System.out.println("count: " + i);
// no need to process the results
} catch (NameNotFoundException e) {
// The base context was not found.
// Just clean up and exit.
} catch (NamingException e) {
// exception handling
} finally {
// close ctx or do Java 7 try-with-resources http://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
}
}
}
测试结果
rv:cn=user01,ou=users: null:null:{gidnumber=gidNumber: 1000, uidnumber=uidNumber: 1000, uid=uid: user01, userpassword=userPassword: [B@51399530, objectclass=objectClass: inetOrgPerson, posixAccount, shadowAccount, homedirectory=homeDirectory: /home/user01, sn=sn: Bar1, cn=cn: User1, user01}
rv:cn=user02,ou=users: null:null:{gidnumber=gidNumber: 1001, uidnumber=uidNumber: 1001, uid=uid: user02, userpassword=userPassword: [B@6b2ea799, objectclass=objectClass: inetOrgPerson, posixAccount, shadowAccount, homedirectory=homeDirectory: /home/user02, sn=sn: Bar2, cn=cn: User2, user02}
count: 2
error测试记录:
- 错误填写连接地址
javax.naming.NamingException: LDAP connection has been closed
- 随便填写端口, 连接超时
javax.naming.NamingException: LDAP response read timed out, timeout used: 7000 ms.
- 错误填写密码, 报错
javax.naming.AuthenticationException: [LDAP: error code 49 - Invalid Credentials]
参考文档
Testing ldap connection
https://stackoverflow.com/questions/14146833/testing-ldap-connection
Testing LDAP Connections With Java
https://www.baeldung.com/java-test-ldap-connections
设置超时时间
When testing connections, it's common that we pass in an incorrect URL or that the server is simply unresponsive. Since the default client behavior blocks indefinitely until a response is received, we'll define timeout parameters. The wait time is defined in milliseconds:
env.put("com.sun.jndi.ldap.read.timeout", "5000");
env.put("com.sun.jndi.ldap.connect.timeout", "5000");
LdapTemplate: LDAP Programming in Java Made Simple
传统方法使用java操作ldap, 需要注意及时关闭各种context. 建议使用spring ldap API.
It is important to always close the naming enumeration and the context.
spring ldap
Spring LDAP Reference
https://docs.spring.io/spring-ldap/docs/current/reference/
spring LDAP 文档里反而提供了java 操作ldap的传统方法用于参考
package com.example.repository;
public class TraditionalPersonRepoImpl implements PersonRepo {
public List<String> getAllPersonNames() {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389/dc=example,dc=com");
DirContext ctx;
try {
ctx = new InitialDirContext(env);
} catch (NamingException e) {
throw new RuntimeException(e);
}
List<String> list = new LinkedList<String>();
NamingEnumeration results = null;
try {
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
results = ctx.search("", "(objectclass=person)", controls);
while (results.hasMore()) {
SearchResult searchResult = (SearchResult) results.next();
Attributes attributes = searchResult.getAttributes();
Attribute attr = attributes.get("cn");
String cn = attr.get().toString();
list.add(cn);
}
} catch (NameNotFoundException e) {
// The base context was not found.
// Just clean up and exit.
} catch (NamingException e) {
throw new RuntimeException(e);
} finally {
if (results != null) {
try {
results.close();
} catch (Exception e) {
// Never mind this.
}
}
if (ctx != null) {
try {
ctx.close();
} catch (Exception e) {
// Never mind this.
}
}
}
return list;
}
}
使用spring ldap版本的操作写法
By using the Spring LDAP AttributesMapper and LdapTemplate classes, we get the exact same functionality with the following code:
package com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}
public List<String> getAllPersonNames() {
return ldapTemplate.search(
query().where("objectclass").is("person"),
new AttributesMapper<String>() {
public String mapFromAttributes(Attributes attrs)
throws NamingException {
return attrs.get("cn").get().toString();
}
});
}
}
he LdapTemplate search method makes sure a DirContext instance is created, performs the search, maps the attributes to a string by using the given AttributesMapper, collects the strings in an internal list, and, finally, returns the list. It also makes sure that the NamingEnumeration and DirContext are properly closed and takes care of any exceptions that might happen.
java 开发 ldap
java 创建ldap group dn定义
@Test
public void CreateDN() {
DirContext ctx = getLdapContex();
try {
// 创建根节点
Attributes attrs = new BasicAttributes();
attrs.put("objectClass", "top");
attrs.put("objectClass", "organizationalUnit");
attrs.put("description", "Example Domain");
ctx.createSubcontext("ou=Group,dc=example,dc=org", attrs);
// 关闭连接
ctx.close();
System.out.println("Base DN created successfully.");
} catch (NamingException e) {
e.printStackTrace();
}
}
java 测试
查询不存在的用户不会报错
/**
* 查询不存在的用户是否会报错, 不会
*/
@Test
@Disabled
public void searchUser() throws NamingException{
String searchFilter = String.format("(cn=%s)", "user011");
String searchBase = LdapUtils.getOU("dc=example, dc=org", "People");
String[] returnedAtts = {"uid", "cn", "sn"};;;
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchCtls.setReturningAttributes(returnedAtts);
DirContext ctx = getLdapContex();
NamingEnumeration<SearchResult> rv = ctx.search(searchBase, searchFilter, searchCtls);
int i = 0;
while (rv.hasMore()) {
SearchResult a = rv.next();
System.out.println("rv" + a);
System.out.println(a.getAttributes());
System.out.println(a.getName());
System.out.println(a.getObject());
System.out.println(a.getNameInNamespace());
i += 1;
}
System.out.println("count: " + i);
}
返回count: 0
删除不存在的用户不会报错
/**
* 删除不存在的用户是否会报错
*
* Destroys the named context and removes it from the namespace. Any attributes associated with the name are also removed. Intermediate contexts are not destroyed.
* This method is idempotent. It succeeds even if the terminal atomic name is not bound in the target context, but throws NameNotFoundException if any of the intermediate contexts do not exist.
*/
@Test
public void removeUser() throws NamingException {
String dn = "cn=user03,ou=People,dc=example,dc=org";
DirContext ctx = getLdapContex();
ctx.destroySubcontext(dn);
}
开发过程 debug 记录
ldap真是一个古老的中间件, 没有足够的背景知识和使用经验, 只能到处瞎摸索.
debug: [LDAP: error code 32 - No Such Object]
context.search查询出现报错javax.naming.NameNotFoundException: [LDAP: error code 32 - No Such Object];
java.lang.RuntimeException: javax.naming.NameNotFoundException: [LDAP: error code 32 - No Such Object]; remaining name 'ou=Group,dc=example,dc=org'
Caused by: javax.naming.NameNotFoundException: [LDAP: error code 32 - No Such Object]; remaining name 'ou=Group,dc=example,dc=org'
at com.sun.jndi.ldap.LdapCtx.mapErrorCode(LdapCtx.java:3286)
at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:3207)
at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2998)
at com.sun.jndi.ldap.LdapCtx.searchAux(LdapCtx.java:1874)
at com.sun.jndi.ldap.LdapCtx.c_search(LdapCtx.java:1797)
at com.sun.jndi.toolkit.ctx.ComponentDirContext.p_search(ComponentDirContext.java:392)
at com.sun.jndi.toolkit.ctx.PartialCompositeDirContext.search
ldap server端日志可以看到具体的查询信息和报错, 报错信息也为err=32
openldap_1 | 64a51e52.29a12c4a 0x7f27db99b700 conn=1001 fd=12 ACCEPT from IP=119.147.10.187:4405 (IP=0.0.0.0:1389)
openldap_1 | 64a51e52.29daf544 0x7f27db99b700 conn=1001 op=0 BIND dn="cn=admin,dc=example,dc=org" method=128
openldap_1 | 64a51e52.29dc121b 0x7f27db99b700 conn=1001 op=0 BIND dn="cn=admin,dc=example,dc=org" mech=SIMPLE bind_ssf=0 ssf=0
openldap_1 | 64a51e52.29dd473a 0x7f27db99b700 conn=1001 op=0 RESULT tag=97 err=0 qtime=0.000015 etime=0.000194 text=
openldap_1 | 64a51e52.2aa33e0a 0x7f27db99b700 conn=1001 op=1 SRCH base="ou=Group,dc=example,dc=org" scope=2 deref=3 filter="(cn=groupaaa)"
openldap_1 | 64a51e52.2aa40ff5 0x7f27db99b700 conn=1001 op=1 SRCH attr=cn
openldap_1 | 64a51e52.2aa5d8b5 0x7f27db99b700 conn=1001 op=1 SEARCH RESULT tag=101 err=32 qtime=0.000016 etime=0.000214 nentries=0 text=
openldap_1 | 64a51e52.2bc9b29d 0x7f27db99b700 conn=1001 op=2 UNBIND
openldap_1 | 64a51e52.2bcb7d56 0x7f27db99b700 conn=1001 fd=12 closed
原因其实就是baseDN找不到, 很奇怪竟然不是返回空, 而是直接报错. 如果baseDN存在, 那么查询不到条目, 只会返回空列表, 而不是直接报错.
在这里是ou=Group,dc=example,dc=org不存在, 需要提前定义好group.
baseDN指定错误也会报错 NameNotFoundException
如果连接串里指定里baseDN, 然后搜索的时候也继续指定baseDN, 测试发现也会报错 No Such Object. 估计被识别成dc=example,dc=org/dc=example,dc=org层级了.
javax.naming.NameNotFoundException: [LDAP: error code 32 - No Such Object]; remaining name 'dc=example,dc=org'
baseDN的参数不存在也会报错NameNotFoundException
下面的代码里, ou=Peoplexx不存在则会报错NameNotFoundException, 设置为String dn = "cn=user03,ou=People,dc=example,dc=org";, 就算cu=user03不存在也不会报错.
@Test
public void removeUser() throws NamingException {
String dn = "cn=user03,ou=Peoplexx,dc=example,dc=org";
DirContext ctx = getLdapContex();
ctx.destroySubcontext(dn);
}
javax.naming.NameNotFoundException: [LDAP: error code 32 - No Such Object]
; remaining name 'cn=user03,ou=Peoplexx,dc=example,dc=org'
at com.sun.jndi.ldap.LdapCtx.mapErrorCode(LdapCtx.java:3286)
连接串指定baseDN的注意点
在ldap里, baseDN真是个重要的概念, 有顶点的baseDN, 也有普通的baseDN, 操作都在baseDN指定的空间下执行.
在ldap的连接串里可以指明baseDN, 注意这时候在ctx.search里不需要指定baseDN.
env.put(Context.PROVIDER_URL, "ldap://ldap.domain.cool:1389/dc=example,dc=org");
NamingEnumeration<SearchResult> rv = ctx.search( "", "(objectclass=personxx)", controls);
若是连接串没有指明baseDN, 需要在search的时候, 指明DN, 不然也会出现dn不存在的报错.
env.put(Context.PROVIDER_URL, "ldap://ldap.domain.cool:1389");
NamingEnumeration<SearchResult> rv = ctx.search( "dc=example,dc=org", "(objectclass=personxx)", controls);
若是连接串里指明了baseDN, search的时候也指明baseDN, 也会出现dn不存在的报错.
javax.naming.NameNotFoundException: [LDAP: error code 32 - No Such Object]; remaining name 'dc=example,dc=org'
debug: no global superior knowledge
javax.naming.OperationNotSupportedException: [LDAP: error code 53 - no global superior knowledge]; remaining name 'dc=example,dc=com'
原因是指定的baseDN (dc=example,dc=com) 不存在, 系统里的是 dc=example,dc=org
// 此处为错误代码
DirContext ctx = getLdapContex();
try {
// 创建根节点
Attributes attrs = new BasicAttributes();
attrs.put("objectClass", "top");
attrs.put("objectClass", "domain");
attrs.put("ou", "Group");
attrs.put("description", "Example Domain");
ctx.createSubcontext("dc=example,dc=com", attrs);
// 关闭连接
ctx.close();
System.out.println("Base DN created successfully.");
} catch (NamingException e) {
e.printStackTrace();
}
debug: [LDAP: error code 65 - attribute 'ou' not allowed]; remaining name 'dc=example,dc=org'
javax.naming.directory.SchemaViolationException: [LDAP: error code 65 - attribute 'ou' not allowed]; remaining name 'dc=example,dc=org'
错误代码, 定义 domain objectClass 不允许有ou属性. 初学者谁能想到这些竟然有系统定义的约束.
// 此处为错误代码
// 创建根节点
Attributes attrs = new BasicAttributes();
attrs.put("objectClass", "top");
attrs.put("objectClass", "domain");
attrs.put("ou", "Group");
attrs.put("description", "Example Domain");
ctx.createSubcontext("dc=example,dc=org", attrs);
// 关闭连接
ctx.close();
debug: object class 'domain' requires attribute 'dc'
[LDAP: error code 65 - object class 'domain' requires attribute 'dc']; remaining name 'ou=Group,dc=example,dc=org'
错误代码, domain objectClass需要有dc属性, 同样也是没想到.
// 此处为错误代码
DirContext ctx = getLdapContex();
try {
// 创建根节点
Attributes attrs = new BasicAttributes();
attrs.put("objectClass", "top");
attrs.put("objectClass", "domain");
attrs.put("description", "Example Domain");
ctx.createSubcontext("ou=Group,dc=example,dc=org", attrs);
// 关闭连接
ctx.close();
System.out.println("Base DN created successfully.");
} catch (NamingException e) {
e.printStackTrace();
}
正确版本创建 ldap group
在chatgpt的建议下, 终于成功创建了group用户组, 然后才能进行用户组的查询而不会报错.
@Test
public void CreateDN() {
DirContext ctx = getLdapContex();
try {
// 创建根节点
Attributes attrs = new BasicAttributes();
attrs.put("objectClass", "top");
attrs.put("objectClass", "organizationalUnit");
attrs.put("description", "Example Domain");
ctx.createSubcontext("ou=Group,dc=example,dc=org", attrs);
// 关闭连接
ctx.close();
System.out.println("Base DN created successfully.");
} catch (NamingException e) {
e.printStackTrace();
}
}
每次启动docker都挂掉
用了osixia的docker容器, 每次启动docker都挂掉, Killing all processes..
ubuntu@VM-0-4-ubuntu:/data/develop/openldap$ docker-compose up
Creating network "openldap_default" with the default driver
Creating openldap_openldap_1 ...
Creating openldap_openldap_1 ... done
Attaching to openldap_openldap_1
openldap_1 | *** INFO | 2023-07-05 10:17:51 | CONTAINER_LOG_LEVEL = 3 (info)
openldap_1 | *** INFO | 2023-07-05 10:17:51 | Killing all processes...
openldap_openldap_1 exited with code 0
原因竟然是docker-compose文件里设置了user用户导致, 删除后恢复正常
version: '2'
services:
openldap:
image: osixia/openldap:1.5.0
ports:
- '1389:389'
- '1636:636'
user: "1000"
environment:
- LDAP_BASE_DN=dc=example,dc=org
- LDAP_ADMIN_PASSWORD=adminpassword
- LDAP_RFC2307BIS_SCHEMA=true
openldap_1 | *** INFO | 2023-07-05 10:21:57 | Running /container/run/startup/:ssl-tools...
openldap_1 | *** INFO | 2023-07-05 10:21:57 | Running /container/run/startup/slapd...
openldap_1 | *** INFO | 2023-07-05 10:21:57 | openldap user and group adjustments
openldap_1 | *** INFO | 2023-07-05 10:21:57 | get current openldap uid/gid info inside container
openldap_1 | *** INFO | 2023-07-05 10:21:57 | -------------------------------------
openldap_1 | *** INFO | 2023-07-05 10:21:57 | openldap GID/UID
openldap_1 | *** INFO | 2023-07-05 10:21:57 | -------------------------------------
openldap_1 | *** INFO | 2023-07-05 10:21:57 | User uid: 911
openldap_1 | *** INFO | 2023-07-05 10:21:57 | User gid: 911
openldap_1 | *** INFO | 2023-07-05 10:21:57 | uid/gid changed: false
openldap_1 | *** INFO | 2023-07-05 10:21:57 | -------------------------------------
openldap_1 | *** INFO | 2023-07-05 10:21:57 | updating file uid/gid ownership
openldap_1 | *** INFO | 2023-07-05 10:21:57 | Database and config directory are empty...
openldap_1 | *** INFO | 2023-07-05 10:21:57 | Init new ldap server...
docker-compose 启动 ldap与界面
- ldap-user-manager
wheelybird/ldap-user-manager
https://hub.docker.com/r/wheelybird/ldap-user-manager
bitnami/openldap
https://hub.docker.com/r/bitnami/openldap
参考
https://github.com/osixia/docker-openldap/blob/master/example/docker-compose.yml
https://github.com/osixia/docker-openldap
https://blog.ruanbekker.com/blog/2022/03/20/run-openldap-with-a-ui-on-docker/
bitnami/openldap 版本
注意这里只是开发测试用途, 如果用于正式环境需要详细阅读各种配置, 修改各种密码.
- 用户组的定义是可以配置的, 一般为group或者groups:
LDAP_GROUP_OU=groups
version: '2'
services:
openldap:
image: docker.io/bitnami/openldap:2.6
ports:
- '1389:1389'
- '1636:1636'
user: "1000"
environment:
- LDAP_ROOT=dc=example,dc=org
- LDAP_ADMIN_USERNAME=admin
- LDAP_ADMIN_PASSWORD=fakepassword
- LDAP_USERS=user01,user02
- LDAP_PASSWORDS=fakepassword1,fakepassword2
- LDAP_CONFIG_ADMIN_ENABLED=false
- LDAP_USER_DC=People
volumes:
- /data/openldap:/bitnami/openldap
networks:
- openldap-network
openldap-ui:
image: wheelybird/ldap-user-manager:v1.10
container_name: openldap-ui
environment:
- SERVER_HOSTNAME=ldap.domain.com
- LDAP_URI=ldap://openldap:1389
- LDAP_BASE_DN=dc=example,dc=org
- LDAP_REQUIRE_STARTTLS=FALSE
- LDAP_ADMINS_GROUP=admins
- LDAP_GROUP_OU=Group
- LDAP_ADMIN_BIND_DN=cn=admin,dc=example,dc=org
- LDAP_ADMIN_BIND_PWD=fakepassword
- LDAP_IGNORE_CERT_ERRORS=true
- NO_HTTPS=TRUE
- PASSWORD_HASH=SSHA
depends_on:
- openldap
ports:
- 13891:80
networks:
- openldap-network
networks:
openldap-network:
driver: bridge
内置ldap进程
/opt/bitnami/openldap/sbin/slapd -h ldap://:1389/ ldapi:/// -F /opt/bitnami/openldap/etc/slapd.d -d 256
安装完的信息检查
⛔ The group OU (ou=Group,dc=example,dc=org) doesn't exist.
☑ The user OU (ou=people,dc=example,dc=org) is present.
osixia/openldap版本
注意这里只是开发测试用途, 如果用于正式环境需要详细阅读各种配置, 修改各种密码.
version: '2'
services:
openldap:
image: docker.io/osixia/openldap:latest
ports:
- '1389:389'
- '1636:636'
environment:
- LDAP_ORGANISATION=ldapusermanager
- LDAP_DOMAIN=example.org
- LDAP_BASE_DN=dc=example,dc=org
- LDAP_ADMIN_PASSWORD=adminpassword
- LDAP_RFC2307BIS_SCHEMA=true
- LDAP_REMOVE_CONFIG_AFTER_SETUP=true
- LDAP_TLS_VERIFY_CLIENT=never
volumes:
- ./var_lib_ldap:/var/lib/ldap
- ./etc_ldap_slapd.d:/etc/ldap/slapd.d
networks:
- openldap
openldap-ui:
image: wheelybird/ldap-user-manager:latest
container_name: openldap-ui
environment:
- LDAP_URI=ldap://openldap:389
- LDAP_BASE_DN=dc=example,dc=org
- LDAP_REQUIRE_STARTTLS=FALSE
- LDAP_ADMINS_GROUP=admins
- LDAP_GROUP_OU=group
- LDAP_ADMIN_BIND_DN=cn=admin,dc=example,dc=org
- LDAP_ADMIN_BIND_PWD=adminpassword
- LDAP_IGNORE_CERT_ERRORS=true
- NO_HTTPS=TRUE
- PASSWORD_HASH=SSHA
depends_on:
- openldap
ports:
- 13891:80
networks:
- openldap
networks:
openldap:
参考: https://blog.ruanbekker.com/blog/2022/03/20/run-openldap-with-a-ui-on-docker/
docker-compose exec openldap bash
# verify user
ldapsearch -x -h openldap -D "uid=ruan,ou=people,dc=example,dc=org" -b "ou=people,dc=example,dc=org" -w "$PASSWORD" -s base 'uid=ruan'
# whoami
ldapwhoami -vvv -h ldap://openldap:389 -p 389 -D 'uid=ruan,ou=people,dc=example,dc=org' -x -w "$PASSWORD"
docker 安装 ldap web 界面
优秀的web界面, 可以协助进行ldap初始化.
安装的docker镜像是: wheelybird/ldap-user-manager:latest
安装完直接访问, 点击login弹出登录界面, 这时候输入管理员身份其实无法登录, 摸索了好久.
后面才知道需要先用管理员账号访问setup页面, 进行初始化后再创建新的管理员, 才能在这个页面进行登录.
访问 http://ip:port/setup 进入管理界面
这交互web优秀的地方在于会有各种建议, 同时会自动协助初始化, 比如初始化 user用户组, group用户组等信息, 不然直接查询用户组会报错.
一路下一步, 初始化后点击创建web的管理员
然后再用管理员身份去登录, 这时候就能进行用户组和用户的创建了
命令行操作访问 ldap
查询所有信息
ldapsearch -H ldap://ldap.domain.com:1389 -x -D "cn=admin,dc=example,dc=org" -w "fakepassword" -b "dc=example,dc=org" "(objectclass=*)"
这是一个使用 ldapsearch 命令进行 LDAP 搜索的示例命令。该命令的含义如下:
ldapsearch:执行 LDAP 搜索的命令。
- -x:使用简单身份验证方式进行身份验证。
- -b "dc=example,dc=com":指定搜索的基础 DN(Distinguished Name),这里搜索的是 "dc=example,dc=com" 下的所有条目。
- -D "cn=admin,dc=example,dc=com":指定用于身份验证的账号的 DN。
- -w "password":指定用于身份验证的账号的密码。
- "(objectclass=*)":指定搜索的过滤器,这里搜索所有的条目。 该命令的作用是在 "dc=example,dc=com" 下搜索所有的条目,并使用 "cn=admin,dc=example,dc=com" 账号进行身份验证。如果身份验证成功,OpenLDAP 会返回所有符合搜索过滤器的条目。
新安装上面的docker镜像后, 执行全量模糊查询得到的所有用户和用户组信息
- 自带用户组结构
- 用户的dn信息为
cn=user01,ou=People,dc=example,dc=org, 归属组织为ou=People,dc=example,dc=org - 所有用户默认在reader用户组下, dn信息为
cn=readers,ou=People,dc=example,dc=org - 用户支持存储密码, 保存在属性
userPassword:: ZmFrZXBhc3N3b3JkMQ==
ldapsearch -H ldap://ldap.domain.com:1389 -x -D "cn=admin,dc=example,dc=org" -w "fakepassword" -b "dc=example,dc=org" "(objectclass=*)"
# extended LDIF
#
# LDAPv3
# base <dc=example,dc=org> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
# example.org
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
dc: example
o: example
# People, example.org
dn: ou=People,dc=example,dc=org
objectClass: organizationalUnit
ou: users
ou: People
# user01, People, example.org
dn: cn=user01,ou=People,dc=example,dc=org
cn: User1
cn: user01
sn: Bar1
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
userPassword:: ZmFrZXBhc3N3b3JkMQ==
uid: user01
uidNumber: 1000
gidNumber: 1000
homeDirectory: /home/user01
# user02, People, example.org
dn: cn=user02,ou=People,dc=example,dc=org
cn: User2
cn: user02
sn: Bar2
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
userPassword:: ZmFrZXBhc3N3b3JkMg==
uid: user02
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/user02
# readers, People, example.org
dn: cn=readers,ou=People,dc=example,dc=org
cn: readers
objectClass: groupOfNames
member: cn=user01,ou=People,dc=example,dc=org
member: cn=user02,ou=People,dc=example,dc=org
# search result
search: 2
result: 0 Success
# numResponses: 6
# numEntries: 5
根据客户端修正后
- 新增加了
ou=group的用户组结构:ou=Group,dc=example,dc=org - 所有web创建的用户保存在
cn=everybody,ou=Group,dc=example,dc=org用户组中
ldapsearch -H ldap://ldap.domain.com:1389 -x -D "cn=admin,dc=example,dc=org" -w "fakepassword" -b "dc=example,dc=org" "(objectClass=*)"
# extended LDIF
#
# LDAPv3
# base <dc=example,dc=org> with scope subtree
# filter: (objectClass=*)
# requesting: ALL
#
# example.org
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
dc: example
o: example
# People, example.org
dn: ou=People,dc=example,dc=org
objectClass: organizationalUnit
ou: users
ou: People
# user01, People, example.org
dn: cn=user01,ou=People,dc=example,dc=org
cn: User1
cn: user01
sn: Bar1
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
userPassword:: ZmFrZXBhc3N3b3JkMQ==
uid: user01
uidNumber: 1000
gidNumber: 1000
homeDirectory: /home/user01
# user02, People, example.org
dn: cn=user02,ou=People,dc=example,dc=org
cn: User2
cn: user02
sn: Bar2
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
userPassword:: ZmFrZXBhc3N3b3JkMg==
uid: user02
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/user02
# readers, People, example.org
dn: cn=readers,ou=People,dc=example,dc=org
cn: readers
objectClass: groupOfNames
member: cn=user01,ou=People,dc=example,dc=org
member: cn=user02,ou=People,dc=example,dc=org
# Group, example.org
dn: ou=Group,dc=example,dc=org
objectClass: organizationalUnit
ou: Group
# lastGID, example.org
dn: cn=lastGID,dc=example,dc=org
objectClass: device
objectClass: top
description: Records the last GID used to create a Posix group. This prevents
the re-use of a GID from a deleted group.
cn: lastGID
serialNumber: 2002
# lastUID, example.org
dn: cn=lastUID,dc=example,dc=org
objectClass: device
objectClass: top
description: Records the last UID used to create a Posix account. This prevent
s the re-use of a UID from a deleted account.
cn: lastUID
serialNumber: 2001
# everybody, Group, example.org
dn: cn=everybody,ou=Group,dc=example,dc=org
objectClass: top
objectClass: posixGroup
cn: everybody
memberUid: geecool
gidNumber: 2001
# admins, Group, example.org
dn: cn=admins,ou=Group,dc=example,dc=org
objectClass: top
objectClass: posixGroup
cn: admins
memberUid: geecool
gidNumber: 2002
# geecool, People, example.org
dn: uid=geecool,ou=People,dc=example,dc=org
givenName: gee
sn: cool
uid: geecool
mail: gee@gee.cool
cn: geecool
objectClass: person
objectClass: inetOrgPerson
objectClass: posixAccount
userPassword:: e1NTSEF9b3A5OHZkdS9jNmh5Y1U5bXdBWjZVb2lqc0ZCdGJsZDNlRnA0VUE9PQ=
=
uidNumber: 2001
gidNumber: 2001
loginShell: /bin/bash
homeDirectory: /home/geecool
# search result
search: 2
result: 0 Success
# numResponses: 12
# numEntries: 11
chatgpt咨询
默认保存的密码是base64加密, 相当于没有加密
ldapadd 创建新用户
ldapadd -H ldap://ldap.domain.com:1389 -x -D "cn=admin,dc=example,dc=org" -w "fakepassword" -c <<EOF
dn: cn=user03,ou=People,dc=example,dc=org
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: User3
sn: Bar3
givenName: User3
uid: user03
userPassword: {SHA}nU4r6L+9x0Zj+JZz5xu5zv6v6w=
EOF
response
adding new entry "cn=user03,ou=People,dc=example,dc=org"
ldapsearch 查询新创建的条目
ldapsearch -H ldap://ldap.domain.com:1389 -x -D "cn=admin,dc=example,dc=org" -w "fakepassword" -b "dc=example,dc=org" "(cn=user03)"
# extended LDIF
#
# LDAPv3
# base <dc=example,dc=org> with scope subtree
# filter: (cn=user03)
# requesting: ALL
#
# user03, People, example.org
dn: cn=user03,ou=People,dc=example,dc=org
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: User3
cn: user03
sn: Bar3
givenName: User3
uid: user03
userPassword:: e1NIQX1uVTRyNkwrOXgwWmorSlp6NXh1NXp2NnY2dz0=
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
chatgpt咨询
java ldap
java ldap各种操作都依赖于DirContext的方法, 包括search, destroy, bind, unbind 等操作.
java ldap 查询用户
测试结论: 测试查询不存在的用户不会报错,返回空. 若是上级的searchbase不存在, 则会报错 [LDAP: error code 32 - No Such Object]
@Test
public void searchUser() throws NamingException{
String searchFilter = String.format("(cn=%s)", "user01");
String searchBase = LdapUtils.getOU("dc=example, dc=org", "People");
String[] returnedAtts = {"uid", "cn", "sn"};
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchCtls.setReturningAttributes(returnedAtts);
DirContext ctx = getLdapContex();
NamingEnumeration<SearchResult> rv = ctx.search(searchBase, searchFilter, searchCtls);
int i = 0;
while (rv.hasMore()) {
SearchResult a = rv.next();
System.out.println("rv: " + a);
System.out.println(a.getAttributes());
System.out.println(a.getName());
System.out.println(a.getNameInNamespace());
i += 1;
}
System.out.println("count: " + i);
}
response:
rv: cn=user01: null:null:{uid=uid: user01, sn=sn: Bar1, cn=cn: User1, user01}
{uid=uid: user01, sn=sn: Bar1, cn=cn: User1, user01}
cn=user01
cn=user01,ou=People
java lap 删除用户
测试结论: java ldap删除不存在的用户不会报错, 因此可以多次重复删除; 如果上级的context不存在,则会报错 [LDAP: error code 32 - No Such Object]
package javax.naming;
/* Destroys the named context and removes it from the namespace. Any attributes associated with the name are also removed. Intermediate contexts are not destroyed.
* This method is idempotent. It succeeds even if the terminal atomic name is not bound in the target context, but throws NameNotFoundException if any of the intermediate contexts do not exist.
*/
public void destroySubcontext(Name name) throws NamingException;
/**
* 删除不存在的用户不会报错
*/
@Test
public void removeUser() throws NamingException {
String dn = "cn=user03,ou=People,dc=example,dc=org";
DirContext ctx = getLdapContex();
ctx.destroySubcontext(dn);
}