LDAP基本使用

270 阅读10分钟

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)中的一些必填属性和可选属性。

image.png

下面是一个典型的 LDAP 目录树结构,其中每个节点表示一个条目。在下一节中,我们将按照这个结构来配置一个简单的 LDAP 服务。

image.png

OpenLdap服务器的安装

详见 juejin.cn/post/722112…

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

项目:gitee.com/FanGaoXS/sp…

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 示例。

image.png

# 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抽象型对象类。以常用的人员类型为例,他们的继承关系:

image.png

常见错误排查

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

image.png