LDAP 基础教程
LDAP全称轻量级目录访问协议(英文:Lightweight Directory Access Protocol),是一个运行在 TCP/IP 上的目录访问协议。目录是一个特殊的数据库,它的数据经常被查询,但是不经常更新。其专门针对读取、浏览和搜索操作进行了特定的优化。目录一般用来包含描述性的,基于属性的信息并支持精细复杂的过滤能力。比如 DNS 协议便是一种最被广泛使用的目录服务。
LDAP 中的信息按照目录信息树结构组织,树中的一个节点称之为条目(Entry),条目包含了该节点的属性及属性值。条目都可以通过识别名 dn 来全局的唯一确定1,可以类比于关系型数据库中的主键。比如 dn 为 uid=ada,ou=People,dc=xinhua,dc=org
的条目表示在组织中一个名字叫做 Ada Catherine 的员工,其中 uid=ada
也被称作相对区别名 rdn。
一个条目的属性通过 LDAP 元数据模型(Scheme)中的对象类(objectClass)所定义,下面的表格列举了对象类 inetOrgPerson(Internet Organizational Person)中的一些必填属性和可选属性。
下面是一个典型的 LDAP 目录树结构,其中每个节点表示一个条目。在下一节中,我们将按照这个结构来配置一个简单的 LDAP 服务。
OpenLdap服务器的安装
springboot整合IDAP
基本使用
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>
配置文件
spring.ldap.urls=ldap://192.168.67.128:389
spring.ldap.username=cn=admin,o=ywyy1,dc=langchao,dc=com
spring.ldap.password=123456
spring.ldap.base=dc=langchao,dc=com
实体类
package com.wzwl.ldap.entity;
import javax.naming.Name;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ldap.odm.annotations.Attribute;
import org.springframework.ldap.odm.annotations.DnAttribute;
import org.springframework.ldap.odm.annotations.Entry;
import org.springframework.ldap.odm.annotations.Id;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entry(base = "ou=people,o=ywyy1", objectClasses = {"inetOrgPerson","posixAccount","shadowAccount"})
public class Person {
@Id
private Name id;
@DnAttribute(value = "uid", index = 1)
private String uid;
@Attribute(name = "cn")
private String commonName;
@Attribute(name = "sn")
private String suerName;
@Attribute(name = "userPassword")
private String userPassword;
@Attribute(name = "uidNumber")
private Integer uidNumber;
@Attribute(name = "gidNumber")
private Integer gidNumber;
@Attribute(name = "homeDirectory")
private String homeDirectory;
@Attribute(name = "loginShell")
private String loginShell;
}
service
package com.wzwl.ldap.service;
import javax.naming.Name;
import com.wzwl.ldap.entity.Person;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PersonRepository extends CrudRepository<Person, Name> {
}
test
package com.wzwl.ldap;
import com.wzwl.ldap.entity.Person;
import com.wzwl.ldap.service.PersonRepository;
import com.wzwl.ldap.service.RoleRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @author Riky Li
* @create 2018-03-20 10:33:18
* @desciption:
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class LdapPersonServiceTest {
@Autowired
private PersonRepository personRepository;
@Autowired
private RoleRepository roleRepository;
@Test
public void findAll() {
personRepository.findAll().forEach(p -> {
System.out.println(p);
});
}
@Test
public void testAddUser() {
Person person = Person.builder()
.uid("cas1")
.commonName("cas Catherine")
.suerName("Catherine")
.gidNumber(1000)
.uidNumber(1000)
.userPassword("123456")
.homeDirectory("/home/users/cas")
.loginShell("/bin/bash")
.build();
personRepository.save(person);
}
@Test
public void findRoles() {
roleRepository.findAll().forEach(p -> {
System.out.println(p);
});
}
}
实现增删改查,用户验证
servie层
package com.example.idapoperation.service;
import com.example.idapoperation.model.LdapUser;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public interface ILdapService {
/**
* LDAP用户认证
*/
boolean authenticate(String loginName, String password);
/**
* 检索域用户
*/
List<LdapUser> searchLdapUser(String keyword);
/**
* 修改密码
*/
void resetPwd(String loginName, String newPassword) throws Exception;
/**
* 新增用户
*
* @param user
* @return
*/
void addUser(LdapUser user) throws Exception;
/**
* 根据cn删除
*
* @param cn
*/
void deleteUser(String cn);
/**
* 根据cn修改用户属性
* @param cn
*/
void updateUserAttr(String cn);
/**
* 查询所有
* @return
*/
List<LdapUser> getAll();
}
impl层
package com.example.idapoperation.service.impl;
import com.example.idapoperation.common.Constant;
import com.example.idapoperation.model.LdapUser;
import com.example.idapoperation.service.ILdapService;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.query.LdapQuery;
import org.springframework.ldap.support.LdapNameBuilder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.naming.directory.*;
import java.util.ArrayList;
import java.util.List;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
@Service("ILdapService")
public class LdapServiceImpl implements ILdapService {
@Resource
private LdapTemplate ldapTemplate;
/**
* LDAP用户认证
*
* @param loginName
* @param password
* @return
*/
@Override
public boolean authenticate(String loginName, String password) {
EqualsFilter filter = new EqualsFilter("sAMAccountName", loginName);
return ldapTemplate.authenticate("", filter.toString(), password);
}
/**
* 检索域用户(根据用户登录名、正式名称、邮箱,模糊搜索)
* <p>
* sAMAccountName:用户登录名(Windows 2000以前版本,以后的版本是userPrincipalName)
* sn:姓;cn:正式名称;mail:邮箱;ou:组织单位;dn:识别名;
*
* @param keyword
* @return
*/
@Override
public List<LdapUser> searchLdapUser(String keyword) {
keyword = "*" + keyword + "*";
LdapQuery query = query().where("sAMAccountName").like(keyword).or("cn").like(keyword).or("mail").like(keyword);
return ldapTemplate.find(query, LdapUser.class);
}
/**
* 重置密码
*
* @param loginName
* @param newPassword
* @throws Exception
*/
@Override
public void resetPwd(String loginName, String newPassword) throws Exception {
// 1. 查找AD用户
LdapQuery query = query().where("sAMAccountName").is(loginName);
LdapUser person = ldapTemplate.findOne(query, LdapUser.class);
// 2. 创建密码
String newQuotedPassword = "\"" + newPassword + "\"";
byte[] newUnicodePassword = newQuotedPassword.getBytes("UTF-16LE");
// 3. 修改密码
ModificationItem item = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("unicodePwd", newUnicodePassword));
ldapTemplate.modifyAttributes(person.getDn(), new ModificationItem[]{item});
}
/**
* 新增用户
*
* @param user
* @return
*/
@Override
public void addUser(LdapUser user) {
// 基类设置
BasicAttribute ocattr = new BasicAttribute("objectClass");
ocattr.add("top");
ocattr.add("person");
ocattr.add("organizationalPerson");
ocattr.add("user");
// 用户属性
Attributes attrs = new BasicAttributes();
attrs.put(ocattr);
//用户登录名
attrs.put("userPrincipalName", user.getLoginName());
//用户登录名 windows 2000以前的版本
attrs.put("sAMAccountName", user.getLoginName());
//用户正式名
attrs.put("cn", user.getUserName());
//姓
attrs.put("sn", user.getSn());
//名
attrs.put("givenname", user.getGivenName());
//显示名称
attrs.put("displayName", user.getDisplayName());
//邮件
attrs.put("mail", user.getEmail());
//下次登录修改密码
attrs.put("pwdLastSet", "0");
//启用账户
attrs.put("userAccountControl","544");
//密码
attrs.put("userPassword", Constant.DEFAULT_PWD);
ldapTemplate.bind(LdapNameBuilder.newInstance().add("CN", user.getUserName()).build(), null, attrs);
}
/**
* 删除用户
*
* @param cn
*/
@Override
public void deleteUser(String cn) {
ldapTemplate.unbind(LdapNameBuilder.newInstance().add("CN", cn).build());
}
@Override
public List<LdapUser> getAll() {
return ldapTemplate.findAll(LdapUser.class);
}
}
测试controller
package com.example.idapoperation.controller;
import com.example.idapoperation.common.MyException;
import com.example.idapoperation.common.Result;
import com.example.idapoperation.enums.ExceptionEnum;
import com.example.idapoperation.model.LdapUser;
import com.example.idapoperation.service.ILdapService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequestMapping("/idap")
@Api(tags = "AD域管理")
public class IdapController {
@Autowired
private ILdapService iLdapService;
@ApiOperation("用户认证")
@PostMapping("/authUser")
@ResponseBody
@ApiImplicitParams({
@ApiImplicitParam(name = "userName", value = "账号", required = true, dataType = "String"),
@ApiImplicitParam(name = "password", value = "密码", required = true, dataType = "String")
})
public Result authUser(@RequestParam String userName, @RequestParam String password) {
Boolean flag = iLdapService.authenticate(userName, password);
if (!flag) {
throw new MyException(ExceptionEnum.AUTH_ERROR.getCode(), ExceptionEnum.AUTH_ERROR.getMsg());
}
return Result.success();
}
@ApiOperation("根据用户名、正式名称、邮箱检索用户")
@PostMapping("/searchUser")
@ResponseBody
@ApiImplicitParams({
@ApiImplicitParam(name = "keyword", value = "参数可以是用户名、正式名称、邮箱都可以", required = true, dataType = "String")
})
public Result searchUser(@RequestParam String keyword) {
List<LdapUser> list = iLdapService.searchLdapUser(keyword);
return Result.success(list);
}
@ApiOperation("重置密码")
@PostMapping("/resetPwd")
@ResponseBody
@ApiImplicitParams({
@ApiImplicitParam(name = "loginName", value = "用户登录名", required = true, dataType = "String"),
@ApiImplicitParam(name = "newPwd", value = "新密码", required = true, dataType = "String")
})
public Result resetPwd(@RequestParam String loginName, @RequestParam String newPwd) {
try {
iLdapService.resetPwd(loginName, newPwd);
} catch (Exception e) {
throw new MyException(ExceptionEnum.RESETPWD_ERROR.getCode(), ExceptionEnum.RESETPWD_ERROR.getMsg());
}
return Result.success();
}
@ApiOperation("新增用户")
@PostMapping("/addUser")
@ResponseBody
public Result addUser(@RequestBody LdapUser ldapUser) {
try {
iLdapService.addUser(ldapUser);
// iLdapService.updateUserAttr(ldapUser.getUserName());
} catch (Exception e) {
e.printStackTrace();
throw new MyException(ExceptionEnum.ADD_ERROR.getCode(), ExceptionEnum.ADD_ERROR.getMsg());
}
return Result.success();
}
/**
* @param cn
* @return
*/
@ApiOperation("删除用户")
@PostMapping("/deleteUser")
@ResponseBody
@ApiImplicitParams({
@ApiImplicitParam(name = "cn", value = "工号", required = true, dataType = "String")
})
public Result deleteUser(@RequestParam(name = "cn") String cn) {
try {
iLdapService.deleteUser(cn);
} catch (Exception e) {
e.printStackTrace();
throw new MyException(ExceptionEnum.DELETE_ERROR.getCode(), ExceptionEnum.DELETE_ERROR.getMsg());
}
return Result.success();
}
@ApiOperation("查询所有用户")
@PostMapping("/findAll")
@ResponseBody
public Result findAll() {
List<LdapUser> list = iLdapService.getAll();
return Result.success(list);
}
LDAP的acl控制
ACL 权限控制
针对 LDAP 需要多管理员模式,进行ACL 权限控制。
编写配置文件
修改已经存在的 LDAP 配置文件加入 ACL 控制
cat acl.ldif
dn: olcDatabase={2}hdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: to attrs=userPassword
by dn="cn=manager,dc=magic,dc=com" write
by dn.children="ou=managers,dc=magic,dc=com" write
by anonymous auth
by self write
by * none
olcAccess: to *
by dn="cn=manager,dc=magic,dc=com" write
by dn.children="ou=managers,dc=magic,dc=com" write
by * read
执行更新命令
ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f acl.ldif
配置解释
- access to attrs=userPassword通过属性找到访问范围密码
- 超级管理员也就是我们ldap配置文件里写的ootdn:"cn=manager,dc=magic,dc=com"有写(write)权限
- 管理员可能不止一个,创建管理员组"ou=managers,dc=magic,dc=com"把管理员统一都放到这个组下,管理员组下的所有用户(dn.children)有写权限
- 匿名用户(anonymous)要通过验证(auth)
- 自己(self)有对自己密码的写(write)权限,其他人(*)都没有权限(none)
- access to * 所有其他属性
- 超级管理员rootdn:"cn=manager,dc=magic,dc=com"有写(write)权限
- 管理员"ou=managers,dc=magic,dc=com"成员有写(write)权限;
- 其他人(*)只有读(read)权限
示例
正常情况下,登录一个 Ldap 用户,可以看到整个树形结构。但在一些情形下,我们希望如 dc=ali,dc=example,dc=com 仅能够被 “ali” 这个节点下的用户看到,而“dc=ali” 这个节点下的用户又仅能访问本节点下的信息,而无法看到如 dc=huawei 节点下的信息。即使 dc=baidu, dc=ali, dc=huawei 之间具有隔离性,各个公司仅能访问自己公司的节点下的 subtree, 而其他公司无法访问或是被访问。 因此要使用 Ldap 中的 ACL 进行控制,在此给出该情形的 ACL 示例。
# acl.ldif 内容
dn: olcDatabase={2}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: to attrs=userPassword
by dn="cn=admin,dc=example,dc=com" write
by anonymous auth
by self write
by * none
olcAccess: to dn.regex="^cn=system_user,ou=system_user"
by dn="cn=admin,dc=example,dc=com" write
by * read
olcAccess: to dn.regex="dc=([^,]+),dc=example,dc=com$"
by dn.regex="cn=system_user,ou=system_user,dc=$1,dc=example,dc=com" write
by dn.regex="dc=$1,dc=example,dc=com$" read
by * none
olcAccess: to *
by dn="cn=admin,dc=example,dc=com" write
by anonymous auth
by self write
by * read
acl.ldif 内容解读:
- 内容中第一个 olcAccess 设置的密码规则;
- 第二个 olcAccess 设置的各个公司下的 cn=system_user,ou=system_user…不可更改 ,仅超管可改;
- 第三个 olcAccess 设置的各个公司用户隔离,仅看到本公司下的节点;
- 第四个 olcAccess 设置的所有属性的可见性,匿名用户隔离;
最后通过 ldapmodify 指令讲 acl 控制加载到 ldap 配置中
ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f acl.ldif
Object Classes解释说明
必备知识:
- C=Country为国家名,可选,为2个字符长;
- DC=Domain Component,主要元素;
- O=Organization 为组织名,可以3—64个字符长;
- OU=Organization Unit为组织单元,最多可以有四级,每级最长32个字符,可以为中文;
- CN=Common Name 为用户名或服务器名,最长可以到80个字符,可以为中文;
目录 必备知识:
- 1、账号:account
- 2、别名:alias
- 3、应用程序实体:applicationEntity
- 4、一个应用程序的进程:applicationProcess
- 5、设备启动参数:bootableDevice
- 6、一个证书颁发机构:certificationAuthority
- 7、国家:country
- 8、区域: dcObject
- 9、设备:device
- 10、dmd
- 11、文档:document
- 12、文档服务: documentSeries
- 13、区域: domain
- 14、对象相关的一个领域: domainRelatedObject
- 15、一个目录系统代理(服务器):dSA
- 16、有好的国家: friendlyCountry
- 17、一群名称(DNs): groupOfNames
- 18、一组唯一的名称(DN和惟一标识符): groupOfUniqueNames
- 19、一个设备的MAC地址: ieee802Device
- 20、互联网组织的人: inetOrgPerson
- 21、主机设备: ipHost
- 22、ip地址: ipNetwork
- 23、ip协议: ipProtocol
- 24、互联网服务: ipService
- 25、包含URI属性类型的对象: labeledURIObject
- 26、位置: locality
- 27、一个组织: organization
- 28、组织者: organizationalPerson
- 29、 组织角色: organizationalRole
- 30、组织单元: organizationalUnit
- 31、人员: organizationalUnit
- 32、可移植操作系统接口账户:: posixAccount
- 33、可移植操作系统接口账户分组: posixGroup
- 34、质量标签数据: qualityLabelledData
- 35、人员的住宅信息: residentialPerson
- 36、房间: room
- 37、阴影口令: shadowAccount
- 38、简单的安全对象: simpleSecurityObject
- 39、简单的安全对象: strongAuthenticationUser
- 40、分项: subentry
- 41、子条目: subschema
- 42、uid对象: uidObject
- 43、用户安全信息: uidObject
更多常见objectClass www.dgrt.cn/news/show-5…
objectClass 介绍
LDAP中,一个条目必须包含一个objectClass属性,且需要赋予至少一个值。每一个值将用作一条LDAP条目进行数据存储的模板;模板中包含了一个条目必须被赋值的属性和可选的属性。
objectClass有着严格的等级之分,最顶层是top和alias。例如,organizationalPerson这个objectClass就隶属于person,而person又隶属于top。
objectClass可分为以下3类:
- 结构型(Structural):如person和organizationUnit;
- 辅助型(Auxiliary):如extensibeObject;
- 抽象型(Abstract):如top,抽象型的objectClass不能直接使用。
通过对象类可以方便的定义条目类型。每个条目可以直接继承多个对象类,这样就继承了各种属性。如果2个对象类中有相同的属性,则条目继承后只会保留1个属性。对象类同时也规定了哪些属性是基本信息,必须含有(Must 活Required,必要属性):哪些属性是扩展信息,可以含有(May或Optional,可选属性)。
解释
- 对象类有三种类型:结构类型(Structural)、抽象类型(Abstract)和辅助类型(Auxiliary)。结构类型是最基本的类型,它规定了对象实体的基本属性,每个条目属于且仅属于一个结构型对象类。抽象类型可以是结构类型或其他抽象类型父类,它将对象属性中共性的部分组织在一起,称为其他类的模板,条目不能直接集成抽象型对象类。辅助类型规定了对象实体的扩展属性。每个条目至少有一个结构性对象类。
- 对象类本身是可以相互继承的,所以对象类的根类是top抽象型对象类。以常用的人员类型为例,他们的继承关系:
常见错误排查
LDAP65错误
这个错误是因为实体类字段必须和objectClasses规定的一直,不能缺少必要属性。
LDAP: error code 32 - No Such Object
ldap上下文里面配置的base是不应该再加到节点的dn里面去的
例如 dn是o=sf,dc=aa,dc=com ldap:context-source的base配置的是dc=aa,dc=com Entry里面的base就只要配置o=sf就可以了,不能再配置成o=sf,dc=aa,dc=com