1. 了解如何LDAP协议

1.1 LDAP协议入门

LDAP是轻量目录访问协议(LightweightDirectory Access Protocol)的缩写,LDAP标准实际上是在X.500标准基础上产生的一个简化版本。

1.2 目录结构

LDAP也可以说成是一种数据库,也有client端和server端。server端是用来存放数据,client端用于操作增删改查等操作,通常说的LDAP是指运行这个数据库的服务器。 只不过,LDAP数据库结构为树结构,数据存储在叶子节点上。

假设你要树上的一个苹果(一条记录),你怎么告诉园丁它的位置呢?
当然首先要说明是哪一棵树(dc,相当于MYSQL的DB),然后是从树根到那个苹果所经过的所有“分叉”(ou),最后就是这个苹果的名字(uid,相当于MySQL表主键id)。好了!这时我们可以清晰地指明这个苹果的位置了,就是那棵“歪脖树”的东边那个分叉上的靠西边那个分叉的再靠北边的分叉上的半红半绿的……

因此,在LDAP中,位置可以描述如下

树(dc=tree)
分叉(ou=furcation)
苹果(cn=apple)

那么如果我想描述一个苹果在ldap里面,那么描述如下

dn:cn=redApple,ou=furcation,dc=tree

dn标识一条记录,描述了数据的详细路径。因此,LDAP树形数据库如下

dn(Distinguished Name):一条记录的详细位置
dc:一条记录所属区域 (哪一颗树)
ou(Organization Unit) :一条记录所属组织 (哪一个分支)
cn/uid:一条记录的名字/ID (哪一个苹果名字)
LDAP目录树的最顶部就是根,也就是所谓的“基准DN"

2 了解AD域

2.1 AD入门

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

2.2 作用
2.2.1 用户服务

管理用户的域账号、用户信息、企业通信录(与电子邮箱系统集成)、用户组管理、用户身份认证、用户授权管理、按需实施组管理策略等。这里不单单指某些线上的应用更多的是指真实的计算机,服务器等。

2.2.2 计算机管理

管理服务器及客户端计算机账户、所有服务器及客户端计算机加入域管理并按需实施组策略。

2.2.3 资源管理

管理打印机、文件共享服务、网络资源等实施组策略。

2.2.4 应用系统的支持

对于电子邮件(Exchange)、在线及时通讯(Lync)、企业信息管理(SharePoint)、微软CRM,ERP等业务系统提供数据认证(身份认证、数据集成、组织规则等)。这里不单是微软产品的集成,其它的业务系统根据公用接口的方式一样可以嵌入进来。

2.2.5 客户端桌面的管理

系统管理员可以集中的配置各种桌面配置策略,如:用户适用域中资源权限限制、界面功能的限制、应用程序执行特征的限制、网络连接限制、安全配置限制等。

2.3 AD域结构常用对象
2.3.1 域(Domain)

域是AD的根,是AD的管理单位。域中包含着大量的域对象,如:组织单位(Organizational Unit),组(Group),用户(User),计算机(Computer),联系人(Contact),打印机,安全策略等。
可简单理解为:公司总部。

2.3.2 组织单位(Organizational Unit 简称ou)

组织单位简称为OU是一个容器对象,可以把域中的对象组织成逻辑组,帮助网络管理员简化管理组。组织单位可以包含下列类型的对象:用户,计算机,工作组,打印机,安全策略,其他组织单位等。可以在组织单位基础上部署组策略,统一管理组织单位中的域对象。

可以简单理解为:分公司。

2.3.3 群组(Group)

群组是一批具有相同管理任务的用户账户,计算机账户或者其他域对象的一个集合。例如公司的开发组,产品组,运维组等等。可以简单理解为分公司的某事业部。
群组类型可以分为两个类型

安全组:用来设置有安全权限相关任务的用户或者计算机账户的集合。比如:Tiger组都可以登录并访问某ftp地址,并拿到某个文件。
通信组:用于用户之间通信的组,适用通信组可以向一组用户发送电子邮件。比如:我要向团队内10为成员都发送同一封邮件这里就要抄送9次,而使用组的话我直接可以发送给@Tiger,所有Tiger组内的成员都会收到邮件。

2.3.4 用户(User)

AD中域用户是最小的管理单位,域用户最容易管理又最难管理,如果赋予域用户的权限过大,将带来安全隐患,如果权限过小域用户无法正常工作。可简单理解成为某个工作人员。
域用户的类型,域中常见用户类型分为:

普通域用户:创建的域用户默认就添加到"Domain Users"中。
域管理员:普通域用户添加进"Domain Admins"中,其权限升为域管理员。
企业管理员:普通域管理员添加进"Enterprise Admins"后,其权限提升为企业管理员,企业管理员具有最高权限。

