Spring框架集成Ldap

一、概念介绍

1.1 LDAP

LDAP(轻型目录访问协议)是一种用于访问和维护分布式目录服务的开放标准协议。LDAP最初是从X.500标准中派生出来的,但相比于X.500,LDAP更加简化和灵活。LDAP协议定义了客户端和服务器之间进行通信的规范,提供了一种在网络上访问和管理分布式目录数据的方式。

LDAP主要有以下几个概念

  1. 目录服务:目录服务是一种存储和组织数据的系统,类似于数据库,但它更适合于存储和检索大量的层次结构数据。目录服务通常用于存储组织结构信息、用户身份验证和授权等数据。
  2. 目录项:目录项是LDAP中存储的基本单位,每个目录项具有一个唯一的标识符(通常是一个全局唯一的Distinguished Name),并包含一组属性-值对。
  3. Distinguished Name(DN):Distinguished Name是一个目录项的完整路径标识符,由多个RDN(Relative Distinguished Name)组成,每个RDN由属性-值对构成。DN用于在目录中唯一标识一个目录项。
  4. 属性:LDAP目录项可以包含多个属性,每个属性由属性名和对应的值构成。属性名通常是标准化的OID(Object Identifier),比如"cn"表示通用名称、"sn"表示姓氏。
  5. 基准线(Base DN):基准线是LDAP服务器上搜索和操作目录项的起始位置,它指定了在哪个部分的目录层次结构下进行操作。
  6. 绑定(Binding):绑定是指客户端与LDAP服务器建立连接并验证身份的过程,以便进行后续操作。客户端需要提供有效的用户名和密码来进行绑定。
  7. 过滤器(Filter):过滤器用于对目录中的条目进行筛选和搜索,以满足特定的查询条件。常见的过滤器操作符包括等于、大于、小于、与、或、非等。

LDAP在企业环境中广泛应用,特别用于管理用户、组织结构和资源访问控制等信息。通过LDAP,可以实现统一的身份认证和授权管理,提供了集中化的目录服务,方便用户和应用程序访问和检索目录数据。

1.2 AD

1.2.1 AD定义

AD是Active Directory的缩写,AD是LDAP的一个应用实例,而不应该是LDAP本身。比如:windows域控的用户、权限管理应该是微软公司使用LDAP存储了一些数据来解决域控这个具体问题,只是AD顺便还提供了用户接口,也可以利用ActiveDirectory当做LDAP服务器存放一些自己的东西而已。比如LDAP是关系型数据库,微软自己在库中建立了几个表,每个表都定义好了字段。显然这些表和字段都是根据微软自己的需求定制的,而不是LDAP协议的规定。然后微软将LDAP做了一些封装接口,用户可以利用这些接口写程序操作LDAP,使得ActiveDirectory也成了一个LDAP服务器。

1.2.2 特点
  1. 组织架构管理:AD以层次结构的方式组织和管理目录数据。AD中的顶层单位是域(Domain),每个域都可以包含多个组织单位(OU)。这种层次结构可以方便地对资源和用户进行组织、分类和授权。
  2. 用户身份验证和授权:AD用于存储和验证用户的身份信息,并提供了灵活的权限控制机制。通过AD,可以实现对用户访问资源的授权、密码策略的管理以及单点登录等功能。
  3. 安全性和访问控制:AD提供了细粒度的访问控制功能,可以根据用户、组、组织单位等进行权限控制。管理员可以定义不同的安全策略和访问权限,以保护敏感数据和资源。
  4. 集中化管理:AD提供了集中化管理工具,如Active Directory Users and Computers(ADUC),使管理员可以方便地管理用户、组、计算机、策略等。通过这些工具,管理员可以轻松地创建、修改和删除目录对象。
  5. 多域和信任关系:AD支持多域环境,不同的域可以建立信任关系,实现域之间的资源共享和访问。这样,用户可以从一个域访问其他域中的资源,实现跨域认证和授权。
  6. 扩展性和复制:AD具有高度的可扩展性,可以根据需要添加或删除域控制器来适应组织的发展和变化。AD使用复制机制来保持域控制器之间的数据一致性,确保在整个网络中的目录数据是同步的。

总而言之,AD是一种强大的目录服务,广泛用于企业网络环境中,提供统一身份认证、访问控制和资源管理功能,并通过LDAP协议与客户端进行交互。它简化了用户和资源管理的过程,提供了高级的安全性和可扩展性,是企业网络的核心基础设施之一。

