LDAP目录服务折腾之后的总结

本文介绍了将PHP后台系统与Active Directory通过LDAP协议打通的过程,详细讲解了LDAP协议定义、目录服务、服务器实现、应用场景,以及在PHP中封装类和使用openLDAP的经验,包括源码安装PHP LDAP扩展的步骤。
摘要由CSDN通过智能技术生成

前言

公司管理员工信息以及组织架构的后台系统要和Active Directory目录服务系统打通,后台系统使用PHP开发,

折腾了二十多天,终于上线了,期间碰到过各种疑难问题,不过总算在GOOGLE大叔的帮忙下还有运维部AD管理员的帮助下解决了。

LDAP协议定义

LDAP(Lightweight Directory Access Protocol)轻量目录访问协议,定义了目录服务实现以及访问规范。

目录定义

A directory is a specialized database specifically designed for searching and browsing,
in additional to supporting basic lookup and update functions.

LDAP协议实现

0.基于TCP/IP的应用层协议 默认端口389 加密端口636
1.客户端发送命令,服务器端响应
2.目录主要操作
    2.0 用户验证(bind操作)
     2.1 添加节点
     2.2 更新节点
     2.3 移动节点
     2.4 删除节点
     2.5 节点搜索
3.节点类型
    3.0 节点属性规范(SCHEMA)
4.节点
    4.0 目录里的对象
     4.1 属性即是节点的数据
     4.2 目录中通过DN(Distinguished Name)唯一标识(可以认为是路径)
     4.2.0 节点DN = RDN(Relative Distinguished Name) + 父节点的DN
     4.3 目录是TREE结构,节点可以有子节点,也可以有父节点
5.属性
    5.0 同一个属性可以有多个值
     5.1 包含属性名称,属性类型
6.节点唯一标识DN说明
    6.0 示例: dn:CN=John Doe,OU=Texas,DC=example,DC=com
     6.1 从右到左 根节点 -> 子节点
     6.2 DC:所在控制域 OU:组织单元 CN:通用名称
7.目录规范(SCHEMA)
    7.0 目录节点相关规则
     7.1 Attribute Syntaxes
     7.2 Matching Rules
     7.3 Matching Rule Uses
     7.4 Attribute Types
     7.5 Object Classes
     7.6 Name Forms
     7.7 Content Rules
     7.8 Structure Rule

LDAP服务器端的实现

openLDAP,Active Directory(Microsoft)等等,除了实现协议之外的功能,还对它进行了扩展

LDAP应用场景

0.单点登录(用户管理)
1.局域网资源统一管理

封装的简单PHP类

适合AD服务器 其他的LDAP服务器需要做相应的修改