总之:Active Directory =LDAP服务器+LDAP应用(Windows域控)。ActiveDirectory先实现一个LDAP服务器,然后自己先用这个LDAP服务器实现了自己的一个具体应用(域控)。

3. 搭建AD域环境

这里需要准备两个虚拟机用于测试,分别上Windows server 2012和window 7 (这里win7的版本要选择专业版本,不要选择家庭教育版,后者会阉割掉部分功能,导致不能加入域)
这里搭建AD环境参考链接AD域环境搭建第一篇AD域环境搭建第二篇

4. 搭建AD域证书服务

这里证书服务安装参考链接:WINDOWS SERVER 2012证书服务安装配置

注意上面安装好后,如果直接用ssl连接ldap去认证AD会报错,这里可以参考链接:

5. 使用Java去接入认证AD

(1). 特别注意:Java操作查询域用户信息获取到的数据和域管理员在电脑上操作查询的数据可能会存在差异(同一个意思的表示字段,两者可能不同)。

(2). 连接ad域有两个地址: ldap://http://XXXXX.com:389 和 ldap://http://XXXXX.com:636(SSL)。

(3). 端口389用于一般的连接,例如登录,查询等非密码操作,端口为636的安全性较高,用户密码相关操作,例如修改密码等。

(4). 域控可能有多台服务器,之间数据同步不及时,可能会导致已经修改的数据被覆盖掉,这个要么域控缩短同步的时间差,要么同时修改每一台服务器的数据。

