• 基于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/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';
    
    • 1

    配置连接

    # LDAP连接配置
    spring:
      ldap:
        enable: true
        urls: ldaps://10.10.18.181:636
        base: "DC=adgamioo,DC=com"
        username: user001@gamioo.com
        password: *********
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注意:

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

    配置信息读取:

    @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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    跳过证书:

    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];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    @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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    DAO层:

    /**
     * UserRepository继承LdapRepository接口实现基于Ldap的增删改查操作
     */
    
    public interface UserRepository extends LdapRepository<LdapUser> {
        LdapUser findByCommonName(String cn);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    操作对象:

    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    常量类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; // 密码已过期
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    实现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<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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139

    以上代码亲测有效!

    常见异常

    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

    Q:为什么修改密码后,新老密码在一段时间内都有效?
    A:经过查阅资料发现,在server 2008级别的AD下,旧密码生存期为5分钟,在server 2003级别的AD下,旧密码生存期为60分钟。
    这个5分钟就是为了防止AD同步延时问题,防止DC数量比较多时,用户登录所在的站点内还没有成功的更新到密码的修改的情况。。这样,即使新密码没有生效,旧密码依然可用。有些网络效率不高的情况下,是会发生密码同步需要一定时间的情况的。鉴于这样的考虑,我们的旧密码,就有启用了一种生存时间的概念。
    值得注意的是,这个缓存,在LDAP验证方式中存在,但却不存在于kerberos验证方式中。换句话说,也就是我们最常见的使用Ctrl-Alt-Del的交互式方式登录到桌面系统是不会存在旧密码可用的情况的。
    Q:为何一个账号,搜出来两个结果,一个user,一个computer,

    List<LdapUser> list = 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=]"
    
    • 1
    • 2
    • 3
    • 4

    A:将以下属性添加到 ldapRegistry.xml 文件:ignoreCase=“false”

    <server>
        <ldapRegistry
            host="9.118.40.171"
            port="389"
            baseDN="dc=com"
            realm="LdapRegistryRealm"
            id="LdapRegistryId"
            ignoreCase="false"
            ldapType="IBM Tivoli Directory Server">
            <idsFilters
                userFilter="(&(uid=%v)(|(objectclass=ePerson)(objectclass=inetOrgPerson)))"
                groupFilter="(&(cn=%v)(|(objectclass=groupOfNames)(objectclass=groupOfUniqueNames)(objectclass=groupOfURLs)))" 
                userIdMap="*:uid"
                groupIdMap="*:cn" 
                groupMemberIdMap="ibm-allGroups:member;ibm-allGroups:uniqueMember;groupOfNames:member;groupOfUniqueNames:uniqueMember"/>
         ldapRegistry>
     server>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    参考链接

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

  • 相关阅读:
    leetcode 热题 100_搜索二维矩阵
    BUUCTF [BJDCTF2020]认真你就输了 1
    通用CI/CD软件平台TeamCity推出代理终端功能,谁能从中获益?
    Elasticsearch上手指南
    尚未解决的难题
    Java 数据结构与算法 堆
    AnyTransition/过渡动画, MatchedGeometryEffect/匹配几何动画效果 的使用
    【C语言课程设计】医院管理系统
    linux驱动开发学习001:概述
    oracle性能优化:ORACLE SQL性能优化系列 (三)[转]
  • 原文地址:https://blog.csdn.net/jiangguilong2000/article/details/133900773