zend框架有个开源的LDAP库实现 完全面向对象

  1 <?php
  2 /**
  3  * @description LDAP客户端类
  4  *
  5  * @author WadeYu
  6  * @date 2015-04-28
  7  * @version 0.0.1
  8  */
  9 class o_ldap{
 10     private $_conn = NULL;
 11     private $_sErrLog = '';
 12     private $_sOperLog = '';
 13     private $_aOptions = array(
 14         'host' => 'ldap://xxx.com',
 15         'port' => '389',
 16         'dnSuffix' => 'OU=xx,OU=xx,DC=xx,DC=com',
 17         'loginUser' => '',
 18         'loginPass' => '',
 19     );
 20     private $_aAllowAttrName = array(
 21         'objectClass',
 22         'objectGUID', //AD对象ID
 23         'userPassword', //AD密码不是这个字段 密码暂时不能通过程序设置
 24         'unicodePwd', //AD密码专用字段 $unicodePwd  = mb_convert_encoding('"' . $newPassword . '"', 'utf-16le');
 25         'cn', //comman name 兄弟节点不能相同
 26         'ou', //organizationalUnit
 27         'description', //员工填工号
 28         'displayName', //中文名
 29         'name', //姓名
 30         'sAMAccountName', //英文名(RTX账号,唯一)
 31         'userPrincipalName', //登陆用户名 和 英文名一致
 32         'ProtectedFromAccidentalDeletion', //对象删除保护
 33         'givenName', //
 34         'sn', //
 35         'employeeNumber', //一卡通卡号
 36         'mail',
 37         'mailNickname',
 38         'manager', //上级 (节点路径 示例:CN=Texas Poker9,OU=Texas Poker,OU=Dept,OU=BoyaaSZ,DC=by,DC=com)
 39         'title', //头衔
 40         'pager', //性别 0男 1女 -1未知
 41         'userAccountControl', //用户账号策略(暂时不能设置) 资料说明地址:https://support.microsoft.com/en-gb/kb/305144
 42         'department',
 43         'managedBy',//部门负责人
 44         'distinguishedName',
 45         'pwdLastSet', //等于0时 下次登录时需要修改密码
 46     );
 47     
 48     public function __construct(array $aOptions = array()){
 49         if (!extension_loaded('ldap')){
 50             $this->_log('LDAP extension not be installed.',true);
 51         }
 52         $this->setOption($aOptions);
 53     }
 54     
 55     /**
 56      * @return exit || true
 57      */
 58     public function connect($force = false){
 59         if (!$this->_conn || $force){
 60             $host = $this->_aOptions['host'];
 61             $port = $this->_aOptions['port'];
 62             $this->_conn = ldap_connect($host,$port);
 63             if ($this->_conn === false){
 64                 $this->_log("Connect LDAP SERVER Failure.[host:{
    $post}:{
    $port}]",true);
 65             }
 66             ldap_set_option($this->_conn, LDAP_OPT_PROTOCOL_VERSION, 3);
 67             ldap_set_option($this->_conn, LDAP_OPT_REFERRALS, 3);
 68             $this->_bind();
 69         }
 70         return true;
 71     }
 72     
 73     /**
 74      * @return exit || true
 75      */
 76     private function _bind(){
 77         $u = $this->_aOptions['loginUser'];
 78         $p = $this->_aOptions['loginPass'];
 79         $ret = @ldap_bind($this->_conn,$u,$p);
 80         if ($ret === false){
 81             $this->_log(__FUNCTION__.'----'.$this->_getLastExecErrLog().'----'."u:{
    $u},p:{
    $p}",true);
 82         }
 83         return $ret;
 84     }
 85     
 86     public function setOption(array $aOptions = array()){
 87         foreach($this->_aOptions as $k => $v){
 88             if (isset($aOptions[$k])){
 89                 $this->_aOptions[$k] = $aOptions[$k];
 90             }
 91         }
 92     }
 93     
 94     public function getOption($field,$default = ''){
 95         return isset($this->_aOptions[$field]) ? $this->_aOptions[$field] : $default;
 96     }
 97     
 98     /**
 99      * @description 查询$dn下符合属性条件的节点 返回$limit条
100      *
101      * @return array [count:x,[[prop:[count:xx,[],[]]],....]]
102      */
103     public function getEntryList($dn,$aAttrFilter,array $aField=array(),$limit = 0,$bFixedDn = true){
104         if (!$dn = trim($dn)){
105             return array();
106         }
107         if (!$this->_checkDn($dn)){
108             return array();
109         }
110         $limit = max(0,intval($limit));
111         $this->connect();
112         if ($bFixedDn){
113             $dn = $this->_getFullDn($dn);
114         }
115         $aOldTmp = $aAttrFilter;
116         $this->_checkAttr($aAttrFilter);
117         if (!$aAttrFilter){
118             $this->_log(__FUNCTION__.'---无效的搜索属性---'.json_encode($aOldTmp));
119             return array();
120         }
121         $sAttrFilter = $this->_mkAttrFilter($aAttrFilter);
122         $attrOnly = 0;
123         $this->_log(__FUNCTION__."---DN:{
    $dn}---sAttr:{
    $sAttrFilter}",false,'oper');
124         $rs = @ldap_search($this->_conn,$dn,$sAttrFilter,$aField,$attrOnly,$limit);
125         if ($rs === false){
126             $this->_log(__FUNCTION__."---dn:{
    $dn}---sAttr:{
    $sAttrFilter}---" . $this->_getLastExecErrLog());
127             return array();
128         }
129         $aRet = @ldap_get_entries($this->_conn,$rs);
130         ldap_free_result($rs);
131         if ($aRet === false){
132             $this->_log(__FUNCTION__.'---'.$this->_getLastExecErrLog());
133             return array();
134         }
135         return $aRet;
136     }
137     
138     /**
139      * @description 删除节点 暂时不考虑递归删除
140      * 
141      * @return boolean
142      */
143     public function delEntry($dn,$bFixedDn = true,$force = 0){
144         if (!$dn = trim($dn)){
145             return false;
146         }
147         if (!$this->_checkDn($dn)){
148             return false;
149         }
150         if ($bFixedDn){
151             $dn = $this->_getFullDn($dn);
152         }
153         $this->_log(__FUNCTION__."---DN:{
    $dn}",false,'oper');
154         $this->connect();
155         /*if($force){
156             $aEntryList = $this->getEntryList($dn,array('objectClass'=>'*'),array('objectClass'));
157             if ($aEntryList && ($aEntryList['count'] > 0)){ 
158                 for($i = 0; $i < $aEntryList['count']; $i++){
159                     $aDel[] = $aEntryList[$i]['dn'];
160                 }
161             }
162             $aDel = array_reverse($aDel); //默认顺序 祖先->子孙 需要先删除子孙节点
163             $ret = true;
164             foreach($aDel as $k => $v){
165                 $ret &= @ldap_delete($this->_conn,$v);
166             }
167             if ($ret === false){
168                 $this->_log(__FUNCTION__.'dn(recursive):'.$dn.'----'.$this->_getLastExecErrLog());
169             }
170             return $ret;
171         }*/
172         $ret = @ldap_delete($this->_conn,$dn);
173         if ($ret === false){
174             $this->_log(__FUNCTION__.'----dn:'.$dn.'-----'.$this->_getLastExecErrLog());
175         }
176         return $ret;
177     }
178     
179     /**
180      * @description 更新节点
181      * 
182      * @return boolean
183      */
184     public function updateEntry($dn,$aAttr = array(),$bFixedDn = true){
185         if (!$dn = trim($dn)){
186             return false;
187         }
188         $this->_checkAttr($aAttr);
189         if (!$aAttr){
190             return false;
191         }
192         if (!$this->_checkDn($dn)){
193             return false;
194         }
195         if ($bFixedDn){
196             $dn = $this->_getFullDn($dn);
197         }
198         $this->_log(__FUNCTION__."---DN:{
    $dn}---aAttr:".str_replace("\n",'',var_export($aAttr,true)),false,'oper');
199         $this->connect();
200         $ret = @ldap_modify($this->_conn,$dn,$aAttr);
201         if ($ret === false){
202             $this->_log(__FUNCTION__.'---'.$this->_getLastExecErrLog().'---dn:'.$dn.'---attr:'.json_encode($aAttr));
203         }
204         return $ret;
205     }
206     
207     /**
208      * @description 添加节点
209      *
210      * @return boolean
211      */
212     public function addEntry($dn,$aAttr = array(), $type = 'employee'/*employee,group*/){
213         if (!$dn = trim($dn)){
214             return false;
215         }
216         $this->_checkAttr($aAttr);
217         if (!$aAttr){
218             return false;
219         }
220         if (!$this->_checkDn($dn)){
221             return false;
222         }
223         $aAttr['objectClass'] = (array)$this->_getObjectClass($type);
224         $this->_log(__FUNCTION__."---DN:{
    $dn}---aAttr:".str_replace("\n",'',var_export($aAttr,true)),false,'oper');
225         $this->connect();
226         $dn = $this->_getFullDn($dn);
227         $ret = @ldap_add($this->_conn,$dn,$aAttr);
228         if ($ret === false){
229             $this->_log(__FUNCTION__.'----dn:'.$dn.'----aAttr:'.json_encode($aAttr).'-----'.$this->_getLastExecErrLog());
230         }
231         return $ret;
232     }
233     
234     /**
235      * @description 移动叶子节点 v3版才支持此方法
236      *
237      * @param $newDn 相对于$parentDn
238      * @param $parentDn 完整DN
239      * @param $bMoveRecur 
240      *
241      * @return boolean
242      */
243     public function moveEntry($oldDn,$newDn,$parentDn,$bDelOld = true,$bFixDn = true,$bMoveRecur = true){
244         //对于AD服务器 此方法可以移动用户节点以及组织节点
245         //$newDn只能包含一个 比如OU=xxx
246         $oldDn = trim($oldDn);
247         $newDn = trim($newDn);
248         $parentDn = trim($parentDn);
249         if(!$oldDn || !$newDn || ($bFixDn && !$parentDn)){
250             return false;
251         }
252         if(!$this->_checkDn($oldDn) || !$this->_checkDn($newDn) || !$this->_checkDn($parentDn)){
253             return false;
254         }
255         $this->connect();
256         if($bFixDn){
257             $oldDn = $this->_getFullDn($oldDn);
258             $parentDn = $this->_getFullDn($parentDn);
259         }
260         $this->_log(__FUNCTION__."---DN:{
    $oldDn} -> {
    $newDn},{
    $parentDn}",false,'oper');
261         $aTmpMove = $aDelDn = array();
262         $aTmpMove[] = array('old'=>$oldDn,'new'=>$newDn);
263         /*if($bMoveRecur){
264             $aDelDn[] = $oldDn;
265             $aTmpList = $this->getEntryList($oldDn,array('objectClass'=>'*'),array('objectClass'),0,0);
266             if($aTmpList && ($aTmpList['count'] > 1)){
267                 for($i = 1; $i < $aTmpList['count']; $i++){
268                     if(!in_array('user',$aTmpList[$i]['objectclass'])){ //$bDelOld=true时,用户节点移动时会自动删除
269                         $aDelDn[] = $aTmpList[$i]['dn'];
270                     }
271                     $aTmpSep = explode($oldDn,$aTmpList[$i]['dn']);
272                     $aTmpMove[] = array(
273                         'old' => $aTmpList[$i]['dn'],
274                         'new' => $aTmpSep[0] . $newDn,
275                     );
276                 }
277             }
278         }*/
279         $bFlag = true;
280         foreach($aTmpMove as $k => $v){
281             $bTmpFlag = ldap_rename($this->_conn,$v['old'],$v['new'],$parentDn,(boolean)$bDelOld);
282             if(!$bTmpFlag){
283                 $this->_log(__FUNCTION__."---o:{
    $v['old']}-n:{
    $v['new']}-p:{
    $parentDn}-recur:{
    $bMoveRecur}-----".$this->_getLastExecErrLog());
284             }
285             $bFlag &= $bTmpFlag;
286         }
287         /*if(!$bFlag){
288             $this->_log(__FUNCTION__."---o:{$oldDn}-n:{$newDn}-p:{$parentDn}-recur:{$bMoveRecur}-----".$this->_getLastExecErrLog());
289         }*/
290         /*if($bFlag && $bDelOld && $aDelDn){
291             $aDelDn = array_reverse($aDelDn);
292             foreach($aDelDn as $k => $v){
293                 $this->delEntry($v,false);
294             }
295         }*/
296         return $bFlag;
297     }
298     
299     public function modEntry($dn,$act = 'add',$aAttr = array()){
300         return false;
301         $dn = $this->_getFullDn($dn);
302         $this->_log(__FUNCTION__."---DN:{
    $dn}---Act:{
    $act}---aAttr:".str_replace("\n",'',var_export($aAttr,true)),false,'oper');
303         $this->connect();
304         $ret = false;
305         switch($act){
306             case 'add': $ret = ldap_mod_add($this->_conn,$dn,$aAttr); break;
307             case 'replace': $ret = ldap_mod_replace($this->_conn,$dn,$aAttr); break;
308             case 'del': $ret = ldap_mod_del($this->_conn,$dn,$aAttr); break;
309         }
310         if(!$ret){
311             $this->_log(__FUNCTION__."---dn:{
    $dn}---act:{
    $act}---attr:".json_encode($aAttr).'---'.$this->_getLastExecErrLog());
312         }
313         return $ret;
314     }
315     
316     /**
317      * @description 批量添加节点
318      * 
319      * @return boolean
320      */
321     public function addBatchEntry($aNodeList = array()){
322     }
323     
324     public function getAttrKv(array $aAttr = array()){
325         if(!isset($aAttr['count']) || ($aAttr['count'] < 1)){
326             return array();
327         }
328         $aRet = array();
329         for($i = 0; $i < $aAttr['count']; $i++){
330             $field = $aAttr[$i];
331             if (!isset($aAttr[$field])){
332                 return array();
333             }
334             unset($aAttr[$field]['count']);
335             $aRet[$field] = $aAttr[$field];
336         }
337         if(isset($aAttr['dn'])){ //dn是字符串
338             $aRet['dn'] = $aAttr['dn'];
339         }
340         return $aRet;
341     }
342     
343     private function _getObjectClass($type = 'employee'){
344         $aRet = array();
345         switch($type){
346             case 'employee' : $aRet = array('top','person','organizationalPerson','user'); break;
347             case 'group' : $aRet = array('top','organizationalUnit'); break;
348         }
349         return $aRet;
350     }
351     
352     public function getFullDn($partDn = ''){
353         return $this->_getFullDn($partDn);
354     }
355     
356     private function _getFullDn($partDn = ''){
357         $partDn = trim($partDn);
358         $partDn = rtrim($partDn,',');
359         return "{
    $partDn},{
    $this->_aOptions['dnSuffix']}";