1.2.3 应用

  1. 用户服务:管理用户的域账号、用户信息、企业通信录(与电子邮箱系统集成)、用户组管理、用户身份认证、用户授权管理、按需实施组管理策略等。这里不单单指某些线上的应用更多的是指真实的计算机,服务器等
  2. 计算机管理:管理服务器及客户端计算机账户、所有服务器及客户端计算机加入域管理并按需实施组策略。
  3. 资源管理:管理打印机、文件共享服务、网络资源等实施组策略。
  4. 应用系统的支持:对于电子邮件(Exchange)、在线及时通讯(Lync)、企业信息管理(SharePoint)、微软CRM,ERP等业务系统提供数据认证(身份认证、数据集成、组织规则等)。这里不单是微软产品的集成,其它的业务系统根据公用接口的方式一样可以嵌入进来。
  5. 客户端桌面管理:系统管理员可以集中的配置各种桌面配置策略,如:用户适用域中资源权限限制、界面功能的限制、应用程序执行特征的限制、网络连接限制、安全配置限制等。

二、集成

1. 引入maven依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>

2. 配置连接

#数据源配置
spring:
  # ldap 连接信息
  ldap:
    urls:
      - ldap://192.168.1.1:3268
    #用于配置连接LDAP服务器时的用户名,通常是用于管理员权限的用户名。
    username: cn=???,cn=Users,dc=???,dc=com
    #用于配置连接LDAP服务器时的密码,与用户名相对应。
    password: 123456
    # #配置LDAP搜索的基础目录,所有的搜索操作都将在此基础目录下进行
    base: dc=???,dc=com

3. 配置类

@Configuration
public class LdapConfig {
    @Bean
    @ConditionalOnMissingBean(LdapOperations.class)
    public LdapTemplate ldapTemplate(ContextSource contextSource){
        LdapTemplate ldapTemplate = new LdapTemplate(contextSource);
        ldapTemplate.setIgnorePartialResultException(true);
        return ldapTemplate;
    }
}

4. 配置LdapPersonAttributeMapper

用于查询结果转换实体类

public class LdapPersonAttributeMapper implements AttributesMapper<Person> {
    @Override
    public Person mapFromAttributes(Attributes attributes) throws NamingException, javax.naming.NamingException {
        Person person = new Person();
        person.setName(attributes.get("cn").get().toString());
        person.setAccount(attributes.get("sAMAccountName").get().toString());
        person.setEmail(attributes.get("mail").get().toString());
        person.setStatus(attributes.get("userAccountControl").get().toString());
        person.setDn(LdapUtils.newLdapName(attributes.get("distinguishedName").get().toString()));
        return person;
    }
}

用户实体类

@Data
@Entry(objectClasses = "person")
public class Person {
    @Id
    @JsonSerialize(using = ToStringSerializer.class)
    private Name dn;

    @Attribute(name = "cn")
    private String name;

    @Attribute(name = "sAMAccountName")
    private String account;

    @Attribute(name = "mail")
    private String email;

    @Attribute(name = "userAccountControl")
    protected String status;

    @Attribute(name = "givenName")
    private String givenName;

    @Attribute(name = "sn")
    private String lastName;

    @Attribute(name = "telephoneNumber")
    private String telephoneNumber;

    @Attribute(name = "userPrincipalName")
    private String userPrincipalName;
}

5. ldap操作实现类

@Slf4j
@Service
@AllArgsConstructor
public class LdapService {

    private final LdapTemplate ldapTemplate;
    public final static String USER_LDAP_NOT_AUTH_PASS = "LDAP认证失败,请联系管理员";
    public final static String USER_LDAP_NOT_VALUE = "登录账号或密码不能为空";
    public final static int ACCOUNT_DISABLE = 0x0001 << 1; // 账户已禁用
    public final static int LOCKOUT = 0x0001 << 4; // 账户已锁定
    public final static int PASSWD_NOTREQD = 0x0001 << 5; // 不需要密码
    public final static int PASSWD_CANT_CHANGE = 0x0001 << 6; // 用户不能更改密码(只读,不能修改)
    public final static int NORMAL_ACCOUNT = 0x0001 << 9; // 正常账户
    public final static int DONT_EXPIRE_PASSWORD = 0x0001 << 16; // 密码永不过期
    public final static int PASSWORD_EXPIRED = 0x0001 << 23; // 密码已过期

