Java程序通過LDAP對用戶進行登陸驗證


 

1. 目錄服務

目錄是一個為查詢、瀏覽和搜索而優化的專業分布式數據庫,它呈樹狀結構組織數據,就好象Linux/Unix系統中的文件目錄一樣。目錄數據庫和關系數據庫不同,它有優異的讀性能,但寫性能差,並且沒有事務處理、回滾等復雜功能,不適於存儲修改頻繁的數據。所以目錄天生是用來查詢的,就好象它的名字一樣。

目錄服務是由目錄數據庫和一套訪問協議組成的系統。類似以下的信息適合儲存在目錄中:

  • 企業員工信息,如姓名、電話、郵箱等;
  • 公用證書和安全密鑰;
  • 公司的物理設備信息,如服務器,它的IP地址、存放位置、廠商、購買時間等;

LDAP是輕量目錄訪問協議(Lightweight Directory Access Protocol)的縮寫,LDAP是從X.500目錄訪問協議的基礎上發展過來的,目前的版本是v3.0。與LDAP一樣提供類似的目錄服務軟件還有ApacheDSActive DirectoryRed Hat Directory Service 。

2. LDAP特點

  • LDAP的結構用樹來表示,而不是用表格。正因為這樣,就不能用SQL語句了
  • LDAP可以很快地得到查詢結果,不過在寫方面,就慢得多
  • LDAP提供了靜態數據的快速查詢方式
  • Client/server模型,Server 用於存儲數據,Client提供操作目錄信息樹的工具
  • 這些工具可以將數據庫的內容以文本格式(LDAP 數據交換格式,LDIF)呈現在您的面前
  • LDAP是一種開放Internet標准,LDAP協議是跨平台的Interent協議

3. LDAP組織數據的方式

4. 基本概念

在瀏覽LDAP相關文檔時經常會遇見一些概念,下面是常見概念的簡單解釋。

4.1 Entry

條目,也叫記錄項,是LDAP中最基本的顆粒,就像字典中的詞條,或者是數據庫中的記錄。通常對LDAP的添加、刪除、更改、檢索都是以條目為基本對象的。

dn:每一個條目都有一個唯一的標識名(distinguished Name ,DN),如上圖中一個 dn:"cn=baby,ou=marketing,ou=people,dc=mydomain,dc=org" 。通過DN的層次型語法結構,可以方便地表示出條目在LDAP樹中的位置,通常用於檢索。

rdn:一般指dn逗號最左邊的部分,如cn=baby。它與RootDN不同,RootDN通常與RootPW同時出現,特指管理LDAP中信息的最高權限用戶。

Base DN:LDAP目錄樹的最頂部就是根,也就是所謂的“Base DN",如"dc=mydomain,dc=org"。

4.2 Attribute

每個條目都可以有很多屬性(Attribute),比如常見的人都有姓名、地址、電話等屬性。每個屬性都有名稱及對應的值,屬性值可以有單個、多個,比如你有多個郵箱。

屬性不是隨便定義的,需要符合一定的規則,而這個規則可以通過schema制定。比如,如果一個entry沒有包含在 inetorgperson 這個 schema 中的objectClass: inetOrgPerson,那么就不能為它指定employeeNumber屬性,因為employeeNumber是在inetOrgPerson中定義的。

LDAP為人員組織機構中常見的對象都設計了屬性(比如commonName,surname)。下面有一些常用的別名:

屬性 別名 語法 描述 值(舉例)
commonName cn Directory String 姓名 sean
surname sn Directory String Chow
organizationalUnitName ou Directory String 單位(部門)名稱 IT_SECTION
organization  o Directory String 組織(公司)名稱 example
telephoneNumber   Telephone Number 電話號碼 110
objectClass     內置屬性 organizationalPerson

4.3 ObjectClass

對象類是屬性的集合,LDAP預想了很多人員組織機構中常見的對象,並將其封裝成對象類。比如人員(person)含有姓(sn)、名(cn)、電話(telephoneNumber)、密碼(userPassword)等屬性,單位職工(organizationalPerson)是人員(person)的繼承類,除了上述屬性之外還含有職務(title)、郵政編碼(postalCode)、通信地址(postalAddress)等屬性。

通過對象類可以方便的定義條目類型。每個條目可以直接繼承多個對象類,這樣就繼承了各種屬性。如果2個對象類中有相同的屬性,則條目繼承后只會保留1個屬性。對象類同時也規定了哪些屬性是基本信息,必須含有(Must 活Required,必要屬性):哪些屬性是擴展信息,可以含有(May或Optional,可選屬性)。

