和利用数据库进行验证类似,LDAP中也是利用登陆名和密码进行验证,LDAP中会定义一个属性password,用来存放用户密码,而登陆名使用较多的都是邮箱地址。
下面是一个正确而又通用的步骤:
1. 从客户端得到登录名和密码。注意这里的登录名和密码一开始是没有被用到的。
2. 先匿名绑定到LDAP服务器,如果LDAP服务器没有启用匿名绑定,一般会提供一个默认的用户,用这个用户进行绑定即可。
3. 之前输入的登录名在这里就有用了,当上一步绑定成功以后,需要执行一个搜索,而filter就是用登陆名来构造,例如: "(|(uid=$login)(mail=$login))" ,这里的login就是登录名。搜索执行完毕后,需要对结果进行判断,如果只返回一个entry,这个就是包含了该用户信息的entry,可以得到该entry的DN,后面使用。如果返回不止一个或者没有返回,说明用户名输入有误,应该退出验证并返回错误信息。
4. 如果能进行到这一步,说明用相应的用户,而上一步执行时得到了用户信息所在的entry的DN,这里就需要用这个DN和第一步中得到的password重新绑定LDAP服务器。
5. 执行完上一步,验证的主要过程就结束了,如果能成功绑定,那么就说明验证成功,如果不行,则应该返回密码错误的信息。
这5大步就是基于LDAP的一个 “两次绑定” 验证方法。
为什么基于LDAP进行验证需要“两次”绑定呢,为什么不能取出password然后和输入进行比较呢,试想一下,如果需要读出密码,服务器上的密码存储要么就不加密,直接可以读出,要么客户就需要知道服务器使用的加密方式,这是不安全,也是不好的,服务器不希望加密方式让客户端知道,客户端也不需要知道这么多。而从实际来看,LDAP服务器对于password属性默认都是不可读的,甚至有的服务器根本就不支持password属性可读,遇到这种情况,也就没有办法取得密码了。
还有一个问题就是,为什么我们需要第一次绑定?为什么不直接使用DN呢,首先就是关于这个DN,对于一般的客户端程序,其并不知道具体的DN是什么。再者让用户输入DN,给用户带来不便的同时,验证也带来问题,因为如果输入的是个目录树而不是所期望的DN,在进行绑定时有可能会让服务器产生不可预料的错误。
从上面看来,基于LDAP进行身份验证,最好也是最通用的方法就是 “两次绑定”。所谓的绑定是一个authentication(身份验证)的过程,不要把它想像成“绑定”,既然是认证,就需要一个用户名和密码,openldap中如果出示的用户名和密码错误,服务器会尝试匿名认证,就和匿名ftp一样。当然,在现实配置中可能需要在认证不获得成功就不能做查询操作,这些是在slapd.conf文件中通过设置ACL实现的。认证所用的用户名和密码为目录树中某个节点的两个属性(用户名和密码),一般情况下,程序会默认使用uid和userPassword属性。写程序进行认证的时候只要提供这个节点的两个属性就可以了。
ldap中常见名词解释:
CN:common name 通用名
对象的属性为CN,例如一个用户的名字为:张三,那么“张三”就是一个CN。
O:organizationName 组织名
OU : OrganizationUnit 组织单位
o和ou都是ldap目录结构的一个属性,建立目录的时候可选新建o,ou 等。在配置我司交换设备ldap的时候具体是配置ou,o还是cn等,要具体看ldap服务器的相应目录是什么属性。
UID: userid
对象的属性为uid,例如我司一个员工的名字为:zsq,他的UID为:z02691,ldap查询的时候可以根据cn,也可以根据uid。配置ldap查询的时候需要考虑用何种查询方式。具体设备配置根据何种方式查询由ldap服务器的相关配置来决定。
DC:Domain Component
DC类似于dns中的每个元素,例如h3c.com,“.”符号分开的两个单词可以看成两个DC。
DN:Distinguished Name
类似于DNS,DN与DNS的区别是:组成DN的每个值都有一个属性类型,例如:
H3c.com是一个dns,那么用dn表示为:dc=h3c,dc=com 级别越高越靠后。H3c和com的属性都是DC。
DN可以表示为ldap的某个目录,也可以表示成目录中的某个对象,这个对象可以是用户等。
下面是一个简单的ldap认证流程代码:
import org.junit.Before;
import org.junit.Test;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.xml.bind.PropertyException;
import java.util.*;
public class LdapTest {
private final String PROPERTIES_SCOPE = "ldap";
private final String FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
private String url = "ldap://192.168.30.254:389";
private String base = "1";
private LdapContext ctx = null;
private Control[] connCtls = null;
public static LdapContext ctx = null;
private final Control[] connCtls = null;
@Test
public void testUser() {
connLDAP();
List<User> list = getUser("users");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
System.out.println("ending");
}
@Test
public void test() {
connLDAP();
}
public boolean connLDAP() {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, FACTORY);
env.put(Context.PROVIDER_URL, url);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, ldapUserDn);
env.put(Context.SECURITY_CREDENTIALS, ldapPassword);
try {
ctx = new InitialLdapContext(env, connCtls);
System.out.println("认证成功!");
return Boolean.TRUE;
} catch (javax.naming.AuthenticationException e) {
System.out.println("认证失败 :");
e.printStackTrace();
return Boolean.FALSE;
} catch (Exception e) {
System.out.println("认证出错 : ");
e.printStackTrace();
return Boolean.FALSE;
} finally {
close();
}
}
public void close() {
if (ctx != null) {
try {
ctx.close();
System.out.println("连接关闭");
} catch (NamingException e) {
e.printStackTrace();
}
}
}
public List<User> getUser(String uname) {
List<User> users = new ArrayList<>();
HashMap<String, String> map = new HashMap<>();
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
//过滤器,可改变查询条件
String searchFilter = "(&(cn=" + uname + ")(objectClass=person))";
String searchBase = "DC=maxcrc,DC=com";
//对象的每个属性名
String[] returnedAtts = {"mail", "cn", "userPassword", "sn", "uid"};
searchCtls.setReturningAttributes(returnedAtts);
NamingEnumeration<SearchResult> answer;
Map<String, String> mmap = new HashMap<>();
try {
answer = ctx.search(searchBase, searchFilter, searchCtls);
while (answer.hasMoreElements()) {
SearchResult sr = (SearchResult) answer.next();
String name = sr.getName();
User user = new User();
if (name.contains("cn") || name.contains("uid")) {
String[] s = name.split(",");
String[] cns = s[0].split("=");
String cn = cns[cns.length - 1];
mmap.put(cn, sr.getNameInNamespace());
NamingEnumeration<? extends Attribute> attrs = sr.getAttributes().getAll();
while (attrs.hasMore()) {
Attribute attr = attrs.next();
switch (attr.getID()) {
case "cn":
user.setCn(attr.get().toString());
break;
case "sn":
user.setSn(attr.get().toString());
break;
case "mail":
user.setMail(attr.get().toString());
break;
case "uid":
user.setUid(attr.get().toString());
break;
case "userPassword":
user.setPassword(attr.get().toString());
break;
}
}
users.add(user);
}
}
} catch (NamingException e) {
e.printStackTrace();
}
mmap.forEach((k, v) -> System.out.println("key : value = " + k + ":" + v));
return users;
}
}
public class User {
private String cn;
private String sn;
private String mail;
private String uid;
private String password;
public String getCn() {
return cn;
}
public void setCn(String cn) {
this.cn = cn;
}
public String getSn() {
return sn;
}
public void setSn(String sn) {
this.sn = sn;
}
public String getMail() {
return mail;
}
public void setMail(String mail) {
this.mail = mail;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"cn='" + cn + '\'' +
", sn='" + sn + '\'' +
", mail='" + mail + '\'' +
", uid='" + uid + '\'' +
", password='" + password + '\'' +
'}';
}
}