当前位置: 首页 > news >正文

基于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: trueurls: ldaps://10.10.18.181:636base: "DC=adgamioo,DC=com"username: user001@gamioo.compassword: *********

注意:

  • 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();}@Overridepublic String[] getDefaultCipherSuites() {return factory.getDefaultCipherSuites();}@Overridepublic String[] getSupportedCipherSuites() {return factory.getSupportedCipherSuites();}@Overridepublic Socket createSocket(Socket socket, String string, int num, boolean bool) throws IOException {return factory.createSocket(socket, string, num, bool);}@Overridepublic Socket createSocket(String string, int num) throws IOException {return factory.createSocket(string, num);}@Overridepublic Socket createSocket(String string, int num, InetAddress netAdd, int i) throws IOException {return factory.createSocket(string, num, netAdd, i);}@Overridepublic Socket createSocket(InetAddress netAdd, int num) throws IOException {return factory.createSocket(netAdd, num);}@Overridepublic 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 {@Resourceprivate LdapProperties properties;//@Beanpublic 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;}@Beanpublic 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 {@Idprivate Name id;@Transientprivate 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);@Resourceprivate UserRepository repository;@Resourcepublic LdapTemplate ldapTemplate;/*** 禁用用户** @param userId 用户id*/@Overridepublic 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*/@Overridepublic 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>*/@Overridepublic List<LdapUser> findAll() {return repository.findAll();}/*** 根据userId查询用户信息** @param userId 用户id* @return User*/@Overridepublic LdapUser findUserBy(String userId) {LdapUser ret = repository.findByCommonName(userId);return ret;}/*** 删除用户** @param userId 用户id*/@Overridepublic 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 密码*/@Overridepublic 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 新密码*/@Overridepublic 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的交互式方式登录到桌面系统是不会存在旧密码可用的情况的。

参考链接

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

相关文章:

基于Spring Boot的LDAP开发全教程

写在前面 协议概述 LDAP&#xff08;轻量级目录访问协议&#xff0c;Lightweight Directory Access Protocol)是一种用于访问和维护分布式目录服务的开放标准协议,是一种基于TCP/IP协议的客户端-服务器协议&#xff0c;用于访问和管理分布式目录服务&#xff0c;如企业内部的…...

在 Linux 上保护 SSH 服务器连接的 8 种方法

SSH 是一种广泛使用的协议&#xff0c;用于安全地访问 Linux 服务器。大多数用户使用默认设置的 SSH 连接来连接到远程服务器。但是&#xff0c;不安全的默认配置也会带来各种安全风险。 具有开放 SSH 访问权限的服务器的 root 帐户可能存在风险。尤其是如果使用的是公共 IP 地…...

摩尔信使MThings的协议转换(数据网关)功能

摩尔信使MThings可以作为现场总线&#xff08;RS485&#xff09;和以太网的数据中枢&#xff0c;并拥有强大的Modbus协议转换功能。 数据网关功能提供协议转换和数据汇聚功能&#xff0c;可实现多维度映射&#xff0c;包括&#xff1a;不同的通道(总线)类型、协议类型&#xff…...

Mac安装Kali保姆级教程

Mac安装Kali保姆级教程 其他安装教程&#xff1a;使用VMware安装系统Window、Linux&#xff08;kali&#xff09;、Mac操作系统 1 虚拟机安装VM Fusion 去官网下载VM Fusion 地址&#xff1a;https://customerconnect.vmware.com/en/evalcenter?pfusion-player-personal-13 …...

利用Spring Boot框架做事件发布和监听

一、编写事件 1.编写事件类并集成spring boot 事件接口&#xff0c;提供访问事件参数属性 public class PeriodicityRuleChangeEvent extends ApplicationEvent {private final JwpDeployWorkOrderRuleDTO jwpDeployWorkOrderRuleDTO;public PeriodicityRuleChangeEvent(Obje…...

KingBase库模式表空间和客户端认证(kylin)

库、模式、表空间 数据库 数据库基集簇与数据库实例 KES集簇是由单个KES实例管理的数据库的集合KES集簇中的库使用相同的全局配置文件和监听端口、共享相关的进程和内存结构同一数据库集簇中的进程、相关的内存结构统称为实例 数据库 数据库是一个长期存储在计算机内的、有…...

h5的扫一扫功能 (非微信浏览器环境下)

必须在 https 域名下才生效 <template><div><van-field label"服务商编码" right-icon"scan" placeholder"扫描二维码获取" click-right-icon"getCameras" /> <div class"scan" :style"{disp…...

Typora 导出PDF 报错 failed to export as pdf. undefined 解决方案

情况 我想把一个很大的markdown 导出为 248页的pdf 然后就报错 failed to export as pdf. undefined 原因 &#xff1a; 个人感觉应该是图片太大了 格式问题之类导致的 解决 文件 -> 偏好设置 - > 导出 -> pdf -> 自定义 -> 把大小全部改为24mm (虽然图中是32 …...

[架构之路-239]:目标系统 - 纵向分层 - 中间件middleware

目录 前言&#xff1a; 一、中间件概述 1.1 中间件在软件层次中的位置 1.2 什么是中间件 1.3 为什么需要中间件 1.4 中间件应用场合&#xff08;应用程序不用的底层需求&#xff1a;计算、存储、通信&#xff09; 1.5 中间件分类 - 按内容分 二、嵌入式系统的中间件 2…...

javascript利用xhr对象实现http流的comet轮循,主要是利用readyState等于3的特点

//此文件 为前端获取http流 <!DOCTYPE html> <html xmlns"http://www.w3.org/1999/xhtml" lang"UTF-8"></html> <html><head><meta http-equiv"Content-Type" content"text/html; charsetUTF-8"/&g…...

【Mybatis源码】XPathParser解析器

XPathParser是Mybatis中定义的进行解析XML文件的类,此类用于读取XML文件中的节点文本与属性;本篇我们主要介绍XPathParser解析XML的原理。 一、XPathParser构造方法 这里我们介绍主要的构造方法 public XPathParser(InputStream inputStream, boolean validation, Propert…...

辉视智慧酒店解决方案助力传统酒店通过智能升级焕发新生

辉视智慧酒店解决方案基于强大的物联网平台&#xff0c;将酒店客控、网络覆盖、客房智能化控制、酒店服务交互等完美融合&#xff0c;打造出全方位的酒店智慧化产品。利用最新的信息化技术&#xff0c;我们推动酒店智慧化转型&#xff0c;综合运用前沿的信息科学和技术、消费方…...

文件和命令的查找与处理

1.命令查找 which which 接命令 2.文件查找 find 按文件名字查找 准确查找 find / -name "hosts" 粗略查找 find / -name "ho*ts" 扩展名查找 find / -name "*.txt" 按文件类型查找 find / -type f 文件查找 find / -ty…...

第七章:最新版零基础学习 PYTHON 教程—Python 列表(第三节 -Python程序访问列表中的索引和值)

有多种方法可以访问列表的元素,但有时我们可能需要访问元素及其所在的索引。让我们看看访问列表中的索引和值的所有不同方法。 目录 使用Naive 方法访问列表中的索引和值 使用列表理解访问列表中的索引和值...

接口测试面试题整理​​​​​​​

HTTP, HTTPS协议 什么是DNSHTTP协议怎么抓取HTTPS协议说出请求接口中常见的返回状态码http协议请求方式HTTP和HTTPS协议区别HTTP和HTTPS实现机有什么不同POST和GET的区别HTTP请求报文与响应报文格式什么是Http协议无状态协议?怎么解决HTTP协议无状态协议常见的POST提交数据方…...

【保姆级教程】ChatGPT/GPT4科研技术应用与AI绘图

查看原文>>>https://mp.weixin.qq.com/s?__bizMzAxNzcxMzc5MQ&mid2247663763&idx1&snbaeb113ffe0e9ebf2b81602b7ccfa0c6&chksm9bed5f83ac9ad6955d78e4a696949ca02e1e531186464847ea9c25a95ba322f817c1fc7d4e86&token1656039588&langzh_CN#rd…...

凉鞋的 Godot 笔记 202. 变量概述与简介

202. 变量概述与简介 想要用好变量不是一件简单的事情&#xff0c;因为变量需要命名。 我们可以从两个角度去看待一个变量&#xff0c;第一个角度是变量的功能&#xff0c;第二个是变量的可读性。 变量的功能其实非常简单&#xff0c;变量可以存储一个值&#xff0c;这个值是…...

HTML 常用标签及练习

常用标签 <head>中的标签 概述 head中的内容不显示到页面上 标签说明<title>定义网页的标题<meta>定义网页的基本信息&#xff08;供搜索引擎&#xff09;<style>定义CSS样式<link>链接外部CSS文件或脚本文件<script>定义脚本语言<…...

Python 编程基础 | 第六章-包与模块管理 | 1、包与模块简介

一、模块 在程序开发过程中&#xff0c;文件代码越来越长&#xff0c;维护越来越不容易。可以把很多不同的功能编写成函数&#xff0c;放到不同的文件里&#xff0c;方便管理和调用。在Python中&#xff0c;一个.py文件就称之为一个模块&#xff08;Module&#xff09;。 1、简…...

为中小企业的网络推广策略解析:扩大品牌知名度和曝光度

目前网络推广已经成为企业获取潜在客户和提升品牌知名度的重要手段。对于中小企业而言&#xff0c;网络推广是一个具有巨大潜力和可行性的营销策略。在本文中&#xff0c;我们将探讨中小企业为什么有必要进行网络推广&#xff0c;并分享一些实用的网络推广策略。 一、扩大品牌知…...

C++_核心编程_多态案例二-制作饮品

#include <iostream> #include <string> using namespace std;/*制作饮品的大致流程为&#xff1a;煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例&#xff0c;提供抽象制作饮品基类&#xff0c;提供子类制作咖啡和茶叶*//*基类*/ class AbstractDr…...

STM32+rt-thread判断是否联网

一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...

css的定位(position)详解:相对定位 绝对定位 固定定位

在 CSS 中&#xff0c;元素的定位通过 position 属性控制&#xff0c;共有 5 种定位模式&#xff1a;static&#xff08;静态定位&#xff09;、relative&#xff08;相对定位&#xff09;、absolute&#xff08;绝对定位&#xff09;、fixed&#xff08;固定定位&#xff09;和…...

《基于Apache Flink的流处理》笔记

思维导图 1-3 章 4-7章 8-11 章 参考资料 源码&#xff1a; https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...

鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南

1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发&#xff0c;使用DevEco Studio作为开发工具&#xff0c;采用Java语言实现&#xff0c;包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...

使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度

文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...

【笔记】WSL 中 Rust 安装与测试完整记录

#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统&#xff1a;Ubuntu 24.04 LTS (WSL2)架构&#xff1a;x86_64 (GNU/Linux)Rust 版本&#xff1a;rustc 1.87.0 (2025-05-09)Cargo 版本&#xff1a;cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...

C/C++ 中附加包含目录、附加库目录与附加依赖项详解

在 C/C 编程的编译和链接过程中&#xff0c;附加包含目录、附加库目录和附加依赖项是三个至关重要的设置&#xff0c;它们相互配合&#xff0c;确保程序能够正确引用外部资源并顺利构建。虽然在学习过程中&#xff0c;这些概念容易让人混淆&#xff0c;但深入理解它们的作用和联…...

JS手写代码篇----使用Promise封装AJAX请求

15、使用Promise封装AJAX请求 promise就有reject和resolve了&#xff0c;就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...

怎么让Comfyui导出的图像不包含工作流信息,

为了数据安全&#xff0c;让Comfyui导出的图像不包含工作流信息&#xff0c;导出的图像就不会拖到comfyui中加载出来工作流。 ComfyUI的目录下node.py 直接移除 pnginfo&#xff08;推荐&#xff09;​​ 在 save_images 方法中&#xff0c;​​删除或注释掉所有与 metadata …...