360     }
361     
362     private function _checkDn($dn = ''){
363         $dn = trim($dn,',');
364         $aDn = explode(',',$dn);
365         foreach($aDn as $k => $v){
366             $aTmp = explode('=',$v);
367             $aTmp[0] = strtolower(trim($aTmp[0]));
368             $aTmp[1] = trim($aTmp[1]);
369             $flag = false;
370             switch($aTmp[0]){ //distingushed name 暂时只允许这3个field
371                 case 'dc': $flag = $this->_checkDc($aTmp[1]); break;
372                 case 'ou': $flag = $this->_checkOu($aTmp[1]); break;
373                 case 'cn': $flag = $this->_checkCn($aTmp[1]); break;
374             }
375             if (!$flag){
376                 $this->_log(__FUNCTION__.'----无效的节点路径----dn:'.$dn);
377                 return false;
378             }
379         }
380         return true;
381     }
382     
383     private function _checkOu($ou = ''){
384         if (!$ou){
385             return false;
386         }
387         if (preg_match('/[^a-zA-Z\s\d\.&\'\d]/',$ou)){
388             $this->_log(__FUNCTION__.'----OU只能包含字母数字空格以及点');
389             return false;
390         }
391         return true;
392     }
393     
394     private function _checkCn($cn = ''){
395         if (!$cn){
396             return false;
397         }
398         return true;
399     }
400     
401     private function _checkDc($dc = ''){
402         if (!$dc){
403             return false;
404         }
405         if (preg_match('/[^a-zA-Z]/',$dc)){
406             $this->_log(__FUNCTION__.'----DC只能包含英文字母');
407             return false;
408         }
409         return true;
410     }
411     
412     private function _mkAttrFilter(array $aAttrFilter = array()){
413         $sStr = '(&';
414         foreach($aAttrFilter as $k => $v){
415             $v = (string)$v;
416             if($k === 'objectGUID'){
417                 $v = $this->_GUIDtoStr($v);
418             }
419             $v = addcslashes($v,'()=');
420             $sStr .= "({
    $k}={
    $v})";
