LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)是一种用于访问和维护分布式目录服务的开放标准协议,是一种基于TCP/IP协议的客户端-服务器协议,用于访问和管理分布式目录服务,如企业内部的用户目录、组织结构和资源信息等。LDAP具有轻量级、高效性和可扩展性等特点,被广泛应用于AD域操作,身份验证、用户管理、电子邮件系统和网络存储等领域。
连接建立:客户端通过TCP连接到LDAP服务器的默认端口389。
用户认证:客户端发送BIND请求进行身份认证。
目录搜索:客户端发送SEARCH请求查询目录信息。
数据操作:客户端发送ADD、DELETE、MODIFY等请求进行目录数据的增删改操作。
连接关闭:传输完成后,客户端发送UNBIND请求关闭连接。
LDAP协议中的数据操作主要包括BIND、UNBIND、SEARCH、ADD、DELETE、MODIFY等请求
o– organization(组织-公司)
ou – organization unit(组织单元-部门)
c - countryName(国家)
dc - domainComponent(域名)
sn – suer name(真实名称)
cn - common name(常用名称
版权声明:本文为CSDN博主「流子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://jiangguilong2000.blog.csdn.net/article/details/133900773
spring-boot-starter-data-ldap是Spring Boot封装的对LDAP自动化配置的实现,它是基于spring-data-ldap来对LDAP服务端进行具体操作的。
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-ldap', version: '2.7.5';
# LDAP连接配置 spring: ldap: enable: true urls: ldaps://10.10.18.181:636 base: "DC=adgamioo,DC=com" username: user001@gamioo.com password: *********
注意:
配置信息读取:
@RefreshScope @ConfigurationProperties(LdapProperties.PREFIX) public class LdapProperties { public static final String PREFIX = "spring.ldap"; private Boolean enable = true; private String urls; private String base; private String userName; /** * Secret key是你账户的密码 */ private String password; }
跳过证书:
import javax.net.ssl.X509TrustManager; import java.security.cert.X509Certificate; public class DummyTrustManager implements X509TrustManager { public void checkClientTrusted(X509Certificate[] cert, String authType) { } public void checkServerTrusted(X509Certificate[] cert, String authType) { } public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }
public class DummySSLSocketFactory extends SSLSocketFactory { private final static Logger logger = LoggerFactory.getLogger(DummySSLSocketFactory.class); private SSLSocketFactory factory; public DummySSLSocketFactory() { try { SSLContext sslcontext = SSLContext.getInstance("TLS"); sslcontext.init(null, new TrustManager[]{new DummyTrustManager()}, new java.security.SecureRandom()); factory = sslcontext.getSocketFactory(); } catch (Exception ex) { logger.error(ex.getMessage(), ex); } } public static SocketFactory getDefault() { return new DummySSLSocketFactory(); } @Override public String[] getDefaultCipherSuites() { return factory.getDefaultCipherSuites(); } @Override public String[] getSupportedCipherSuites() { return factory.getSupportedCipherSuites(); } @Override public Socket createSocket(Socket socket, String string, int num, boolean bool) throws IOException { return factory.createSocket(socket, string, num, bool); } @Override public Socket createSocket(String string, int num) throws IOException { return factory.createSocket(string, num); } @Override public Socket createSocket(String string, int num, InetAddress netAdd, int i) throws IOException { return factory.createSocket(string, num, netAdd, i); } @Override public Socket createSocket(InetAddress netAdd, int num) throws IOException { return factory.createSocket(netAdd, num); } @Override public Socket createSocket(InetAddress netAdd1, int num, InetAddress netAdd2, int i) throws IOException { return factory.createSocket(netAdd1, num, netAdd2, i); } }
@AutoConfiguration @EnableConfigurationProperties(LdapProperties.class) @ConditionalOnProperty(value = LdapProperties.PREFIX + ".enabled", havingValue = "true", matchIfMissing = true) @EnableLdapRepositories(basePackages = "io.gamioo.core.ldap.dao") public class LdapConfiguration { @Resource private LdapProperties properties; // @Bean public ContextSource contextSource() { // Security.setProperty("jdk.tls.disabledAlgorithms", ""); System.setProperty("com.sun.jndi.ldap.object.disableEndpointIdentification", "true"); LdapContextSource source = new LdapContextSource(); source.setUserDn(properties.getUserName()); source.setPassword(properties.getPassword()); source.setBase(properties.getBase()); source.setUrl(properties.getUrls()); Mapconfig = new HashMap<>(); config.put(Context.AUTHORITATIVE, "true"); config.put(Context.SECURITY_PROTOCOL, "ssl"); config.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); config.put(Context.SECURITY_AUTHENTICATION, "simple"); // 解决乱码 config.put("java.naming.ldap.attributes.binary", "objectGUID"); config.put("java.naming.ldap.factory.socket", DummySSLSocketFactory.class.getName()); source.setBaseEnvironmentProperties(config); return source; } @Bean public LdapTemplate ldapTemplate(ContextSource contextSource) { LdapTemplate template = new LdapTemplate(contextSource); template.setIgnorePartialResultException(true); return template; } }
DAO层:
/** * UserRepository继承LdapRepository接口实现基于Ldap的增删改查操作 */ public interface UserRepository extends LdapRepository{ LdapUser findByCommonName(String cn); }
操作对象:
import org.springframework.data.domain.Persistable; import org.springframework.ldap.odm.annotations.Attribute; import org.springframework.ldap.odm.annotations.Entry; import org.springframework.ldap.odm.annotations.Id; import org.springframework.ldap.odm.annotations.Transient; import javax.naming.Name; @Entry(base = "", objectClasses = {"person", "user", "top", "organizationalPerson"}) public final class LdapUser implements Persistable { @Id private Name id; @Transient private boolean isNew; @Attribute(name = "userPrincipalName") private String userPrincipalName; @Attribute(name = "userAccountControl") private String status; @Attribute(name = "distinguishedName") private String dn; @Attribute(name = "cn") private String commonName; @Attribute(name = "givenName") private String givenName; @Attribute(name = "unicodePwd", type = Attribute.Type.BINARY) private byte[] unicodePassword; @Attribute(name = "sAMAccountName") private String accountName; @Attribute(name = "displayName") private String displayName; }
常量类LdapConstant ,主要用于控制账号的禁用还是正常使用:
public interface LdapConstant { int ACCOUNT_DISABLE = 0x0001 << 1; // 账户已禁用 int LOCKOUT = 0x0001 << 4; // 账户已锁定 int PASSWD_NOTREQD = 0x0001 << 5; // 不需要密码 int PASSWD_CANT_CHANGE = 0x0001 << 6; // 用户不能更改密码(只读,不能修改) int NORMAL_ACCOUNT = 0x0001 << 9; // 正常账户 int DONT_EXPIRE_PASSWORD = 0x0001 << 16; // 密码永不过期 int PASSWORD_EXPIRED = 0x0001 << 23; // 密码已过期 }
实现AD域用户创建,认证、查询用户、更新用户,重置密码,禁用用户等操作
@Service @Transactional(rollbackFor = Exception.class) public class LdapServiceImpl implements ILdapService { private final static Logger logger = LoggerFactory.getLogger(LdapServiceImpl.class); @Resource private UserRepository repository; @Resource public LdapTemplate ldapTemplate; /** * 禁用用户 * * @param userId 用户id */ @Override public void disableUser(String userId) { logger.info("disable user:{}", userId); LdapUser user = this.findUserBy(userId); if (user != null) { user.setStatus(String.valueOf(LdapConstant.ACCOUNT_DISABLE)); repository.save(user); } } /** * 激活用户 * * @param userId 用户id */ @Override public void activeUser(String userId) { logger.info("active user:{}", userId); LdapUser user = this.findUserBy(userId); if (user != null) { user.setStatus(String.valueOf(LdapConstant.NORMAL_ACCOUNT)); repository.save(user); } } /** * 查询所有用户信息 * * @return List*/ @Override public List findAll() { return repository.findAll(); } /** * 根据userId查询用户信息 * * @param userId 用户id * @return User */ @Override public LdapUser findUserBy(String userId) { LdapUser ret = repository.findByCommonName(userId); return ret; } /** * 删除用户 * * @param userId 用户id */ @Override public void deleteUser(String userId) { logger.info("delete user:{}", userId); LdapUser user = this.findUserBy(userId); if (user != null) { repository.delete(user); } } /** * 创建用户(账号 + 密码) * * @param userId 用户id * @param password 密码 */ @Override public void createUser(String userId, String password) { logger.info("create user:{},password:{}", userId, password); Name name = LdapNameBuilder.newInstance().add("CN", "Users").add("CN", userId).build(); LdapUser user = new LdapUser(); user.setCommonName(userId); user.setDisplayName(userId); user.setGivenName(userId); user.setNew(true); user.setAccountName(userId); user.setStatus(String.valueOf(LdapConstant.NORMAL_ACCOUNT)); user.setUserPrincipalName(userId + "@adgamioo.com"); user.setId(name); user.setUnicodePassword(this.encodePwd(password)); repository.save(user); } /** * 修改用户 * * @param user user */ public void updateUser(LdapUser user) { logger.info("update user:{}", user.getAccountName()); repository.save(user); } /** * 重置密码 * * @param userId 用户id * @param newPassword 新密码 */ @Override public void resetPwd(String userId, String newPassword) { logger.info("resetPwd user:{},{}", userId, newPassword); // 1. 查找AD用户 LdapUser user = repository.findByCommonName(userId); ModificationItem[] mods = new ModificationItem[1]; mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("unicodePwd", encodePwd(newPassword))); ldapTemplate.modifyAttributes(user.getId(), mods); } /** * 密码加密 * * @param source 密文 * @return 加密后密码 */ private byte[] encodePwd(String source) { String quotedPassword = "\"" + source + "\""; // 注意:必须在密码前后加上双引号 return quotedPassword.getBytes(StandardCharsets.UTF_16LE); } }
以上代码亲测有效!
javax.naming.NameAlreadyBoundException: [LDAP: error code 68 - 00000524: UpdErr: DSID-031A11F8, problem 6005 (ENTRY_EXISTS), data 0
同名的实体已经存在
javax.naming.NameNotFoundException: [LDAP: error code 32 - 0000208D: NameErr: DSID-03100245, problem 2001 (NO_OBJECT), data 0, best match of:
‘DC=adgamioo,DC=com’
一般是路径节点下没有找到对应实体,可能是base路径已经配置了,id中又去加了路径
javax.naming.CommunicationException: simple bind failed: adgamioo.com:636
java.net.SocketException: Connection or outbound has closed
连接失败,比如ldaps服务没开启等
org.springframework.ldap.OperationNotSupportedException: [LDAP: error code 53 - 0000001F: SvcErr: DSID-031A126A, problem 5003 (WILL_NOT_PERFORM), data 0
比如在389端口下进行密码修改或者创建有密码的用户,又或是修改userAccountControl
Q:为什么修改密码后,新老密码在一段时间内都有效?
A:经过查阅资料发现,在server 2008级别的AD下,旧密码生存期为5分钟,在server 2003级别的AD下,旧密码生存期为60分钟。
这个5分钟就是为了防止AD同步延时问题,防止DC数量比较多时,用户登录所在的站点内还没有成功的更新到密码的修改的情况。。这样,即使新密码没有生效,旧密码依然可用。有些网络效率不高的情况下,是会发生密码同步需要一定时间的情况的。鉴于这样的考虑,我们的旧密码,就有启用了一种生存时间的概念。
值得注意的是,这个缓存,在LDAP验证方式中存在,但却不存在于kerberos验证方式中。换句话说,也就是我们最常见的使用Ctrl-Alt-Del的交互式方式登录到桌面系统是不会存在旧密码可用的情况的。
Q:为何一个账号,搜出来两个结果,一个user,一个computer,
Listlist = repository.findAllByCommonName("allen"); list = {LinkedList@18451} size = 2 0 = {LdapUser@18476} "LdapUser[accountName=allen,commonName=allen,displayName=allen,dn=CN=allen,CN=Users,DC=adGAMIOO,DC=com,givenName=allen,id=CN=allen,CN=Users,isNew=false,status=512,unicodePassword= ,userPrincipalName=allen@adgamioo.com]" 1 = {LdapUser@18477} "LdapUser[accountName=ALLEN$,commonName=ALLEN,displayName=ALLEN$,dn=CN=ALLEN,CN=Computers,DC=adGAMIOO,DC=com,givenName= ,id=CN=ALLEN,CN=Computers,isNew=false,status=4096,unicodePassword= ,userPrincipalName= ]"
A:将以下属性添加到 ldapRegistry.xml 文件:ignoreCase=“false”
Spring LDAP Reference官方文档
ldap常见错误码