工作需要学习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进行身份验证呢,下面是一个正确而又通用的步骤:
-
从客户端得到登陆名和密码。注意这里的登陆名和密码一开始并没有被用到。
-
先匿名绑定到LDAP服务器,如果LDAP服务器没有启用匿名绑定,一般会提供一个默认的用户,用这个用户进行绑定即可。
-
之前输入的登陆名在这里就有用了,当上一步绑定成功以后,需要执行一个搜索,而filter就是用登陆名来构造,形如: “(|(uid=$login)(mail=$login))” ,这里的login就是登陆名。 搜索执行完毕后,需要对结果进行判断,如果只返回一个entry,这个就是包含了该用户信息的entry,可以得到该entry的DN,后面使用。 如果返回不止一个或者没有返回,说明用户名输入有误,应该退出验证并返回错误信息。
-
如果能进行到这一步,说明用相应的用户,而上一步执行时得到了用户信息所在的entry的DN,这里就需要用这个DN和第一步中得到的password重新绑定LDAP服务器。
-
执行完上一步,验证的主要过程就结束了,如果能成功绑定,那么就说明验证成功,如果不行,则应该返回密码错误的信息。
这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;
}
}