基于Spring Boot的LDAP开发全教程

写在前面

协议概述

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/133893194

依赖库引入

spring-boot-starter-data-ldap是Spring Boot封装的对LDAP自动化配置的实现,它是基于spring-data-ldap来对LDAP服务端进行具体操作的。

bash 复制代码
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-ldap', version: '2.7.5';

配置连接

yaml 复制代码
# LDAP连接配置
spring:
  ldap:
    enable: true
    urls: ldaps://10.10.18.181:636
    base: "DC=adgamioo,DC=com"
    username: [email protected]
    password: *********

注意:

  • ldap默认端口为389,ldaps默认端口为636 创建有密码的账号,重置密码操作必须使用ldaps协议;
  • 使用ldaps协议必须配置ssl证书,大部分解决方案是需要从ldap 服务器上导出证书,然后再通过Java的keytool 工具导入证书,比较繁琐,反正从服务器上导出证书那一步就很烦了。当然了也有办法绕过证书,下面,说一下如何代码配置ldap 跳过SSL。

配置信息读取:

java 复制代码
@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;
}

跳过证书:

java 复制代码
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];
    }
}
java 复制代码
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);
    }
}
java 复制代码
@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());
        Map<String, Object> config = 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层:

java 复制代码
/**
 * UserRepository继承LdapRepository接口实现基于Ldap的增删改查操作
 */

public interface UserRepository extends LdapRepository<LdapUser> {
    LdapUser findByCommonName(String cn);
}

操作对象:

java 复制代码
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 ,主要用于控制账号的禁用还是正常使用:

java 复制代码
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域用户创建,认证、查询用户、更新用户,重置密码,禁用用户等操作

java 复制代码
@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<LdapUser>
     */
    @Override
    public List<LdapUser> 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: adyorha.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

Q:为什么修改密码后,新老密码在一段时间内都有效?

A:经过查阅资料发现,在server 2008级别的AD下,旧密码生存期为5分钟,在server 2003级别的AD下,旧密码生存期为60分钟。

这个5分钟就是为了防止AD同步延时问题,防止DC数量比较多时,用户登录所在的站点内还没有成功的更新到密码的修改的情况。。这样,即使新密码没有生效,旧密码依然可用。有些网络效率不高的情况下,是会发生密码同步需要一定时间的情况的。鉴于这样的考虑,我们的旧密码,就有启用了一种生存时间的概念。

值得注意的是,这个缓存,在LDAP验证方式中存在,但却不存在于kerberos验证方式中。换句话说,也就是我们最常见的使用Ctrl-Alt-Del的交互式方式登录到桌面系统是不会存在旧密码可用的情况的。

参考链接

Spring LDAP Reference官方文档
ldap常见错误码

相关推荐
勤奋的知更鸟1 分钟前
Java编程之组合模式
java·开发语言·设计模式·组合模式
千|寻2 分钟前
【画江湖】langchain4j - Java1.8下spring boot集成ollama调用本地大模型之问道系列(第一问)
java·spring boot·后端·langchain
程序员岳焱15 分钟前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
后端·sql·mysql
爱编程的喵16 分钟前
深入理解JavaScript原型机制:从Java到JS的面向对象编程之路
java·前端·javascript
龚思凯21 分钟前
Node.js 模块导入语法变革全解析
后端·node.js
天行健的回响24 分钟前
枚举在实际开发中的使用小Tips
后端
on the way 12327 分钟前
行为型设计模式之Mediator(中介者)
java·设计模式·中介者模式
保持学习ing29 分钟前
Spring注解开发
java·深度学习·spring·框架
wuhunyu29 分钟前
基于 langchain4j 的简易 RAG
后端
techzhi30 分钟前
SeaweedFS S3 Spring Boot Starter
java·spring boot·后端