工作需要学习LDAP认证,记录一下。

LDAP

https://zhuanlan.zhihu.com/p/32732045

目录简单来说就是一种树状结构的数据库。
而目录服务是一种以树状结构的目录数据库为基础,外加各种访问协议的信息查询服务。
顾名思义,目录天生就是用来查询的。 与关系型数据库(如 MariaDB)相比,目录服务最大的优点就是读取性能极高。
但是,目录服务的写入性能就非常差了,而且不支持事务处理等容错功能,因此不适合频繁修改数据。
简单说来, LDAP是一种特殊的数据库。但是和一般的数据库不同, LDAP对查询进行了优化,与写性能相比LDAP的读性能要优秀很多。
X.500 是 ISO 制定的一套目录服务的标准,它是一个协议族,定义了一个机构如何在全局范围内共享名称和与名称相关联的对象。
通过它,可以将局部的目录服务连接起来,构建基于 Internet 的分布在全球的目录服务系统……而目录访问协议(DAP)是 X.500 的核心组成之一。
DAP 非常复杂,所以人们都不喜欢使用它。 所以,轻量级目录访问协议(LDAP)应运而生!LDAP 是基于 X.500 的 DAP 发展而来的,目前最新版本是第 3 版。

LDAP 协议的主要特点:

• 基于 TCP/IP。
• 以树状结构存储数据。
• 读取速度快,写入速度慢。
• 服务器用于存放数据,客户端用于操作数据。
• 跨平台、维护简单。
• 支持 SSL/TLS 加密。
• 协议是开放的。

LDAP 的四大模型:

• 信息模型:规定了 LDAP 目录信息的表示方式以及数据的存储结构。
• 命名模型:规定了 LDAP 目录中数据如何进行组织和区分。
• 功能模型:规定了可以对 LDAP 目录进行的操作(如查询、更新、认证等)。
• 安全模型:规定了保护 LDAP 目录中的数据的安全措施。
基于 LDAP 协议的产品有很多,最有名两个的是开源的 OpenDirectory 以及微软开发的 ActiveDirectory。

LDAP认证

LDAP认证就是把用户数据放在LDAP服务器上,通过LDAP服务器上的数据对用户进行认证处理。
有几种实现的原理,简单讲解两种:
a).每一个登陆,连接请求先去拉取所有的可通过用户的列表,然后去查找是否在已注册用户列表。(不推荐)
b).每一个登陆,连接请求去发送本地的用户、密码给LDAP服务器,然后在LDAP服务器上进行匹配,然后判断是否可以通过认证。(推荐)

和利用数据库进行验证类似,LDAP中也是利用登陆名和密码进行验证,LDAP中会定义一个属性password,用来存放用户密码,而登陆名使用较多的都是mail地址。
那怎么样才能正确的用LDAP进行身份验证呢,下面是一个正确而又通用的步骤:

  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进行身份验证,最好也是最通用的方法就是 “两次绑定”。

所谓的bind是一个authentication的过程,不要把它想像成“绑定”,既然是认证,就需要一个用户名和密码,openldap中如果出示的用户名和密码错误,
服务器会尝试匿名认证,就和匿名ftp一样。当然,在现实配置中可能需要在认证不获得成功就不能做查询操作,这些是在slapd.conf文件中通过设置ACL实现的。
认证所用的用户名和密码为目录树中某个节点的两个属性(用户名和密码),一般情况下,程序会默认使用uid和userPassword属性。
写程序进行认证的时候只要提供这个节点的两个属性就可以了。

LDAP认证的java代码实现

代码实现主要分三步:
第一, 使用默认用户名连接LDAP;
第二, 利用用户输入的user id 获取用户的dn;
第三, 利用获取到的dn和用户输入的password再次连接LDAP。

参考:
https://webcache.googleusercontent.com/search?q=cache:upz9WQxjhlcJ:https://blog.csdn.net/shendeguang/article/details/8467282+&cd=2&hl=en&ct=clnk&gl=us

import java.util.Hashtable;    
import javax.naming.AuthenticationException;  
import javax.naming.Context;  
import javax.naming.NamingEnumeration;  
import javax.naming.NamingException;  
import javax.naming.directory.DirContext;  
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;    

public class UserAuthenticate {

   private String URL = "ldap://localhost:389/";
   private String BASEDN = "c=china,dc=jayway,dc=se";//root
   private String FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
   private LdapContext ctx = null;
   private Hashtable env = null;
   private Control[] connCtls = null;   //连接ldap服务器
   
   private void LDAP_connect(){
     env = new Hashtable();
	 env.put(Context.INITIAL_CONTEXT_FACTORY,FACTORY);
	 env.put(Context.PROVIDER_URL, URL+BASEDN);//LDAP server
	 env.put(Context.SECURITY_AUTHENTICATION, "simple");
	 env.put(Context.SECURITY_PRINCIPAL, "cn=Manager,c=china,dc=jayway,dc=se");
	 env.put(Context.SECURITY_CREDENTIALS, "mysecret");    //此处若不指定用户名和密码,则自动转换为匿名登录
     try{
    	  ctx = new InitialLdapContext(env,connCtls);
		}catch(javax.naming.AuthenticationException e){
		  System.out.println("Authentication faild: "+e.toString());
		}catch(Exception e){
		  System.out.println("Something wrong while authenticating: "+e.toString());
		}
	}
	
	//找到到entry的 DN
	String getUserDN(String email){
    	String userDN = "";
		LDAP_connect();
		
		try{
		   SearchControls constraints = new SearchControls();
		   constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
		   NamingEnumeration en = ctx.search("","mail="+email, constraints); //The UID you are going to query,* means all nodes
		   if(en == null){   System.out.println("Have no NamingEnumeration.");   }
		   if(!en.hasMoreElements()){   System.out.println("Have no element.");   }
		   while (en != null && en.hasMoreElements()){//maybe more than one element
		       Object obj = en.nextElement();
			   if(obj instanceof SearchResult){
			       SearchResult si = (SearchResult) obj;
				   userDN += si.getName();
				   userDN += "," + BASEDN;
				}else{
  				   System.out.println(obj);
				}
				System.out.println();
			}
		}catch(Exception e){
 		    System.out.println("Exception in search():"+e);
		}

		return userDN;
	} 

    //验证
	public boolean authenricate(String ID,String password){
  	    boolean valide = false;
		String userDN = getUserDN(ID);
		try {
		    ctx.addToEnvironment(Context.SECURITY_PRINCIPAL,userDN);
			ctx.addToEnvironment(Context.SECURITY_CREDENTIALS,password);
			ctx.reconnect(connCtls);
			System.out.println(userDN + " is authenticated(验证成功!)");
			valide = true;
		}catch (AuthenticationException e) {
		    System.out.println(userDN + " is not authenticated");
			System.out.println(e.toString());
			valide = false;
		}catch (NamingException e) {
		    System.out.println(userDN + " is not authenticated");
			valide = false;
		}
		return valide;
	} 
}