對象類有三種類型:結構類型(Structural)、抽象類型(Abstract)和輔助類型(Auxiliary)。結構類型是最基本的類型,它規定了對象實體的基本屬性,每個條目屬於且僅屬於一個結構型對象類。抽象類型可以是結構類型或其他抽象類型父類,它將對象屬性中共性的部分組織在一起,稱為其他類的模板,條目不能直接集成抽象型對象類。輔助類型規定了對象實體的擴展屬性。每個條目至少有一個結構性對象類。

對象類本身是可以相互繼承的,所以對象類的根類是top抽象型對象類。以常用的人員類型為例,他們的繼承關系:

下面是inetOrgPerson對象類的在schema中的定義,可以清楚的看到它的父類SUB和可選屬性MAY、必要屬性MUST(繼承自organizationalPerson),關於各屬性的語法則在schema中的attributetype定義。

# inetOrgPerson # The inetOrgPerson represents people who are associated with an # organization in some way. It is a structural class and is derived # from the organizationalPerson which is defined in X.521 [X521]. objectclass ( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' DESC 'RFC2798: Internet Organizational Person' SUP organizationalPerson STRUCTURAL MAY ( audio $ businessCategory $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ givenName $ homePhone $ homePostalAddress $ initials $ jpegPhoto $ labeledURI $ mail $ manager $ mobile $ o $ pager $ photo $ roomNumber $ secretary $ uid $ userCertificate $ x500uniqueIdentifier $ preferredLanguage $ userSMIMECertificate $ userPKCS12 ) ) 

4.4 Schema

對象類(ObjectClass)、屬性類型(AttributeType)、語法(Syntax)分別約定了條目、屬性、值,他們之間的關系如下圖所示。所以這些構成了模式(Schema)——對象類的集合。條目數據在導入時通常需要接受模式檢查,它確保了目錄中所有的條目數據結構都是一致的。

schema(一般在/etc/ldap/schema/目錄)在導入時要注意前后順序。

4.5 backend & database

ldap的后台進程slapd接收、響應請求,但實際存儲數據、獲取數據的操作是由Backends做的,而數據是存放在database中,所以你可以看到往往你可以看到backenddatabase指令是一樣的值如 bdb 。一個 backend 可以有多個 database instance,但每個 database 的 suffix 和 rootdn 不一樣。openldap 2.4版本的模塊是動態加載的,所以在使用backend時需要moduleload back_bdb指令。

bdb是一個高性能的支持事務和故障恢復的數據庫后端,可以滿足絕大部分需求。許多舊文檔里(包括官方)說建議將bdb作為首選后端服務(primary backend),但2.4版文檔明確說hdb才是被首先推薦使用的,這從 2.4.40 版默認安裝后的配置文件里也可以看出。hdb是基於bdb的,但是它通過擴展的索引和緩存技術可以加快數據訪問,修改entries會更有效率,有興趣可以訪問上的鏈接或slapd.backends

另外config是特殊的backend,用來在運行時管理slapd的配置,它只能有一個實例,甚至無需顯式在slapd.conf中配置。

4.6 TLS & SASL

分布式LDAP 是以明文的格式通過網絡來發送信息的,包括client訪問ldap的密碼(當然一般密碼已然是二進制的),SSL/TLS 的加密協議就是來保證數據傳送的保密性和完整性。

SASL (Simple Authenticaion and Security Layer)簡單身份驗證安全框架,它能夠實現openldap客戶端到服務端的用戶驗證,也是ldapsearchldapmodify這些標准客戶端工具默認嘗試與LDAP服務端認證用戶的方式(前提是已經安裝好 Cyrus SASL)。SASL有幾大工業實現標准:Kerveros V5、DIGEST-MD5、EXTERNAL、PLAIN、LOGIN。

Kerveros V5是里面最復雜的一種,使用GSSAPI機制,必須配置完整的Kerberos V5安全系統,密碼不再存放在目錄服務器中,每一個dn與Kerberos數據庫的主體對應。DIGEST-MD5稍微簡單一點,密碼通過saslpasswd2生成放在sasldb數據庫中,或者將明文hash存到LDAP dn的userPassword中,每一個authid映射成目錄服務器的dn,常和SSL配合使用。參考將 LDAP 客戶端配置為使用安全性

EXTERNAL一般用於初始化添加schema時使用,如ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/core.ldif

4.7 LDIF

LDIF(LDAP Data Interchange Format,數據交換格式)是LDAP數據庫信息的一種文本格式,用於數據的導入導出,每行都是“屬性: 值”對,見 openldap ldif格式示例

OpenLDAP(2.4.3x)服務器安裝配置方法見這里

參考