    /**
     * 账号认证
     * LDAP认证过程中出现的错误代码
     * 525	用户不存在
     * 52e	密码或凭证无效
     * 530	此时不允许登录
     * 531	在此工作站上不允许登录
     * 532	密码过期
     * 533	账户禁用
     * 701	账户过期
     * 773	用户必须重置密码
     * 775	用户账户锁定
     */
    public R auth(String account, String password) {
        if (Func.isBlank(account) || Func.isBlank(password)){
            throw new ServiceException(USER_LDAP_NOT_VALUE);
        }
        try {
            ldapTemplate.authenticate(LdapQueryBuilder.query().where("sAMAccountName").is(account), password);
        } catch (Exception e) {
            String message = e.getMessage();
            log.error(message);
            for (LdapAuthCodeEnum ldapAuthCodeEnum : LdapAuthCodeEnum.values()) {
                if (message.contains(ldapAuthCodeEnum.code)) {
                    return R.fail(ldapAuthCodeEnum.message);
                }
            }
            return R.fail(USER_LDAP_NOT_AUTH_PASS);
        }
        return R.status(Boolean.TRUE);
    }

    /**
     * 账号查找
     */
    public Person find1(String account) {
        Person person = ldapTemplate.findOne(LdapQueryBuilder.query().where("sAMAccountName").is(account)
                .and("objectClass").is("person"), Person.class);
        return person;
    }

    /**
     * 账号查找
     */
    public Person find2(String account) {
        List<Person> list = ldapTemplate.find(LdapQueryBuilder.query()
                .where("sAMAccountName").is(account)
                .and("objectClass").is("person"), Person.class);
        if (list == null || list.size() == 0) {
            return null;
        } else if (list.size() > 1) {
            throw new RuntimeException("匹配到多个用户");
        } else {
            return list.get(0);
        }
    }

    /**
     * 查询所有用户
     */
    public List<Person> queryAll1(String ou) {
        List<Person> list = ldapTemplate.find(LdapQueryBuilder.query().base("ou="+ou).where("objectClass").is("person"), Person.class);
        return list;
    }

    /**
     * 查找用户
     */
    public List<Person> queryAll2() {
        List<Person> list = ldapTemplate.search(LdapQueryBuilder.query().base("ou=测试集团").where("objectClass").is("person"), new LdapPersonAttributeMapper());
        return list;
    }

    /**
     * 分页查找用户
     */
    public List<Person> queryAllPage() {
        SearchControls controls = new SearchControls();
        controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
        controls.setReturningObjFlag(true);

        SearchExecutor executor = ctx -> ctx.search(LdapUtils.newLdapName("ou=测试集团"), "(&(objectClass=person))", controls);
        PagedResultsCookie cookie = null;
        PagedResultsDirContextProcessor requestControl;
        AttributesMapperCallbackHandler<Person> callbackHandler = new AttributesMapperCallbackHandler<>(new LdapPersonAttributeMapper());
        do {
            requestControl = new PagedResultsDirContextProcessor(500, cookie);
            ldapTemplate.search(executor, callbackHandler, requestControl);
            cookie = requestControl.getCookie();
        } while (requestControl.hasMore());
        return callbackHandler.getList();
    }

    /**
     * 增量查询
     */
    public List<Person> queryDiff1(Date startTime) {
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss.SZ");
        List<Person> list = ldapTemplate.find(LdapQueryBuilder.query().base("ou=测试集团").where("objectClass").is("person")
                .and("whenChanged").gte(format.format(startTime)), Person.class);
        return list;
    }

    /**
     * 增量查询
     */
    public List<Person> queryDiff2(Date startTime) {
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss.SZ");
        List<Person> list = ldapTemplate.search(LdapQueryBuilder.query().base("ou=测试集团").where("objectClass").is("person")
                .and("whenChanged").gte(format.format(startTime)), new LdapPersonAttributeMapper());
        return list;
    }

    /**
     * 添加ou
     */
    public boolean addOU(String name, String baseDn) {
        Attributes ouAttributes = new BasicAttributes();
        BasicAttribute ouBasicAttribute = new BasicAttribute("objectClass");
        ouBasicAttribute.add("organizationalUnit");
        ouAttributes.put(ouBasicAttribute);
        if (!StringUtils.hasText(baseDn)) {
            baseDn = "";
        }
        LdapName dn = LdapUtils.newLdapName(baseDn);
        try {
            dn.add(new Rdn("ou", name));
        } catch (InvalidNameException e) {
            log.error("{}", e.getMessage(), e);
            return false;
        }
        ldapTemplate.bind(dn, null, ouAttributes);
        return true;
    }

