基于Spring Boot的LDAP开发全教程

简介: 基于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';

配置连接

# LDAP连接配置
spring:
  ldap:
    enable: true
    urls: ldaps://10.10.18.181:636
    base: "DC=adgamioo,DC=com"
    username: user001@gamioo.com
    password: *********

注意:

  • 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;
}

跳过证书:

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());
        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层:

/**
 * UserRepository继承LdapRepository接口实现基于Ldap的增删改查操作
 */
public interface UserRepository extends LdapRepository<LdapUser> {
    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<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: 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=<null>,userPrincipalName=allen@adgamioo.com]"
 1 = {LdapUser@18477} "LdapUser[accountName=ALLEN$,commonName=ALLEN,displayName=ALLEN$,dn=CN=ALLEN,CN=Computers,DC=adGAMIOO,DC=com,givenName=<null>,id=CN=ALLEN,CN=Computers,isNew=false,status=4096,unicodePassword=<null>,userPrincipalName=<null>]"

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>

参考链接

Spring LDAP Reference官方文档

ldap常见错误码

目录
相关文章
|
15天前
|
XML JavaScript Java
Spring Retry 教程
Spring Retry 是 Spring 提供的用于处理方法重试的库,通过 AOP 提供声明式重试机制,不侵入业务逻辑代码。主要步骤包括:添加依赖、启用重试机制、设置重试策略(如异常类型、重试次数、延迟策略等),并可定义重试失败后的回调方法。适用于因瞬时故障导致的操作失败场景。
Spring Retry 教程
|
9天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
527 8
|
20天前
|
Java 数据库连接 数据格式
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
IOC/DI配置管理DruidDataSource和properties、核心容器的创建、获取bean的方式、spring注解开发、注解开发管理第三方bean、Spring整合Mybatis和Junit
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
|
20天前
|
Java 数据库连接 Maven
Spring基础1——Spring(配置开发版),IOC和DI
spring介绍、入门案例、控制反转IOC、IOC容器、Bean、依赖注入DI
Spring基础1——Spring(配置开发版),IOC和DI
|
28天前
|
缓存 Java 应用服务中间件
随着微服务架构的兴起,Spring Boot凭借其快速开发和易部署的特点,成为构建RESTful API的首选框架
【9月更文挑战第6天】随着微服务架构的兴起,Spring Boot凭借其快速开发和易部署的特点,成为构建RESTful API的首选框架。Nginx作为高性能的HTTP反向代理服务器,常用于前端负载均衡,提升应用的可用性和响应速度。本文详细介绍如何通过合理配置实现Spring Boot与Nginx的高效协同工作,包括负载均衡策略、静态资源缓存、数据压缩传输及Spring Boot内部优化(如线程池配置、缓存策略等)。通过这些方法,开发者可以显著提升系统的整体性能,打造高性能、高可用的Web应用。
58 2
|
29天前
|
NoSQL 前端开发 Java
使用 Spring Boot + Neo4j 实现知识图谱功能开发
在数据驱动的时代,知识图谱作为一种强大的信息组织方式,正逐渐在各个领域展现出其独特的价值。本文将围绕使用Spring Boot结合Neo4j图数据库来实现知识图谱功能开发的技术细节进行分享,帮助读者理解并掌握这一技术栈在实际项目中的应用。
100 4
|
1月前
|
安全 Java 开发者
强大!Spring Cloud Gateway新特性及高级开发技巧
在微服务架构日益盛行的今天,网关作为微服务架构中的关键组件,承担着路由、安全、监控、限流等多重职责。Spring Cloud Gateway作为新一代的微服务网关,凭借其基于Spring Framework 5、Project Reactor和Spring Boot 2.0的强大技术栈,正逐步成为业界的主流选择。本文将深入探讨Spring Cloud Gateway的新特性及高级开发技巧,助力开发者更好地掌握这一强大的网关工具。
109 6
|
1月前
|
IDE Java 开发工具
还在为繁琐的配置头疼吗?一文教你如何用 Spring Boot 快速启动,让开发效率飙升,从此告别加班——打造你的首个轻量级应用!
【9月更文挑战第2天】Spring Boot 是一款基于 Spring 框架的简化开发工具包,采用“约定优于配置”的原则,帮助开发者快速创建独立的生产级应用程序。本文将指导您完成首个 Spring Boot 项目的搭建过程,包括环境配置、项目初始化、添加依赖、编写控制器及运行应用。首先需确保 JDK 版本不低于 8,并安装支持 Spring Boot 的现代 IDE,如 IntelliJ IDEA 或 Eclipse。
86 5
|
2月前
|
Java Spring 人工智能
AI 时代浪潮下,Spring 框架异步编程点亮高效开发之路,你还在等什么?
【8月更文挑战第31天】在快节奏的软件开发中,Spring框架通过@Async注解和异步执行器提供了强大的异步编程工具,提升应用性能与用户体验。异步编程如同魔法,使任务在后台执行而不阻塞主线程,保持界面流畅。只需添加@Async注解即可实现方法的异步执行,或通过配置异步执行器来管理线程池,提高系统吞吐量和资源利用率。尽管存在线程安全等问题,但异步编程能显著增强应用的响应性和效率。
32 0
|
2月前
|
Java Spring 开发者
解锁 Spring Boot 自动化配置的黑科技:带你走进一键配置的高效开发新时代,再也不怕繁琐设置!
【8月更文挑战第31天】Spring Boot 的自动化配置机制极大简化了开发流程,使开发者能专注业务逻辑。通过 `@SpringBootApplication` 注解组合,特别是 `@EnableAutoConfiguration`,Spring Boot 可自动激活所需配置。例如,添加 JPA 依赖后,只需在 `application.properties` 配置数据库信息,即可自动完成 JPA 和数据源设置。这一机制基于多种条件注解(如 `@ConditionalOnClass`)实现智能配置。深入理解该机制有助于提升开发效率并更好地解决问题。
49 0
下一篇
无影云桌面