基于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常见错误码

目录
打赏
0
0
0
1
6
分享
相关文章
飞算 JavaAI:革新电商订单系统 Spring Boot 微服务开发
在电商订单系统开发中,传统方式耗时约30天,需应对复杂代码、调试与测试。飞算JavaAI作为一款AI代码生成工具,专注于简化Spring Boot微服务开发。它能根据业务需求自动生成RESTful API、数据库交互及事务管理代码,将开发时间缩短至1小时,效率提升80%。通过减少样板代码编写,提供规范且准确的代码,飞算JavaAI显著降低了开发成本,为软件开发带来革新动力。
从基础到进阶:Spring Boot + Thymeleaf 整合开发中的常见坑与界面优化
本文深入探讨了 **Spring Boot + Thymeleaf** 开发中常见的参数绑定问题与界面优化技巧。从基础的 Spring MVC 请求参数绑定机制出发,分析了 `MissingServletRequestParameterException` 的成因及解决方法,例如确保前后端参数名、类型一致,正确设置请求方式(GET/POST)。同时,通过实际案例展示了如何优化支付页面的视觉效果,借助简单的 CSS 样式提升用户体验。最后,提供了官方文档等学习资源,帮助开发者更高效地掌握相关技能。无论是初学者还是进阶用户,都能从中受益,轻松应对项目开发中的挑战。
123 0
|
4月前
|
基于SpringBoot的Redis开发实战教程
Redis在Spring Boot中的应用非常广泛,其高性能和灵活性使其成为构建高效分布式系统的理想选择。通过深入理解本文的内容,您可以更好地利用Redis的特性,为应用程序提供高效的缓存和消息处理能力。
273 79
Java 21 与 Spring Boot 3.2 微服务开发从入门到精通实操指南
《Java 21与Spring Boot 3.2微服务开发实践》摘要: 本文基于Java 21和Spring Boot 3.2最新特性,通过完整代码示例展示了微服务开发全流程。主要内容包括:1) 使用Spring Initializr初始化项目,集成Web、JPA、H2等组件;2) 配置虚拟线程支持高并发;3) 采用记录类优化DTO设计;4) 实现JPA Repository与Stream API数据访问;5) 服务层整合虚拟线程异步处理和结构化并发;6) 构建RESTful API并使用Springdoc生成文档。文中特别演示了虚拟线程配置(@Async)和StructuredTaskSco
128 0
Java 开发玩转 MCP:从 Claude 自动化到 Spring AI Alibaba 生态整合
本文详细讲解了Java开发者如何基于Spring AI Alibaba框架玩转MCP(Model Context Protocol),涵盖基础概念、快速体验、服务发布与调用等内容。重点包括将Spring应用发布为MCP Server(支持stdio与SSE模式)、开发MCP Client调用服务,以及在Spring AI Alibaba的OpenManus中使用MCP增强工具能力。通过实际示例,如天气查询与百度地图路线规划,展示了MCP在AI应用中的强大作用。最后总结了MCP对AI开发的意义及其在Spring AI中的实现价值。
1182 9
保姆级Spring AI 注解式开发教程,你肯定想不到还能这么玩!
这是一份详尽的 Spring AI 注解式开发教程,涵盖从环境配置到高级功能的全流程。Spring AI 是 Spring 框架中的一个模块,支持 NLP、CV 等 AI 任务。通过注解(如自定义 `@AiPrompt`)与 AOP 切面技术,简化了 AI 服务集成,实现业务逻辑与 AI 基础设施解耦。教程包含创建项目、配置文件、流式响应处理、缓存优化及多任务并行执行等内容,助你快速构建高效、可维护的 AI 应用。
SaaS云计算技术的智慧工地源码,基于Java+Spring Cloud框架开发
智慧工地源码基于微服务+Java+Spring Cloud +UniApp +MySql架构,利用传感器、监控摄像头、AI、大数据等技术,实现施工现场的实时监测、数据分析与智能决策。平台涵盖人员、车辆、视频监控、施工质量、设备、环境和能耗管理七大维度,提供可视化管理、智能化报警、移动智能办公及分布计算存储等功能,全面提升工地的安全性、效率和质量。

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等