    /**
     * 删除ou
     */
    public boolean deleteOU(String dn, boolean recursive) {
        try {
            ldapTemplate.lookup(dn);
        } catch (NameNotFoundException e) {
            throw new RuntimeException("OU不存在");
        }
        ldapTemplate.unbind(dn, recursive);
        return true;
    }

    /**
     * 添加用户
     */
    public boolean addUser(String name, String account, String baseDn) {
        Attributes attrs = new BasicAttributes();
        BasicAttribute oc = new BasicAttribute("objectClass");
        oc.add("top");
        oc.add("person");
        oc.add("organizationalPerson");
        oc.add("user");
        attrs.put(oc);
        attrs.put("sAMAccountName", account);
        attrs.put("userPrincipalName", account+"@qq.com");
        attrs.put("cn", name);
        attrs.put("displayName", name);
        attrs.put("mail", account + "@qq.com");
        int status = NORMAL_ACCOUNT;
        attrs.put("userAccountControl", String.valueOf(status));
        String pwd = ""; // 初始密码

        attrs.put("unicodePwd", encodePwd(pwd));
        if (!StringUtils.hasText(baseDn)) {
            baseDn = "";
        }
        LdapName dn = LdapNameBuilder.newInstance(baseDn)
                .add("cn", name)
                .build();
        ldapTemplate.bind(dn, null, attrs);
        return true;
    }

    /**
     * 删除用户
     */
    public boolean deleteUser(String dn) {
        try {
            ldapTemplate.lookup(dn);
        } catch (NameNotFoundException e) {
            throw new RuntimeException("用户不存在");
        }
        ldapTemplate.unbind(dn);
        return true;
    }

    /**
     * 修改密码
     */
    public boolean updatePwd(String dn, String newPwd) {
        try {
            ldapTemplate.lookup(dn);
        } catch (NameNotFoundException e) {
            throw new RuntimeException("用户不存在");
        }
        ModificationItem[] mods = new ModificationItem[1];
        mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("unicodePwd", encodePwd(newPwd)));
        ldapTemplate.modifyAttributes(dn, mods);
        return true;
    }

    /**
     * 添加用户组
     */
    public boolean addGroup(String name, String baseDn) {
        Attributes groupAttributes=new BasicAttributes();
        BasicAttribute oc=new BasicAttribute("objectClass");
        oc.add("top");
        oc.add("group");
        groupAttributes.put(oc);
        groupAttributes.put("cn", name);
        groupAttributes.put("sAMAccountName", name);
        BasicAttribute member = new BasicAttribute("member");
        member.add("cn=张三,ou=部门一,ou=测试集团");
        member.add("cn=李四,ou=部门一,ou=测试集团");
        groupAttributes.put(member);
        if(!StringUtils.hasText(baseDn)){
            baseDn = "";
        }
        LdapName dn = LdapUtils.newLdapName(baseDn);
        try {
            dn.add(new Rdn("cn", name));
        } catch (InvalidNameException e) {
            log.error("{}",e.getMessage(), e);
            return false;
        }
        ldapTemplate.bind(dn,null,groupAttributes);
        return true;
    }

    /**
     * 添加用户至用户组
     */
    public boolean addUserToGroup(String userDn, String groupDn) {
        DirContextOperations ctxGroup;
        try {
            ctxGroup = ldapTemplate.lookupContext(groupDn);
        }catch (NameNotFoundException e){
            throw new RuntimeException("用户组不存在");
        }
        DirContextOperations ctxUser;
        try {
            ctxUser = ldapTemplate.lookupContext(userDn);
        }catch (NameNotFoundException e){
            throw new RuntimeException("用户组不存在");
        }
        ctxGroup.addAttributeValue("member", ctxUser.getStringAttribute("distinguishedName"));
        ldapTemplate.modifyAttributes(ctxGroup);
        return true;
    }

    /**
     * 从用户组中删除用户
     */
    public boolean removeUserFromGroup(String userDn, String groupDn) {
        DirContextOperations ctxGroup;
        try {
            ctxGroup = ldapTemplate.lookupContext(groupDn);
        }catch (NameNotFoundException e){
            throw new RuntimeException("用户组不存在");
        }
        DirContextOperations ctxUser;
        try {
            ctxUser = ldapTemplate.lookupContext(userDn);
        }catch (NameNotFoundException e){
            throw new RuntimeException("用户组不存在");
        }
        ctxGroup.removeAttributeValue("member", ctxUser.getStringAttribute("distinguishedname"));
        ldapTemplate.modifyAttributes(ctxGroup);
        return true;
    }

    private byte[] encodePwd(String source) {
        String quotedPassword = "\"" + source + "\""; // 注意:必须在密码前后加上双引号
        return quotedPassword.getBytes(StandardCharsets.UTF_16LE);
    }

}