421         }
422         $sStr .= ')';
423         return $sStr;
424     }
425     
426     //来自PHP.NET http://php.net/manual/en/function.ldap-search.php
427     //http://php.net/manual/en/function.ldap-get-values-len.php
428     //GUID关键字
429     private function _GUIDtoStr($binary_guid){
430         $hex_guid = unpack("H*hex", $binary_guid); 
431         $hex = $hex_guid["hex"];
432         $j = 0;$str = '\\';
433         for($i = 0; $i < strlen($hex); $i++){
434             if($j == 2){
435                 $str .= '\\';
436                 $j = 0;
437             }
438             $str .= $hex[$i];
439             $j++;
440         }
441         return $str;
442         /*$hex1 = substr($hex, -26, 2) . substr($hex, -28, 2) . substr($hex, -30, 2) . substr($hex, -32, 2);
443         $hex2 = substr($hex, -22, 2) . substr($hex, -24, 2);
444         $hex3 = substr($hex, -18, 2) . substr($hex, -20, 2);
445         $hex4 = substr($hex, -16, 4);
446         $hex5 = substr($hex, -12, 12);
447         $guid_str = $hex1 . "-" . $hex2 . "-" . $hex3 . "-" . $hex4 . "-" . $hex5;
448         return $guid_str;*/
449     }
450     
451     private func
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值