5.1 使用389登陆
/**
* 只要不抛出异常就是验证通过
* 返回的上下文可以查询Ad域的用户信息
* @param username 用户名称,cn,ou,dc 分别:用户,组,域; 例如:CN=lufei,ou=深圳分公司,ou=开发部,dc=ceshi,dc=com 或 lufei@ceshi.com
* @param password 用户密码
* */
public static DirContext login(String username,String password){
    DirContext ctx = null;
    Hashtable<String, String> env = new Hashtable<String, String>();
    env.put(Context.SECURITY_AUTHENTICATION, "simple"); // LDAP访问安全级别(none,simple,strong);
    env.put(Context.SECURITY_PRINCIPAL, username); // AD的用户名
    env.put(Context.SECURITY_CREDENTIALS, password); // AD的密码
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); // LDAP工厂类
    env.put("com.sun.jndi.ldap.connect.timeout", "3000");// 连接超时设置为3秒
    env.put(Context.PROVIDER_URL, "ldap://" + URL + ":" + PORT);// 默认端口389
    try {
        ctx = new InitialDirContext(env);// 初始化上下文
        System.out.println("身份验证成功!");
        return ctx;
    } catch (AuthenticationException e) {
        System.out.println("身份验证失败!");
        e.printStackTrace();
        return ctx;
    } catch (javax.naming.CommunicationException e) {
        System.out.println("AD域连接失败!");
        e.printStackTrace();
        return ctx;
    } catch (Exception e) {
        System.out.println("身份验证未知异常!");
        e.printStackTrace();
        return ctx;
    } finally {
        if (null != ctx) {
            try {
                ctx.close();
                ctx = null;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
5.2 使用636登录

这里需要将证书到处到本地,用keytool将证书导入本地秘钥库,可以参考链接:

/**
 * @param username 用户名称,cn,ou,dc 分别:用户,组,域; 例如:CN=lufei,ou=深圳分公司,ou=开发部,dc=ceshi,dc=com 或 lufei@ceshi.com
 * @param password
 * */
public static LdapContext sslLogin(String username, String password){
    Hashtable env = new Hashtable();
    String javaHome = System.getProperty("java.home");
    String keystore = javaHome+"/lib/security/cacerts";
    System.setProperty("javax.net.ssl.trustStore", keystore);
    System.setProperty("javax.net.ssl.trustStorePassword", "changeit");//私钥密码
    System.setProperty("com.sun.jndi.ldap.object.disableEndpointIdentification", "true");
    String LDAP_URL = "ldaps://"+ URL +":"+ SSL_PORT; // LDAP访问地址

    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.SECURITY_PROTOCOL, "ssl");//链接认证服务器
    env.put(Context.PROVIDER_URL, LDAP_URL);
    env.put(Context.SECURITY_AUTHENTICATION, "simple");
    env.put(Context.SECURITY_PRINCIPAL, username);
    env.put(Context.SECURITY_CREDENTIALS, password);
    try {
        LdapContext ldapContext = new InitialLdapContext(env, null);
        System.out.println("认证成功!");
        return ldapContext;
    } catch (javax.naming.AuthenticationException e) {
        e.printStackTrace();
        System.out.println("认证失败!");
    } catch (Exception e) {
        e.printStackTrace();
        System.out.println("认证出错!");
    }
    return null;
}
5.3 查看用户信息
/**
 * 查询用户信息
 * @param dirContext
 * @param searchBase 域节点 如dc=ceshi,dc=com
 * @param userCount 查询的用户名称 用户名称,cn,ou,dc 分别:用户,组,域; 例如:CN=lufei,ou=深圳分公司,ou=开发部,dc=ceshi,dc=com
 * */
public static void getUserInfo(DirContext dirContext,String searchBase,String userCount) throws NamingException {
    String company = "";
    String result = "";
    List<Map<String, String>> resultList = new ArrayList<>();
    /**
     * LDAP搜索过滤器类
     * cn=*name*模糊查询
     * cn=name 精确查询
     * String searchFilter = "(objectClass="+type+")";*/
    String searchFilter = "(sAMAccountName="+userCount+")";
    SearchControls searchControls = new SearchControls();    // 创建搜索控制器
    String  returnedAtts[]={"description","sAMAccountName","userAccountControl"};//返回指定属性,null返回所有属性,空数组不返回任何属性
    searchControls.setReturningAttributes(returnedAtts);
    searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);//设置搜索范围 深度
    NamingEnumeration answers = dirContext.search(searchBase,searchFilter,searchControls);//根据设置的域节点、过滤器类和搜索控制器搜索LDAP得到结果
    //初始化搜索结果数为0
    int totalResults = 0;
    int rows = 0;
    //遍历结果集
    while (answers.hasMoreElements()) {
        SearchResult searchResult = (SearchResult) answers.next();//得到符合条件的dn
        ++rows;
        String dn = searchResult.getName();//获取dn
        Attributes attributes =  searchResult.getAttributes(); //得到符合条件的属性集
        if (attributes != null) {
            try {
                for (NamingEnumeration ne = attributes.getAll();ne.hasMore();) {//遍历属性
                    Attribute attribute = (Attribute) ne.next();
                    System.out.println(attribute.getID() + ":" + attribute.get());
                    for (NamingEnumeration e = attribute.getAll();e.hasMore();totalResults++) {
                        company = e.next().toString();
                        Map<String,String> tempMap = new HashMap<>();
                        tempMap.put(attribute.getID(), company.toString());
                        resultList.add(tempMap);
                    }
                }
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    System.out.println("查询到用户总数为:"+rows);
}
5.4 重置用户密码
public static Map<String, String> updateAdPwd(LdapContext ldapContext,String newPassword,String dn) {

    Map<String,String> map = new HashMap<String,String>();
    ModificationItem[] mods = new ModificationItem[2];
    if (ldapContext!=null){
        try {
            String newQuotedPassword = "\"" + newPassword + "\"";
            byte[] newUnicodePassword = newQuotedPassword.getBytes("UTF-16LE");
            // unicodePwd:修改的字段,newUnicodePassword:修改的值
            mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
                    new BasicAttribute("unicodePwd", newUnicodePassword));
            mods[1] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
                    new BasicAttribute("pwdLastSet", "0"));  // 首次登录必须修改密码
            ldapContext.modifyAttributes(dn, mods);//
            map.put("result", "S");
            map.put("message","成功");
        }catch (Exception e){
            map.put("result","E");
            map.put("message", "无法重置密码");
        }finally {
            try{
                ldapContext.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }else {
        map.put("result","E");
        map.put("message", "验证失败");
    }
    return map;
}
5.4 解锁账号
/**
 * 解锁账号
 * @param ldapContext
 * @param dn 被解锁的帐号(这个dn指的是查询用户信息里的dn的值,不是域账号)
 * */
public Map<String, String> deblocking(LdapContext ldapContext,String dn  ) {

    Map<String,String> map = new HashMap<String,String>();
    ModificationItem[] mods = new ModificationItem[1];
    if (ldapContext!=null){
        try {
            // "0" 表示未锁定,不为0表示锁定
            mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
                    new BasicAttribute("lockoutTime","0"));
            // 解锁域帐号
            ldapContext.modifyAttributes(dn, mods);
            map.put("result", "S");
            map.put("message","成功");
        }catch (Exception e){
            map.put("result","E");
            map.put("message", "解锁失败");
        }finally {
            try{
                ldapContext.close();
            }catch (Exception e){
                e.printStackTrace();
            }

        }

    }else {
        map.put("result","E");
        map.put("message", "验证失败");
    }
    return map;
 }
}