增加Ldap登录模式

注:ldap是整个企业的用户验证,ldap验证通过并不等于可以登录当前系统,需要管理员将指定人员加入当前系统,进行绑定。所以ldap认证通过,还要验证本系统的用户是否绑定域登录账号, 结合ldap的auth接口自己实现 

四、常见问题

1. Ldap认证错误编码

/**
 * Ldap认证错误编码 枚举类
 */
@Getter
@AllArgsConstructor
public enum LdapAuthCodeEnum {
	CODE_525("525", "域用户不存在"),
	CODE_52e("52e", "域密码或凭证无效"),
	CODE_530("530", "域此时不允许登录"),
	CODE_531("531", "在此工作站上不允许登录"),
	CODE_532("532", "域密码过期"),
	CODE_533("533", "域账户禁用"),
	CODE_701("701", "域账户过期"),
	CODE_773("773", "域用户必须重置密码"),
	CODE_775("775", "域用户账户锁定");
	 String code;
	 String message;
}
// 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data  525 , v893
// 十六进制: 0x525 - 用户不存在
// 十进制: 1317 - ERROR_NO_SUCH_USER  (指定的账户不存在.)
// 注释: 当用户名无效时返回

// 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data 52e, v893
// 十六进制: 0x52e - 无效的凭证
// 十进制: 1326 - ERROR_LOGON_FAILURE (登录失败,未知的用户名或者密码错误.)
// 注释: 当用户名有效但是密码或者凭证无效的时候返回。


// 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data 530, v893
// 十六进制: 0x530 - 此时禁止登录
// 十进制: 1328 - ERROR_INVALID_LOGON_HOURS (登录失败,登录时间违规.)
// 注释: 仅当输入了正确的用户名和密码或凭证时才返回此值。说明用户被禁止登录了

// 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data 531, v893
// 十六进制: 0x531 - 此用户禁止在当前工作站登录
// 十进制: 1329 - ERROR_INVALID_WORKSTATION (登录失败,在此计算机上该用户不允许登录.)
// LDAP[userWorkstations: <multivalued list of workstation names>]
// 注释: 当输入了正确的用户名和密码或凭证时才返回此值。

// 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data 532, v893
// 十六进制: 0x532 - 密码过期
// 十进制: 1330 - ERROR_PASSWORD_EXPIRED (登录失败,指定的账户密码过期.)
// LDAP[userAccountControl: <bitmask=0x00800000>] - PASSWORDEXPIRED
// 注释: 当输入了正确的用户名和密码或凭证是猜返回此值。

// 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data 533, v893
// 十六进制: 0x533 - 账户被禁用
// 十进制: 1331 - ERROR_ACCOUNT_DISABLED (登录失败,账户当前被禁用了.)
// LDAP[userAccountControl: <bitmask=0x00000002>] - ACCOUNTDISABLE
// 注释: 当输入了正确的用户名和密码或凭证是猜返回此值。


// 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data 701, v893
// 十六进制: 0x701 - 账户已过期
// 十进制: 1793 - ERROR_ACCOUNT_EXPIRED (用户账户已过期.)
// LDAP[accountExpires: <value of -1, 0, or extemely large value indicates account will not expire>] - ACCOUNTEXPIRED
// 注释: 当输入了正确的用户名和密码或凭证是猜返回此值。


// 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data 773, v893
// 十六进制: 0x773 - 账户密码必须被重置
// 十进制: 1907 - ERROR_PASSWORD_MUST_CHANGE (用户密码在第一次登录之前必须修改.)
// LDAP[pwdLastSet: <value of 0 indicates admin-required password change>] - MUST_CHANGE_PASSWD
// 注释: 当输入了正确的用户名和密码或凭证是猜返回此值。


// 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data 775, v893
// 十六进制: 0x775 - 账户被锁定
// 十进制: 1909 - ERROR_ACCOUNT_LOCKED_OUT (账户当前已被锁定,不允许登录The referenced account is currently locked out and may not be logged on to.)
// LDAP[userAccountControl: <bitmask=0x00000010>] - LOCKOUT
// 注释: 即便是输入了错误的密码也可能返回此值

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森林中进行全局搜索,跨越多个域。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值