【LDAP】Spring项目同步LDAP域用户信息总览(含ldapTemplate.search仅查询1000条数据的解决方案)
一、LDAP简介
- 维基百科介绍:
轻量级目录访问协议( LDAP) 是一种开放的、供应商中立的行业标准应用协议,用于通过Internet 协议(IP) 网络访问和维护分布式目录信息服务。目录服务允许在整个网络中共享有关用户、系统、网络、服务和应用程序的信息,因此在开发Intranet和 Internet 应用程序中发挥着重要作用。例如,目录服务可以提供任何有组织的记录集,通常具有分层结构,例如公司电子邮件目录。类似地,电话簿也是具有地址和电话号码的用户列表。
LDAP广泛应用于组织中的身份管理、访问控制、电子邮件系统、单点登录等领域,提供了一种高效、灵活的方式来组织和检索分布式信息。
二、关于LDAP的一些关键概念
-
目录服务(Directory Service): LDAP主要用于访问目录服务,而目录服务是一种层次结构化的数据库,用于存储和检索有关组织中各种实体(如用户、组、设备)的信息。
-
层次结构(Hierarchy): LDAP目录数据以层次结构的形式组织,类似于文件系统的树状结构。每个目录项(entry)都有一个唯一的标识符(Distinguished Name,DN),表示在层次结构中的位置。
-
条目(Entry): 目录中的每个实体都被称为一个条目。每个条目包含一组属性-值对,描述了实体的特征和属性。
-
属性(Attribute): 属性是条目的特定信息,例如用户的姓名、电子邮件地址等。每个属性都有一个唯一的标识符(Attribute Type),对应着属性值。
-
LDAP协议: LDAP定义了一套客户端和服务器之间进行通信的规范,以实现对目录服务的操作。常见的LDAP操作包括搜索、添加、删除、修改等。
-
端口: LDAP通常使用TCP协议,标准端口号是389。同时,LDAP也可以通过安全套接字层(SSL)进行加密通信,使用的端口号是636。
-
认证: LDAP支持基于用户名和密码的身份验证,确保只有授权用户能够访问和修改目录信息。
三、LDAP用户信息案例
以下是一个简单的LDAP用户信息的例子:
- Distinguished Name (DN):
uid=johndoe,ou=people,dc=example,dc=com
- 表示该用户在LDAP层次结构中的唯一标识符。
- Attributes (属性):
- ‘uid’: johndoe
- 用户的唯一标识符,通常是用户名。
- ‘cn’:John Doe
- 用户的通用名称,即用户的全名。
- ‘sn’: Doe
- 用户的姓氏。
- ‘givenName’: John
- 用户的名字。
- ‘mail’: johndoe@example.com
- 用户的电子邮件地址。
- ‘userPassword’: {SHA}5en6G6MezRroT3XKqkdPOmY/BfQ=
- 用户的密码,通常以加密形式存储。
这是一个基本的LDAP用户信息示例,其中包含了用户的基本身份信息、电子邮件地址以及密码。这样的信息可以用于实现身份验证、访问控制等功能。在实际应用中,用户信息的属性和结构可能会根据组织的需求而有所不同。
四、Java同步LDAP用户信息到MySQL
1、引入Maven依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>
2、LDAP认证信息配置
注:LDAP 389端口和3268的区别
LDAP(Lightweight Directory Access Protocol)通常使用两个不同的端口进行通信,分别是389和3268。它们之间的主要区别在于目标服务器和所执行的操作。
- LDAP端口 389:
- 用途: 389端口是LDAP的标准端口,主要用于非安全的LDAP通信。
- 目标服务器: 通常用于与单个LDAP服务器建立连接,执行标准的LDAP操作。
- 操作类型: 适用于基本的LDAP查询、添加、修改、删除等操作。
- 加密: 数据在传输过程中不加密,可能存在安全风险。
- LDAP端口 3268:
- 用途: 3268端口也是LDAP端口,但用于LDAP全局编录服务。
- 目标服务器: 用于在整个Active Directory森林中搜索(查询)用户信息。
- 操作类型: 主要用于执行LDAP搜索操作,支持在多个域控制器之间进行全局搜索。
- 加密: 支持加密通信,提供更安全的数据传输。
总结:
- 使用389端口时,连接的是特定的LDAP服务器,适用于单个域或单个目录树。
- 使用3268端口时,连接的是全局编录服务,适用于在整个Active Directory森林中进行全局搜索,跨越多个域。
由于当前案例为全局检索用户信息,故使用 3268 端口
# ldap 连接信息ldap:urls: ldap://10.*.*.*:3268username: *********password: *********
3、用户实体
用于将检索到的用户信息映射为Java实例对象
import lombok.Data;@Data
public class LdapUser {/*** 唯一标识(code)*/private String code;/*** 用户名(account)*/private String account;/*** 昵称(name)*/private String name;/*** 真名(realName)*/private String realName;/*** 用户邮箱(email)*/private String email;/*** 手机号*/private String phone;}
4、同步功能代码
package org.springblade.modules.system.timing;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.common.cache.SysCache;
import org.springblade.common.constant.DictBizConstant;
import org.springblade.common.constant.TenantConstant;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tenant.BladeTenantProperties;
import org.springblade.core.tool.constant.BladeConstant;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.DesUtil;
import org.springblade.core.tool.utils.DigestUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.modules.system.entity.Tenant;
import org.springblade.modules.system.entity.User;
import org.springblade.modules.system.ldap.entity.LdapUser;
import org.springblade.modules.system.service.impl.UserServiceImpl;
import org.springframework.ldap.control.PagedResultsDirContextProcessor;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.query.LdapQuery;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.naming.directory.SearchControls;
import java.util.ArrayList;
import java.util.List;import static org.springblade.common.constant.DictBizConstant.*;
import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE;
import static org.springframework.ldap.query.LdapQueryBuilder.query;/*** LDAP用户信息同步定时任务*/
@Component
@AllArgsConstructor
@Slf4j
public class GotionUserTask {private final UserServiceImpl userService;private final LdapTemplate ldapTemplate;/*** 每日凌晨执行一次*/@Scheduled(cron = "0 0 0 * * ?")public void userSyncTask() {List<LdapUser> allLdapUsers = this.queryUsersPage();// 所有用户唯一值for (LdapUser ldapUser : allLdapUsers) {if (!"null".equals(ldapUser.getAccount()) && ldapUser.getAccount() != null){CacheUtil.clear(USER_CACHE);// 将 LdapUser 转换为 UserUser user = this.ldapUserToSystemUser(ldapUser);if (StringUtil.isBlank(user.getTenantId())) {user.setTenantId(BladeConstant.ADMIN_TENANT_ID);}Long userCount = userService.getBaseMapper().selectCount(Wrappers.<User>query().lambda().eq(User::getTenantId, user.getTenantId()).eq(User::getAccount, user.getAccount()));if (userCount > 0L && Func.isEmpty(user.getId())) {// 若当前用户已存在,则更新用户信息User oldUser = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getAccount, user.getAccount()));oldUser.setCode(ldapUser.getCode());oldUser.setName(ldapUser.getName());oldUser.setRealName(!"null".equals(ldapUser.getRealName()) ? ldapUser.getRealName() : null);oldUser.setEmail(!"null".equals(ldapUser.getEmail()) ? ldapUser.getEmail() : null);oldUser.setPhone(!"null".equals(ldapUser.getPhone()) ? ldapUser.getPhone() : null);userService.updateUser(oldUser);} else {userService.submit(user);}}}}/*** 分页查询LDAP中的所有的用户息** @return 结果*/public List<LdapUser> queryUsersPage() {List<LdapUser> list = new ArrayList<>();SearchControls searchControls = new SearchControls();/** 0:OBJECT_SCOPE,搜索指定的命名对象。* 1:ONE LEVEL_SCOPE,只搜索指定命名对象的一个级别,这是缺省值。* 2:SUBTREE_SCOPE,搜索以指定命名对象为根结点的整棵树*/searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);// 每次查询条数:默认1000条PagedResultsDirContextProcessor processor = new PagedResultsDirContextProcessor(1000);//返回的参数// 映射对象AttributesMapper<LdapUser> CN_ATTRIBUTES_MAPPER = attributes -> {LdapUser ldapUser = new LdapUser();ldapUser.setCode(String.valueOf(attributes.get("distinguishedName") != null ? attributes.get("distinguishedName").get() : null));ldapUser.setAccount(String.valueOf(attributes.get("sAMAccountName") != null ? attributes.get("sAMAccountName").get() : null));ldapUser.setName(String.valueOf(attributes.get("cn") != null ? attributes.get("cn").get() : null));ldapUser.setRealName(String.valueOf(attributes.get("name") != null ? attributes.get("name").get() : null));ldapUser.setEmail(String.valueOf(attributes.get("mail") != null ? attributes.get("mail").get() : null));ldapUser.setPhone(String.valueOf(attributes.get("telephoneNumber") != null ? attributes.get("telephoneNumber").get() : null));return ldapUser;};//查询条件LdapQuery queryPerson = query().base(DictBizConstant.GOTION_CODE_PUBLIC_SIGNS).where("objectClass").is("person");do {List<LdapUser> search = ldapTemplate.search(queryPerson.base(),queryPerson.filter().encode(),searchControls,CN_ATTRIBUTES_MAPPER,processor);list.addAll(search);} while (processor.hasMore());return list;}/**将映射的LDAP用户对象 转换为 当前系统的用户对象*/public User ldapUserToSystemUser(LdapUser ldapUser) {User user = new User();user.setCode(!"null".equals(ldapUser.getCode()) ? ldapUser.getCode() : null);user.setUserType(GOTION_DEF_USER_TYPE); // 默认用户类型user.setAccount(ldapUser.getAccount()); // 登录名称(用户在当前系统的登录名,应为)user.setPassword(GOTION_DEF_PWD); // 默认密码user.setName(ldapUser.getName());user.setRealName(!"null".equals(ldapUser.getRealName()) ? ldapUser.getRealName() : null);user.setAvatar(null);user.setEmail(!"null".equals(ldapUser.getEmail()) ? ldapUser.getEmail() : null);user.setPhone(!"null".equals(ldapUser.getPhone()) ? ldapUser.getPhone() : null);user.setSex(null);user.setRoleId(GOTION_DEF_ROLE); // 默认权限user.setDeptId(GOTION_DEF_DEPT); // 默认部门user.setPostId(GOTION_DEF_POST); // 默认岗位return user;}}
5、结果
LDAP中的用户信息:
MySQL中的用户信息:
Over!
加粗样式