原文鏈接地址:http://seanlook.com/2015/01/15/openldap_introduction/

 

     在去年南京項目中,客戶方要求用戶登陸需要在其他平台下進行認證,當時客戶用的LDAP“數據庫”管理方式,后來查閱Java已經對LDAP進行了封裝,不需要下載其他jar包就可以實現。

       補腦:【LDAP】是"Lightweight Directory Access Protocol"的縮寫,中文翻譯過來就叫“輕量目錄訪問協議”,看字面大概能猜出應該是以樹狀形存儲數據的數據庫,后來翻閱資料確實如此。其中包含幾個重要的參數:CN,DN,DC,OU。至於這個縮寫單詞的含義,有興趣可以去百科或者官網腦補一下,下面程序中將會引用這幾個才參數,也會進行簡單的描述。

 

[java]  view plain  copy
 
  1. package com.angma.mes.zbe.moudle.service.util;  
  2.   
  3. import com.angma.mes.jagybarcode.manager.controller.plan.machine.MachineShopTaskCreateController;  
  4. import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;  
  5. import java.security.MessageDigest;  
  6. import java.security.NoSuchAlgorithmException;  
  7. import java.util.Hashtable;  
  8. import java.util.logging.Level;  
  9. import java.util.logging.Logger;  
  10. import javax.naming.Context;  
  11. import javax.naming.NamingException;  
  12. import javax.naming.directory.DirContext;  
  13. import javax.naming.directory.InitialDirContext;  
  14.   
  15. /** 
  16.  * 用戶登陸認證,LDAP跨域認證,通過LDAP對用戶進行更新 
  17.  *  
  18.  * @author xlj 
  19.  * @date 2015.07.10 
  20.  */  
  21. public class LdapUtil {  
  22.   
  23.     private static DirContext ctx;  
  24.   
  25.     // LDAP服務器端口默認為389  
  26.     private static final String LDAP_URL = "ldap://127.0.0.1:389";  
  27.   
  28.     // ROOT根據此參數確認用戶組織所在位置  
  29.     private static final String LDAP_PRINCIPAL = "OU=CMA Users,DC=changan-mazda,DC=com,DC=cn";  
  30.   
  31.     // LDAP驅動  
  32.     private static final String LDAP_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";  
  33.   
  34.     private static Logger logger = Logger.getLogger(LdapUtil.class);  
  35.   
  36.     /**** 測試 ****/  
  37.     public static void main(String[] args) {  
  38.         LdapUtil.getLoginContext();  
  39.         LdapUtil.addUserLdap("10000", "123456");  
  40.         LdapUtil.updatePasswordLdap("10000", "1234567");  
  41.         LdapUtil.deleteUserLdap("10000");  
  42.     }  
  43.   
  44.     // 通過連接LDAP服務器對用戶進行認證,返回LDAP對象  
  45.     public static DirContext getLoginContext() {  
  46.         String account = "zhangsan"; // 模擬用戶名  
  47.         String password = "123456"; // 模擬密碼  
  48.         for (int i = 0; i < 5; i++) { // 驗證次數  
  49.             Hashtable env = new Hashtable();  
  50.             env.put(Context.SECURITY_AUTHENTICATION, "simple");  
  51.             env.put(Context.SECURITY_CREDENTIALS, password);  
  52.             // cn=屬於哪個組織結構名稱,ou=某個組織結構名稱下等級位置編號  
  53.             env.put(Context.SECURITY_PRINCIPAL, "cn=" + account + ", ou=Level0" + i + "00," + LDAP_URL);  
  54.             env.put(Context.INITIAL_CONTEXT_FACTORY, LDAP_PRINCIPAL);  
  55.             env.put(Context.PROVIDER_URL, LDAP_FACTORY);  
  56.             try {  
  57.                 // 連接LDAP進行認證  
  58.                 ctx = new InitialDirContext(env);  
  59.                 System.out.println("認證成功");  
  60.                 logger.info("【" + account + "】用戶於【" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "】登陸系統成功");  
  61.             } catch (javax.naming.AuthenticationException e) {  
  62.                 System.out.println("認證失敗");  
  63.             } catch (NamingException err) {  
  64.                 logger.info("--------->>【" + account + "】用戶驗證失敗【" + i + "】次");  
  65.             } catch (Exception e) {  
  66.                 System.out.println("認證出錯:");  
  67.                 e.printStackTrace();  
  68.             }  
  69.         }  
  70.         return ctx;  
  71.     }  
  72.   
  73.     // 將輸入用戶和密碼進行加密算法后驗證  
  74.     public static boolean verifySHA(String ldappw, String inputpw) {  
  75.   
  76.         // MessageDigest 提供了消息摘要算法,如 MD5 或 SHA,的功能,這里LDAP使用的是SHA-1  
  77.         MessageDigest md = MessageDigest.getInstance("SHA-1");  
  78.   
  79.         // 取出加密字符  
  80.         if (ldappw.startsWith("{SSHA}")) {  
  81.             ldappw = ldappw.substring(6);  
  82.         } else if (ldappw.startsWith("{SHA}")) {  
  83.             ldappw = ldappw.substring(5);  
  84.         }  
  85.   
  86.         // 解碼BASE64  
  87.         byte[] ldappwbyte = Base64.decode(ldappw);  
  88.         byte[] shacode;  
  89.         byte[] salt;  
  90.   
  91.         // 前20位是SHA-1加密段,20位后是最初加密時的隨機明文  
  92.         if (ldappwbyte.length <= 20) {  
  93.             shacode = ldappwbyte;  
  94.             salt = new byte[0];  
  95.         } else {  
  96.             shacode = new byte[20];  
  97.             salt = new byte[ldappwbyte.length - 20];  
  98.             System.arraycopy(ldappwbyte, 0, shacode, 0, 20);  
  99.             System.arraycopy(ldappwbyte, 20, salt, 0, salt.length);  
  100.         }  
  101.   
  102.         // 把用戶輸入的密碼添加到摘要計算信息  
  103.         md.update(inputpw.getBytes());  
  104.         // 把隨機明文添加到摘要計算信息  
  105.         md.update(salt);  
  106.   
  107.         // 按SSHA把當前用戶密碼進行計算  
  108.         byte[] inputpwbyte = md.digest();  
  109.   
  110.         // 返回校驗結果  
  111.         return MessageDigest.isEqual(shacode, inputpwbyte);  
  112.     }  
  113.   
  114.     // 添加用戶  
  115.     public static boolean addUserLdap(String account, String password) {  
  116.         boolean success = false;  
  117.         try {  
  118.             ctx = LdapUtil.getLoginContext();  
  119.             BasicAttributes attrsbu = new BasicAttributes();  
  120.             BasicAttribute objclassSet = new BasicAttribute("objectclass");  
  121.             objclassSet.add("person");  
  122.             objclassSet.add("top");  
  123.             objclassSet.add("organizationalPerson");  
  124.             objclassSet.add("inetOrgPerson");  
  125.             attrsbu.put(objclassSet);  
  126.             attrsbu.put("sn", account);  
  127.             attrsbu.put("uid", account);  
  128.             attrsbu.put("userPassword", password);  
  129.             ctx.createSubcontext("cn=" + account + ",ou=People", attrsbu);  
  130.             ctx.close();  
  131.             return true;  
  132.         } catch (NamingException ex) {  
  133.             try {  
  134.                 if (ctx != null) {  
  135.                     ctx.close();  
  136.                 }  
  137.             } catch (NamingException namingException) {  
  138.                 namingException.printStackTrace();  
  139.             }  
  140.             logger.info("--------->>添加用戶失敗");  
  141.         }  
  142.         return false;  
  143.     }  
  144.   
  145.     // 修改密碼  
  146.     public static boolean updatePasswordLdap(String account, String password) {  
  147.         boolean success = false;  
  148.         try {  
  149.             ctx = LdapUtil.getLoginContext();  
  150.             ModificationItem[] modificationItem = new ModificationItem[1];  
  151.             modificationItem[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("userPassword", password));  
  152.             ctx.modifyAttributes("cn=" + account + ",ou=People", modificationItem);  
  153.             ctx.close();  
  154.             return true;  
  155.         } catch (NamingException ex) {  
  156.             try {  
  157.                 if (ctx != null) {  
  158.                     ctx.close();  
  159.                 }  
  160.             } catch (NamingException namingException) {  
  161.                 namingException.printStackTrace();  
  162.             }  
  163.             logger.info("--------->>修改密碼失敗");  
  164.         }  
  165.         return success;  
  166.     }  
  167.   
  168.     // 刪除用戶  
  169.     public static boolean deleteUserLdap(String account) {  
  170.         try {  
  171.             ctx = LdapUtil.getLoginContext();  
  172.             ctx.destroySubcontext("cn=" + account);  
  173.         } catch (Exception ex) {  
  174.             try {  
  175.                 if (ctx != null) {  
  176.                     ctx.close();  
  177.                 }  
  178.             } catch (NamingException namingException) {  
  179.                 namingException.printStackTrace();  
  180.             }  
  181.             logger.info("--------->>刪除用戶失敗");  
  182.             return false;  
  183.         }  
  184.         return true;  
  185.     }  
  186.   
  187.     // 關閉LDAP服務器連接  
  188.     public static void closeCtx() {  
  189.         try {  
  190.             ctx.close();  
  191.         } catch (NamingException ex) {  
  192.             logger.info("--------->> 關閉LDAP連接失敗");  
  193.         }  
  194.     }  
  195. }  

 

 

聲明:以上教程為博主原創,若需轉載請注明出處,謝謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM