Fork me on GitHub

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

前言

公司管理员工信息以及组织架构的后台系统要和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 function _checkAttr(& $aAttr = array()){
452         foreach((array)$aAttr as $k => $v){
453             if (!in_array($k,$this->_aAllowAttrName)){
454                 unset($aAttr[$k]);
455             }
456         }
457         return true;
458     }
459     
460     public function getErrLog(){
461         return $this->_sErrLog;
462     }
463     
464     public function getOperLog(){
465         return $this->_sOperLog;
466     }
467     
468     private function _log($str = '',$bExit = false,$type = 'err'/*err,oper*/){
469         if ($bExit){
470             die($str);
471         }
472         $date = date('Y-m-d H:i:s');
473         if($type === 'err'){
474             $this->_sErrLog .= "{$date}----{$str}\n";
475         } else if ($type === 'oper'){
476             $this->_sOperLog .= "{$date}----{$str}\n";
477         }
478     }
479     
480     public function close(){
481         ldap_close($this->_conn);
482     }
483     
484     private function _getLastExecErrLog(){
485         $no = ldap_errno($this->_conn);
486         $err = ldap_error($this->_conn);
487         return "---exec Error:{$no}---{$err}";
488     }
489 }
View Code

辅助PHP类---汉字转拼音

<?php
/**
 * @desc 汉字转拼音 参考 http://www.php100.com/html/webkaifa/PHP/PHP/2012/0820/10914.html
 */
class o_lib_helper_pinyin{
	private $_DataKey = "a|ai|an|ang|ao|ba|bai|ban|bang|bao|bei|ben|beng|bi|bian|biao|bie|bin|bing|bo|bu|ca|cai|can|cang|cao|ce|ceng|cha
		|chai|chan|chang|chao|che|chen|cheng|chi|chong|chou|chu|chuai|chuan|chuang|chui|chun|chuo|ci|cong|cou|cu|
		cuan|cui|cun|cuo|da|dai|dan|dang|dao|de|deng|di|dian|diao|die|ding|diu|dong|dou|du|duan|dui|dun|duo|e|en|er
		|fa|fan|fang|fei|fen|feng|fo|fou|fu|ga|gai|gan|gang|gao|ge|gei|gen|geng|gong|gou|gu|gua|guai|guan|guang|gui
		|gun|guo|ha|hai|han|hang|hao|he|hei|hen|heng|hong|hou|hu|hua|huai|huan|huang|hui|hun|huo|ji|jia|jian|jiang
		|jiao|jie|jin|jing|jiong|jiu|ju|juan|jue|jun|ka|kai|kan|kang|kao|ke|ken|keng|kong|kou|ku|kua|kuai|kuan|kuang
		|kui|kun|kuo|la|lai|lan|lang|lao|le|lei|leng|li|lia|lian|liang|liao|lie|lin|ling|liu|long|lou|lu|lv|luan|lue
		|lun|luo|ma|mai|man|mang|mao|me|mei|men|meng|mi|mian|miao|mie|min|ming|miu|mo|mou|mu|na|nai|nan|nang|nao|ne
		|nei|nen|neng|ni|nian|niang|niao|nie|nin|ning|niu|nong|nu|nv|nuan|nue|nuo|o|ou|pa|pai|pan|pang|pao|pei|pen
		|peng|pi|pian|piao|pie|pin|ping|po|pu|qi|qia|qian|qiang|qiao|qie|qin|qing|qiong|qiu|qu|quan|que|qun|ran|rang
		|rao|re|ren|reng|ri|rong|rou|ru|ruan|rui|run|ruo|sa|sai|san|sang|sao|se|sen|seng|sha|shai|shan|shang|shao|
		she|shen|sheng|shi|shou|shu|shua|shuai|shuan|shuang|shui|shun|shuo|si|song|sou|su|suan|sui|sun|suo|ta|tai|
		tan|tang|tao|te|teng|ti|tian|tiao|tie|ting|tong|tou|tu|tuan|tui|tun|tuo|wa|wai|wan|wang|wei|wen|weng|wo|wu
		|xi|xia|xian|xiang|xiao|xie|xin|xing|xiong|xiu|xu|xuan|xue|xun|ya|yan|yang|yao|ye|yi|yin|ying|yo|yong|you
		|yu|yuan|yue|yun|za|zai|zan|zang|zao|ze|zei|zen|zeng|zha|zhai|zhan|zhang|zhao|zhe|zhen|zheng|zhi|zhong|
		zhou|zhu|zhua|zhuai|zhuan|zhuang|zhui|zhun|zhuo|zi|zong|zou|zu|zuan|zui|zun|zuo";
	private $_DataValue = "-20319|-20317|-20304|-20295|-20292|-20283|-20265|-20257|-20242|-20230|-20051|-20036|-20032|-20026|-20002|-19990
		|-19986|-19982|-19976|-19805|-19784|-19775|-19774|-19763|-19756|-19751|-19746|-19741|-19739|-19728|-19725
		|-19715|-19540|-19531|-19525|-19515|-19500|-19484|-19479|-19467|-19289|-19288|-19281|-19275|-19270|-19263
		|-19261|-19249|-19243|-19242|-19238|-19235|-19227|-19224|-19218|-19212|-19038|-19023|-19018|-19006|-19003
		|-18996|-18977|-18961|-18952|-18783|-18774|-18773|-18763|-18756|-18741|-18735|-18731|-18722|-18710|-18697
		|-18696|-18526|-18518|-18501|-18490|-18478|-18463|-18448|-18447|-18446|-18239|-18237|-18231|-18220|-18211
		|-18201|-18184|-18183|-18181|-18012|-17997|-17988|-17970|-17964|-17961|-17950|-17947|-17931|-17928|-17922
		|-17759|-17752|-17733|-17730|-17721|-17703|-17701|-17697|-17692|-17683|-17676|-17496|-17487|-17482|-17468
		|-17454|-17433|-17427|-17417|-17202|-17185|-16983|-16970|-16942|-16915|-16733|-16708|-16706|-16689|-16664
		|-16657|-16647|-16474|-16470|-16465|-16459|-16452|-16448|-16433|-16429|-16427|-16423|-16419|-16412|-16407
		|-16403|-16401|-16393|-16220|-16216|-16212|-16205|-16202|-16187|-16180|-16171|-16169|-16158|-16155|-15959
		|-15958|-15944|-15933|-15920|-15915|-15903|-15889|-15878|-15707|-15701|-15681|-15667|-15661|-15659|-15652
		|-15640|-15631|-15625|-15454|-15448|-15436|-15435|-15419|-15416|-15408|-15394|-15385|-15377|-15375|-15369
		|-15363|-15362|-15183|-15180|-15165|-15158|-15153|-15150|-15149|-15144|-15143|-15141|-15140|-15139|-15128
		|-15121|-15119|-15117|-15110|-15109|-14941|-14937|-14933|-14930|-14929|-14928|-14926|-14922|-14921|-14914
		|-14908|-14902|-14894|-14889|-14882|-14873|-14871|-14857|-14678|-14674|-14670|-14668|-14663|-14654|-14645
		|-14630|-14594|-14429|-14407|-14399|-14384|-14379|-14368|-14355|-14353|-14345|-14170|-14159|-14151|-14149
		|-14145|-14140|-14137|-14135|-14125|-14123|-14122|-14112|-14109|-14099|-14097|-14094|-14092|-14090|-14087
		|-14083|-13917|-13914|-13910|-13907|-13906|-13905|-13896|-13894|-13878|-13870|-13859|-13847|-13831|-13658
		|-13611|-13601|-13406|-13404|-13400|-13398|-13395|-13391|-13387|-13383|-13367|-13359|-13356|-13343|-13340
		|-13329|-13326|-13318|-13147|-13138|-13120|-13107|-13096|-13095|-13091|-13076|-13068|-13063|-13060|-12888
		|-12875|-12871|-12860|-12858|-12852|-12849|-12838|-12831|-12829|-12812|-12802|-12607|-12597|-12594|-12585
		|-12556|-12359|-12346|-12320|-12300|-12120|-12099|-12089|-12074|-12067|-12058|-12039|-11867|-11861|-11847
		|-11831|-11798|-11781|-11604|-11589|-11536|-11358|-11340|-11339|-11324|-11303|-11097|-11077|-11067|-11055
		|-11052|-11045|-11041|-11038|-11024|-11020|-11019|-11018|-11014|-10838|-10832|-10815|-10800|-10790|-10780
		|-10764|-10587|-10544|-10533|-10519|-10331|-10329|-10328|-10322|-10315|-10309|-10307|-10296|-10281|-10274
		|-10270|-10262|-10260|-10256|-10254";
	private $_Data = array();
	
	public function __construct(){
		$_TDataKey = explode('|', str_replace(array("\t","\n","\r\n",' '),array('','','',''),$this->_DataKey));
		$_TDataValue = explode('|', str_replace(array("\t","\n","\r\n",' '),array('','','',''),$this->_DataValue));
		$_Data = (PHP_VERSION>='5.0') ? array_combine($_TDataKey, $_TDataValue) : $this->_Array_Combine($_TDataKey, $_TDataValue);
		arsort($_Data);
		$this->_Data = $_Data;
	}
	
	public function Pinyin($_String, $_Code='gb2312'){
		reset($this->_Data);
		if($_Code != 'gb2312') $_String = $this->_U2_Utf8_Gb($_String);
		$_Res = '';$_aRes = array();$_en = '';
		for($i=0; $i<strlen($_String); $i++)
		{
			$_P = ord(substr($_String, $i, 1));
			if($_P < 160){
				$_en .= chr($_P);
				continue;
			}
			if($_en){
				$_aRes[] = $_en;
				$_en = '';
			}
			if($_P>160) { $_Q = ord(substr($_String, ++$i, 1)); $_P = $_P*256 + $_Q - 65536; }
			//$_Res .= _Pinyin($_P, $_Data);
			$_aRes[] = $this->_Pinyin($_P, $this->_Data);
		}
		foreach($_aRes as $k => $v){
			$v = preg_replace("/[^a-zA-Z0-9]*/", '', $v);
			$v = ucfirst($v);
			$_aRes[$k] = $v;
		}
		return implode(' ',$_aRes);
		//return preg_replace("/[^a-zA-Z0-9]*/", '', $_Res);
	}
	
	private function _Pinyin($_Num, $_Data){
		if ($_Num>0 && $_Num<160 ) return chr($_Num);
		elseif($_Num<-20319 || $_Num>-10247) return '';
		else {
		foreach($_Data as $k=>$v){ if($v<=$_Num) break; }
		return $k;
		}
	}
	
	private function _U2_Utf8_Gb($_C){
		$_String = '';
		if($_C < 0x80) $_String .= $_C;
		elseif($_C < 0x800)
		{
			$_String .= chr(0xC0 | $_C>>6);
			$_String .= chr(0x80 | $_C & 0x3F);
		}elseif($_C < 0x10000){
			$_String .= chr(0xE0 | $_C>>12);
			$_String .= chr(0x80 | $_C>>6 & 0x3F);
			$_String .= chr(0x80 | $_C & 0x3F);
		} elseif($_C < 0x200000) {
			$_String .= chr(0xF0 | $_C>>18);
			$_String .= chr(0x80 | $_C>>12 & 0x3F);
			$_String .= chr(0x80 | $_C>>6 & 0x3F);
			$_String .= chr(0x80 | $_C & 0x3F);
		}
		return iconv('UTF-8', 'GB2312', $_String);
	}
	
	private function _Array_Combine($_Arr1, $_Arr2){
		for($i=0; $i<count($_Arr1); $i++) $_Res[$_Arr1[$i]] = $_Arr2[$i];
		return $_Res;
	}
}

辅助PHP类---简单TREE处理

<?php
/**
 * @description 简单tree类
 *
 * @author WadeYu
 * @date 2015-04-30
 * @version 0.0.1
 */
class o_simpletree{
	private $_aNodeList = array(); // [id => []]
	private $_aChildNodeList = array(); // [parentId => [childId,childId,]]
	private $_aTopNodeList = array(); //[id => []]
	private $_pk = '';
	private $_parentPk = '';
	private $_aTreeKV = array(); //tree key的显示的内容
	private $_aJoinKey = array();
	
	public function __construct(array $aData = array()/*[[id:0,fid:1],[]]*/, array $aOption = array()){
		$this->_pk = $aOption['pk'];
		$this->_parentPk = $aOption['parentPk'];
		$this->_aTreeKV = (array)$aOption['aTreeKV'];
		$this->_mkNodeList($aData);
		$this->_mkChildNodeList();
	}
	
	public function getTree($parentPk){
		$aRet = array();
		$aChild = array();
		if (!isset($this->_aChildNodeList[$parentPk])){
			return $aRet;
		}
		$aChild = $this->_aChildNodeList[$parentPk];
		foreach((array)$aChild as $k => $v){
			$currNode = $this->_aNodeList[$v];
			$tmpK = '';
			$i = 0;
			foreach($this->_aTreeKV as $k2 => $v2){
				$tmpV = $currNode[$v2];
				if($i == 0){
					$tmpK .= $tmpV;
				} else if ($i == 1){
					$tmpK .= "({$tmpV})";
				} else if ($i == 2){
					$tmpK .= "[{$tmpV}]";
				}
				$i++;
			}
			if (isset($this->_aChildNodeList[$v])){
				$aRet[$tmpK] = $this->getTree($v);
			} else {
				$aRet[$tmpK] = 1;
			}
		}
		return $aRet;
	}
	
	public function joinKey($aTree,$prefix = ''){
		$prefix = trim($prefix);
		if(!is_array($aTree)){
			return $prefix;
		}
		foreach((array)$aTree as $k => $v){
			if (is_array($v)){
				$this->joinKey($v,"{$prefix}{$k}/");
			} else {
				$this->_aJoinKey["{$prefix}{$k}"] = 1;
			}
		}
		return true;
	}
	
	public function getJoinKey(){
		return $this->_aJoinKey;
	}
	
	private function _mkNodeList(array $aData = array()){
		foreach($aData as $k => $v){
			$this->_aNodeList[$v[$this->_pk]] = $v;
		}
	}
	
	private function _mkChildNodeList(){
		foreach($this->_aNodeList as $k => $v){
			if ($v['fid']){
				$this->_aChildNodeList[$v[$this->_parentPk]][] = $v[$this->_pk];
			} else {
				$this->_aTopNodeList[$v[$this->_pk]] = $v;
			}
		}
	}
}

  

 

 

LDAP开源实现:openLDAP

0.相关环境说明
    a.操作系统

1 [root@vm ldap]# lsb_release -a
2 LSB Version:    :core-3.1-amd64:core-3.1-ia32:core-3.1-noarch:graphics-3.1-amd64:graphics-3.1-ia32:graphics-3.1-noarch
3 Distributor ID:    CentOS
4 Description:    CentOS release 5.4 (Final)
5 Release:    5.4
6 Codename:    Final

 1.安装

    a.yum -y install openldap-servers openldap-clients

2.配置
    a.配置HOST
        [root@vm ldap]# vi /etc/hosts
       127.0.0.1 test.com
     b.创建证书

cd /etc/pki/tls/certs
[root@vm certs]# pwd
/etc/pki/tls/certs
[root@vm certs]# rm -rf slapd.pem
[root@vm certs]# make slapd.pem
#执行命令之后 显示如下信息 按照提示填写即可
umask 77 ; \
	PEM1=`/bin/mktemp /tmp/openssl.XXXXXX` ; \
	PEM2=`/bin/mktemp /tmp/openssl.XXXXXX` ; \
	/usr/bin/openssl req -utf8 -newkey rsa:2048 -keyout $PEM1 -nodes -x509 -days 365 -out $PEM2 -set_serial 0 ; \
	cat $PEM1 >  slapd.pem ; \
	echo ""    >> slapd.pem ; \
	cat $PEM2 >> slapd.pem ; \
	rm -f $PEM1 $PEM2
Generating a 2048 bit RSA private key
.....................................+++
.........+++
writing new private key to '/tmp/openssl.IQ8972'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [GB]:CN
State or Province Name (full name) [Berkshire]:GuangDong
Locality Name (eg, city) [Newbury]:ShenZhen
Organization Name (eg, company) [My Company Ltd]:Boyaa
Organizational Unit Name (eg, section) []:OA
Common Name (eg, your name or your server's hostname) []:Test LDAP  
Email Address []:WadeYu@boyaa.com
[root@vm certs]#

   c.生成管理员密码

            root@vm ~]# slappasswd 

            New password:
           Re-enter new password:
          {SSHA}2eG1IBeHhSjfgS7pjoAci1bHz5p4AVeS

    d.配置slapd.conf
        [root@vm certs]# vi /etc/openldap/slapd.conf

       去掉TLS相关注释

       设置数据库配置

    e.BerkeleyDb配置

        [root@vm certs]# cd /etc/openldap/
        [root@vm openldap]# mv ./DB_CONFIG.example /var/lib/ldap/DB_CONFIG

    f.配置ldap.conf

        [root@vm openldap]# vi ldap.conf

    g.开启加密支持
        [root@vm ~]# vim /etc/sysconfig/ldap
        SLAPD_LDAPS=yes

3.使用slapadd命令添加根节点 未启动前

1 [root@vm ~]# cd ~
2 [root@vm ~]# vim root.ldif
3 dn: dc=test,dc=com
4 dc: test
5 objectClass: dcObject
6 objectClass: organizationalUnit
7 ou: test.com
8 [root@vm ~]# slapadd -v -n 1 -l root.ldif

4.启动slapd
[root@vm ~]# slapd -h "ldap:// ldaps://"
389非加密端口 636加密端口

5.检测

[root@vm ~]# ldapsearch -x -H ldap://localhost
# extended LDIF
#
# LDAPv3
# base <> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# test.com
dn: dc=test,dc=com
dc: test
objectClass: dcObject
objectClass: organizationalUnit
ou: test.com

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

[root@vm ~]# ldapsearch -x -H ldaps://localhost
# extended LDIF
#
# LDAPv3
# base <> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# test.com
dn: dc=test,dc=com
dc: test
objectClass: dcObject
objectClass: organizationalUnit
ou: test.com

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

  

6.phpldapadmin客户端访问
    a.官网下载源码放入WEB目录下 下载页面:http://phpldapadmin.sourceforge.net/wiki/index.php/Download
    b.安装依赖的扩展gettext ldap这2个扩展
    c.按需配置 源码目录下config/config.php

问题

1.ldaps无法访问
/etc/openldap/ldap.conf TLS_REQCERT never加上这个

源码安装PHP扩展步骤

1.下载PHP源码
2.切换到扩展源码所在目录 示例:cd /src path/ext/extname
3.执行phpize命令 (1.PHP源代码或者PHP安装目录包含此命令 2.当前目录会生成一个检查编译扩展的环境脚本configure)
4../configure --with-php-config=/usr/local/php/bin/php-config (1.编译环境配置检查 2.生成make命令需要的编译配置文件makefile)
5.make && make install (编译安装)
6.php.ini 加上配置 extension = "xxxxx.so"
7.重启php service php-fpm restart

安装PHP LDAP扩展

示例:centos系统安装LDAP扩展
1.安装依赖库openldap openldap-devel
yum install openldap
yum install openldap-devel
2.参考:源码安装PHP扩展步骤

 

补充(最新LDAP操作类LARAVEL框架版)

LDAP基础类

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

组织架构信息更新类

   1 <?php
   2 /**
   3  * @description 测试用例说明
   4  *
   5  * @组织节点
   6  * 0.添加是否正常
   7  * 1.修改节点是否正常
   8  *   1.0修改了中文名称是否正常
   9  *     1.1修改了其它属性是否正常
  10  *     1.2修改了上级组织是否正常
  11  *   1.3修改了需要移动节点的属性也修改了其它属性是否正常
  12  * 2.移动节点是否正常
  13  *   2.0参考编号1
  14  * 3.删除节点是否正常
  15  *   3.0暂时未提供删除入口 
  16  * 4.没有修改时是否有更新操作
  17  *   4.0原则上没有修改不应该更新AD服务器
  18  *
  19  * @用户节点
  20  * 0.添加是否正常
  21  *   0.0敏感属性设置是否正常 例如用户密码 用户账号策略控制
  22  *   0.1RTX账号不能重复
  23  *   0.2没有填的项不能设置 否则会覆盖掉老数据
  24  *   0.3同一父节点下中文名称不能相同
  25  * 1.修改或者移动是否正常
  26  *   1.0RTX账号必须唯一
  27  *   1.1姓名同一父节点下必须唯一
  28  *   1.2修改了姓名或者修改了所在部门需要移动节点 
  29  *   1.3修改了其它属性 执行更新操作
  30  *   1.4没有属性修改 原则上不更新AD服务器
  31  *   1.5设置敏感属性信息是否正常
  32  * 2.删除是否正常
  33  *   2.0暂时未提供入口
  34  *
  35  * @description 碰到的问题说明
  36  *
  37  * 0.敏感属性只能通过加密连接设置
  38  * 1.解决中文乱码 移动节点 只有LDAPV3协议支持
  39  * 2.运维建议尽量只进行更新操作 域服务器OBJECTGUID数量有限制 超过了域服务器就不能使用了
  40  */
  41 namespace App\Lib;
  42 use App\Lib\Ldap;
  43 use App\Lib\Pinyin;
  44 use App\Lib\fn;
  45 use DB;
  46 use App\Http\Model\Group;
  47 use App\Http\Model\User;
  48 use Mail;
  49 class Hrldap{
  50     private static $_aCfg = array();
  51     private static $_oLdap = null;
  52     private $_adGuidField = 'adguid';
  53     private $_tbluser = 'pb_user';
  54     private $_tblgroup = 'pb_group';
  55     private $_isTestSvr = false;
  56     public $aOpertor = array();
  57     const STATUS_USER_LEAVE = 2; //员工离职状态
  58     const STATUS_USER_NOLEAVE = 1; //员工在职状态
  59     const STATUS_GROUP_DEL = 1; //组织架构删除状态
  60     
  61     public function __construct(){
  62         if (!self::$_aCfg){
  63             self::$_aCfg = config('ldap');
  64         }
  65         if (self::$_oLdap === null){
  66             self::$_oLdap = new Ldap($this->_getReMapCfg());
  67         }
  68         $this->_isTestSvr = (env('APP_ENV') == 'local');
  69         $this->aOpertor = ['username'=>'cron','id'=>0]; //设置一个默认值
  70     }
  71     
  72     private function _getReMapCfg(){
  73         return array(
  74             'host' => self::$_aCfg['ldapHost'],
  75             'port' => self::$_aCfg['ldapPort'], 
  76             'dnSuffix' => self::$_aCfg['ldapDnSuffix'],
  77             'loginUser' => self::$_aCfg['ldapUser'],
  78             'loginPass' => self::$_aCfg['ldapPass'],
  79             'logDir' => self::$_aCfg['logDir'],
  80         );
  81     }
  82     
  83     public function getCfg($field = ''){
  84         if($field && isset(self::$_aCfg[$field])){
  85             return self::$_aCfg[$field];
  86         }
  87         return self::$_aCfg;
  88     }
  89     
  90     /**
  91      * @desc 通用入口
  92      *
  93      */
  94     public function apiHr(array &$newData, $type = 'group'/*group,user*/,$aOpertor = [],$bLog = true){
  95         //$newData 数据库最新的数据
  96         $this->aOpertor = $aOpertor;
  97         try{
  98             switch($type){
  99                 case 'group':
 100                     $aRet = $this->_frmGroupData($newData);
 101                 break;
 102                 case 'user':
 103                     $aRet = $this->_frmUserData($newData);
 104                 break;
 105             }
 106             if($bLog){
 107                 //$this->_log('endToFile');
 108                 $this->_log('ldap');
 109                 //$this->_log('ldapOperLog');
 110                 $this->_sendMonitorMail();
 111             }
 112             return $aRet;
 113         }catch(Exception $ex){
 114             if($bLog){
 115                 //$this->_log('endToFile');
 116                 $this->_log('ldap');
 117                 $this->_sendMonitorMail();
 118             }
 119             return $this->_genRet(-9999,$ex->getMessage());
 120         }
 121     }
 122     
 123     public function afterHandleCb(){
 124         //$this->_log('endToFile');
 125         $this->_log('ldap');
 126         //$this->_log('ldapOperLog');
 127         $this->_sendMonitorMail();
 128     }
 129     
 130     /**
 131      * @desc 批量更新通用入口
 132      *
 133      * @param $aData [type:[id:{},...,]]
 134      *
 135      */
 136     public function apiHrBat(array &$aData,$aOpertor = []){
 137         if(!$aData || !is_array($aData)){
 138             return false;
 139         }
 140         $this->aOpertor = $aOpertor;
 141         $aType = array('group' => $this->_tblgroup,'user' => $this->_tbluser);
 142         $aColumns = array(
 143             'group'=>'*',
 144             'user'=>'id,username,cname,sex,groupid,position,mposition,utype,boss,phone,email,status,degree',
 145         );
 146         $bTmp = true;
 147         $st = microtime(true);
 148         foreach($aData as $k => $v){
 149             if(!isset($aType[$k])){
 150                 continue;
 151             }
 152             $aTmpIds = array_keys($v);
 153             if(!$aTmpIds){
 154                 continue;
 155             }
 156             $sTmpIds = implode(',',$aTmpIds);
 157             $aTmpList = DB::select( "SELECT {$aColumns[$k]} FROM {$aType[$k]} WHERE id in ({$sTmpIds})" );
 158             foreach((array)$aTmpList as $k2 => $v2){
 159                 $aTmpRet = array();
 160                 switch($k){
 161                     case 'group': $aTmpRet = $this->_frmGroupData((array)$v2); break;
 162                     case 'user': $aTmpRet = $this->_frmUserData((array)$v2); break;
 163                 }
 164                 $bTmp &= (($aTmpRet['sts'] === 0) ? true : false);
 165             }
 166         }
 167         $et = microtime(true);
 168         //$this->_log('endToFile');
 169         $this->_log('ldap');
 170         $this->_log('def','def',array('f'=>'aApiHrBatCnt.log','s'=>date('Y-m-d H:i:s').'---批量更新消耗时间:'.($et-$st)."---更新状态:{$bTmp}\n"));
 171         $this->_log('def','def',array('f'=>'aApiHrBatData.log','s'=>date('Y-m-d H:i:s').json_encode($aData)."\n"));
 172         //$this->_log('ldapOperLog');
 173         $this->_sendMonitorMail();
 174         return $bTmp;
 175     }
 176     
 177     /**
 178      * @desc 根据数据库数据批量更新信息到AD服务器(组织节点)
 179      *
 180      * @return boolean
 181      */
 182     public function apiSyncOuToAd(){    
 183     }
 184     
 185     /**
 186      * @desc 根据数据库数据批量更新信息到AD服务器(员工节点)
 187      *
 188      * @return boolean
 189      */
 190     public function apiSyncUserToAd($bOnlyUpdate = false,$aUpdateField = array(),$aUid = array()){ 
 191     }
 192     
 193     public function getLdapObj(){
 194         return self::$_oLdap;
 195     }
 196     
 197     /**
 198      * @desc 灰度开放部门
 199      *
 200      */
 201     private function _checkOpOk($id = 1){//组织ID
 202         return true; //开启全部部门
 203         $aOuList = $this->_getOuList();
 204         if(!isset($aOuList[$id])){
 205             return false;
 206         }
 207         $aIdLvl = array();
 208         $aTmpOu = $aOuList[$id];
 209         while($aTmpOu){
 210             $aIdLvl[] = $aTmpOu['id'];
 211             $aTmpOu = $aOuList[$aTmpOu['fid']];
 212         }
 213         $topId = array_pop($aIdLvl);
 214         if(in_array($topId,array(328,/*业务支持中心*/))){ //暂时开放这些部门
 215             return true;
 216         }
 217         return false;
 218     }
 219     
 220     private $_aNewestAdguidMap = array();
 221     private function _frmGroupData(array &$newData){
 222         $sNewDes = $newData['name'] = trim($newData['name']);
 223         $iBoss = $newData['boss'] = intval($newData['boss']);
 224         $id = $newData['id'] = intval($newData['id']);
 225         if( !$sNewDes || !$id /*|| !$iBoss*/){
 226             return $this->_genRet(-1);
 227         }
 228         if(!$this->_checkOpOk($id)){ //灰度测试 暂时只对部分部门开放
 229             return $this->_genRet(0);
 230         }
 231         if($newData['del'] == self::STATUS_GROUP_DEL){ //关闭节点 有在职员工 AD不移动到LEFTOUT
 232             if( count(User::getOcolsByGroup($id)) > 0 ){
 233                 return $this->_genRet(0);
 234             }
 235         }
 236         //关闭节点 移动节点到OU=OU,OU=HasLeft
 237         /*if($newData['del'] == 1){//关闭节点 AD服务器不做处理
 238             return $this->_genRet(0);
 239         }*/
 240         //AD服务器老节点存在 执行更新移动操作
 241         //否则走添加新节点流程
 242         //新添加节点业务机需要保存节点GUID
 243         if(isset($this->_aNewestAdguidMap[$id])){ //循环更新的话ADGUID可能会变化
 244             $newData[$this->_adGuidField] = $this->_aNewestAdguidMap[$id];
 245         }
 246         $sNewAdName = $this->_getOuPinYin($sNewDes,$id);
 247         $this->_getOuList($newData);//更新OU LIST 节点缓存
 248         $aNewAttr = $this->_getGroupData($sNewDes,$sNewAdName,$iBoss);
 249         $sGuidField = $this->_adGuidField;
 250         $sAdGuid = $newData[$sGuidField] ? $this->_decodeAdGuid($newData[$sGuidField]) : ''; //AD服务器节点唯一标识
 251         $aOldEntry = array();
 252         $sTopOu = self::$_oLdap->getOption('dnSuffix');
 253         if($sAdGuid){ //没找到是否需要退出 ?
 254             $aTmpEntryList = self::$_oLdap->getEntryList($sTopOu,array('objectClass'=>'organizationalUnit','objectGUID'=>$sAdGuid),array(),0,0);
 255             if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){
 256                 $aOldEntry = $aTmpEntryList[0];
 257             }
 258         }
 259         if(($newData['del'] == self::STATUS_GROUP_DEL) && !$aOldEntry){ //关闭节点 域中节点不存在的话 不做任何处理
 260             return $this->_genRet(0);
 261         }
 262         if($aOldEntry){ //AD服务器节点已存在 更新或者移动
 263             $sTmpOldDn = str_replace(",{$sTopOu}",'',$aOldEntry['dn']);
 264             $aTmpAttr = $this->_getAttrDiff($aNewAttr,$aOldEntry);
 265             unset($aTmpAttr['name']); //恶魔属性 不能更新
 266             $bTmp = true;
 267             if($aTmpAttr && ($newData['del'] != self::STATUS_GROUP_DEL)){
 268                 $bTmp = self::$_oLdap->updateEntry($sTmpOldDn,$aTmpAttr);
 269                 $this->_log('update','group',array('flag'=>$bTmp,'dn'=>$sTmpOldDn,'attr'=>$aTmpAttr),$newData,$aOldEntry);
 270             }
 271             if(!$bTmp){
 272                 return $this->_genRet(-2,'部门ID:'.$id.'---节点更新失败---line:'.__LINE__);
 273             }
 274             $sTmp2 = "OU={$sNewAdName}";
 275             if($newData['del'] == self::STATUS_GROUP_DEL){ //关闭节点 把节点移动到OU=OU,OU=HasLeft
 276                 $sTmp4 = self::$_aCfg['ldapDnHasLeftOu'];
 277             }else{
 278                 $sTmp4 = $this->_getOuDn($newData['fid']);
 279             }
 280             $sTmp5 = $sTmp2 . ($sTmp4 ? ",{$sTmp4}" : '');
 281             if(strpos($sTmpOldDn,$sTmp5) === false){ //DN变了 需要移动节点
 282                 $bTmp = self::$_oLdap->moveEntry($sTmpOldDn,$sTmp2,$sTmp4);
 283                 $this->_log('move','group',array('flag'=>$bTmp,'path'=>"{$sTmpOldDn}->{$sTmp5}"),$newData,$aOldEntry);
 284             }
 285             return $this->_genRet($bTmp ? 0 : -3,'部门ID:'.$id.'---移动节点失败---line:'.__LINE__);
 286         }
 287         //走到这一步只剩下添加节点操作了
 288         //父级节点不存在自动递归创建
 289         //否则新组织节点也创建不了
 290         $aAdd = array();
 291         $aOuList = $this->_getOuList(); //数据库中的组织架构列表
 292         $aTmpOu = $newData;
 293         do{
 294             if($aTmpOu[$sGuidField]){ //数据库中存在域对象的GUID AD域中存在的概率99% 还需要走AD服务器验证下?
 295                 $bTmpBreak = true;
 296                 if($this->_isTestSvr){
 297                     $aTmpFilter = array('objectClass'=>'organizationalUnit','objectGUID'=>$this->_decodeAdGuid($aTmpOu[$sGuidField]));
 298                     $aTmpEntryList = self::$_oLdap->getEntryList($sTopOu,$aTmpFilter,array(),0,0);
 299                     if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){    
 300                     } else {
 301                         $bTmpBreak = false;
 302                     }
 303                 }
 304                 if($bTmpBreak){
 305                     break;
 306                 }
 307             }
 308             $aAdd[] = $aTmpOu;
 309             $aTmpOu = isset($aOuList[$aTmpOu['fid']]) ? $aOuList[$aTmpOu['fid']] : [];
 310         }while($aTmpOu);
 311         if(!$aAdd){
 312             return $this->_genRet(0);
 313         }
 314         $aAdd = array_reverse($aAdd); //祖先节点要先添加 
 315         $bTmpFlag = true;
 316         $sPreFixDn = $this->_getOuDn($aAdd[0]['fid']);
 317         if(!$sPreFixDn){
 318             return $this->_genRet(-6,'部门ID:'.$id.'---新建节点失败---取第一个父节点(fid:'.$aAdd[0]['fid'].')DN失败');
 319         }
 320         foreach($aAdd as $k => $v){
 321             $sTmpOuPinYin = $this->_getOuPinYin($v['name'],$v['id']);
 322             $aTmpAttr = $this->_getGroupData($v['name'],$sTmpOuPinYin,$v['boss']);
 323             //$sTmpDn = $this->_getOuDn($v['id']);
 324             $sTmpDn = "OU={$sTmpOuPinYin}," . $sPreFixDn;
 325             $bTmpFlag2 = self::$_oLdap->addEntry($sTmpDn,$aTmpAttr,'group');
 326             $bTmpFlag = $bTmpFlag2;
 327             $this->_log('add','group',array('flag'=>$bTmpFlag2,'dn'=>$sTmpDn,'attr'=>$aTmpAttr),$v,array());
 328             if($bTmpFlag2){ //更新业务机组织表AD节点objectGUID
 329                 $this->_addAdGuidMap($sTmpDn,$v['id'],array('name'=>$aTmpAttr['name'][0]),'group',true);
 330             } else { //父节点都更新失败,子节点无地方落脚啊,果断不往下走
 331                 break;
 332             }
 333             $sPreFixDn = $sTmpDn;
 334         }
 335         return $this->_genRet($bTmpFlag ? 0 : -5,'部门ID:'.$id.'---新建节点失败');
 336     }
 337     
 338     private function _encodeAdGuid($str){
 339         return base64_encode($str);
 340     }
 341     
 342     private function _decodeAdGuid($str){
 343         return base64_decode($str);
 344     }
 345     
 346     private $_aCachePinYin = array();
 347     private function _getOuPinYin($name,$id){
 348         if(isset($this->_aCachePinYin[$id])){
 349             return $this->_aCachePinYin[$id];
 350         }
 351         $this->_aCachePinYin[$id] = (new Pinyin())->Pinyin($name,1) . " {$id}"; //加上ID后缀 避免名称冲突
 352         return $this->_aCachePinYin[$id];
 353     }
 354     
 355     private function _addAdGuidMap($dn,$id,$aAttrFilter = array(),$type = 'group'/*group,user*/,$bUpdateGuid = false){
 356         $adGuid = '';
 357         $sErrFile = 'aLdapAddAdGuidMapErr.log';
 358         $aTmpEntryList = self::$_oLdap->getEntryList($dn,$aAttrFilter,array('objectGUID'));
 359         if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){
 360             $adGuid = $aTmpEntryList[0]['objectguid'][0];
 361         }
 362         if(!$adGuid){
 363             $this->_debug(date('Y-m-d H:i:s')."---dn:{$dn}---id:{$id}---type:{$type}---取objectGUID失败\n",$sErrFile);
 364             return false;
 365         }
 366         $aMapTbl = array(
 367             'group' => $this->_tblgroup,
 368             'user' => $this->_tbluser,
 369         );
 370         if(!isset($aMapTbl[$type])){
 371             return false;
 372         }
 373         $tbl = $aMapTbl[$type];
 374         $adGuid = $this->_encodeAdGuid($adGuid);
 375         $ret = DB::update( "UPDATE {$tbl} SET {$this->_adGuidField} = '{$adGuid}' WHERE id = '{$id}'" );
 376         if(!$ret){
 377             $this->_debug(date('Y-m-d H:i:s')."---dn:{$dn}---id:{$id}---type:{$type}---guid:{$adGuid}---objectGUID入库失败\n",$sErrFile);
 378         }
 379         if($bUpdateGuid && $ret){
 380             switch($type){
 381                 case 'group': 
 382                     $aTmpOuList = $this->_getOuList();
 383                     $aTmpOu = $aTmpOuList[$id];
 384                     $aTmpOu[$this->_adGuidField] = $adGuid;
 385                     $this->_getOuList($aTmpOu);
 386                     $this->_aNewestAdguidMap[$id] = $adGuid;
 387                     break;
 388             }
 389         }
 390         return $ret;
 391     }
 392     
 393     private function _debug($msg,$sFile){
 394         $this->_log('def','def',array('f'=>$sFile,'s'=>$msg));
 395     }
 396     
 397     /**
 398      * @desc 拼接域节点路径
 399      *
 400      */
 401     private $_aCacheOuDn = array();
 402     private function _getOuDn($id){
 403         if($id == 1){ //一级部门OU = Dept;
 404             return self::$_aCfg['ldapDnDept']; //组织架构DN专有后缀;
 405         }
 406         $aRet = array();
 407         $aOuList = (array)$this->_getOuList();
 408         $aTmpOu = $aOuList[$id];
 409         if (!$aTmpOu){
 410             throw new Exception(__FUNCTION__.'----非法的组织ID:'.$id.'---line:'.__LINE__);
 411         }
 412         $adguid = 'x';
 413         if($aTmpOu[$this->_adGuidField]){
 414             $adguid = $aTmpOu[$this->_adGuidField];
 415         }
 416         if(isset($this->_aCacheOuDn[$id][$adguid])){
 417             return $this->_aCacheOuDn[$id][$adguid];
 418         }
 419         if(!isset($this->_aCacheOuDn[$id])){
 420             $this->_aCacheOuDn[$id] = array();
 421         }
 422         if($aTmpOu[$this->_adGuidField]){ //通过GUID找到当前节点DN 不通过规则拼接
 423             $sTopOu = self::$_oLdap->getOption('dnSuffix');
 424             $sAdGuid = $this->_decodeAdGuid($aTmpOu[$this->_adGuidField]);
 425             $aTmpEntryList = self::$_oLdap->getEntryList($sTopOu,array('objectClass'=>'organizationalUnit','objectGUID'=>$sAdGuid),array('objectGUID'),0,0);
 426             if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){
 427                 $sTmpOldDn = str_replace(",{$sTopOu}",'',$aTmpEntryList[0]['dn']);
 428                 $this->_aCacheOuDn[$id][$adguid] = $sTmpOldDn;
 429             } else {
 430                 $this->_aCacheOuDn[$id][$adguid] = '';
 431             }
 432             return $this->_aCacheOuDn[$id][$adguid];
 433         }
 434         while($aTmpOu){
 435             $ou = $this->_getOuPinYin($aTmpOu['name'],$aTmpOu['id']);
 436             $aRet[] = "OU={$ou}";
 437             $aTmpOu = isset( $aOuList[$aTmpOu['fid']] ) ? $aOuList[$aTmpOu['fid']] : [];
 438         }
 439         $aRet[] = self::$_aCfg['ldapDnDept']; //组织架构DN专有后缀
 440         $this->_aCacheOuDn[$id][$adguid] = implode(',',$aRet);
 441         return $this->_aCacheOuDn[$id][$adguid];
 442     }
 443     
 444     /**
 445      * @desc 返回单个节点信息
 446      *
 447      */
 448     private function _getOneEntry($dn,$aFilter){
 449         $aTmp = array();
 450         $aEntryList = self::$_oLdap->getEntryList($dn,$aFilter);
 451         if ($aEntryList && ($aEntryList['count'] > 0)){ 
 452             $aTmp = $aEntryList[0];
 453         }
 454         return $aTmp;
 455     }
 456     
 457     /**
 458      * @desc 组织架构节点信息
 459      *
 460      * @return array
 461      */
 462     private $_aGroupNodeAttr = array( //组织节点属性
 463         'description' => '中文名', //组织中文名
 464         'name' => '拼音',//组织英文名称
 465         'managedBy' => '负责人', //部门负责人
 466     );
 467     private function _getGroupData($desc,$adname,$iBoss){
 468         $aRet = array(
 469             'description' => array($desc), //组织中文名
 470             'name' => array($adname),//组织英文名称
 471         );
 472         $sManagedBy = $this->_getUserObjectId($iBoss,true); //部门负责人域对象ID
 473         if($sManagedBy){
 474             $aRet['managedBy'] = array($sManagedBy); //部门负责人
 475         }
 476         return $aRet;
 477     }
 478     
 479     /**
 480      * @desc 用户节点对象ID
 481      *
 482      * @return String
 483      */
 484     private $_aCacheUserObjDn = array();
 485     private function _getUserObjectId($uid,$bChecked = false){
 486         if(isset($this->_aCacheUserObjDn[$uid])){
 487             return $this->_aCacheUserObjDn[$uid];
 488         }
 489         //if(LOCAL){
 490             $aUser = DB::select( "SELECT id,groupid AS pid,cname AS name FROM {$this->_tbluser} WHERE id = '{$uid}'" );
 491         //} else {
 492             //$aUser = o('hr')->getUser($uid);
 493         //}
 494         if (!$aUser){
 495             $this->_aCacheUserObjDn[$uid] = '';
 496             return '';
 497         }
 498         $aUser = (array)$aUser[0];
 499         $sOuDn = $this->_getOuDn($aUser['pid']);
 500         if(!$sOuDn){
 501             $this->_aCacheUserObjDn[$uid] = '';
 502             return '';
 503         }
 504         $dn = "CN={$aUser['name']},{$sOuDn}";
 505         if ($bChecked){
 506             $exists = $this->_getOneEntry($dn,array('objectClass'=>'user'));
 507             if (!$exists){
 508                 $dn = '';
 509             }
 510         }
 511         if ($dn){
 512             $dn = self::$_oLdap->getFullDn($dn);
 513         }
 514         $this->_aCacheUserObjDn[$uid] = $dn;
 515         return $dn;
 516     }
 517     
 518     /**
 519      * @desc 组织架构信息
 520      *
 521      * @return array
 522      */
 523     private function _getOuList(array $aNewNode = array()){
 524         static $aCache = NULL;
 525         if ($aCache === NULL){                                         
 526             $aCache = array();
 527             $aList = DB::select("SELECT * FROM {$this->_tblgroup} WHERE fid > 0");
 528             foreach((array)$aList as $k => $v){
 529                 $v = (array)$v;
 530                 $aCache[$v['id']] = $v;
 531             }
 532         }
 533         if($aNewNode && ($aNewNode['fid'] > 0)){
 534             $aCache[$aNewNode['id']] = $aNewNode;
 535             return true;
 536         }
 537         return $aCache;
 538     }
 539     
 540     private function _genRet($errno = 0,$errMsg = ''){
 541         return array('sts' => $errno, 'msg' => $errMsg);
 542     }
 543     
 544     private $_cacheLog = '';
 545     private $_cacheLogFile = 'aLdapOper_#1.log';
 546     private $_ldapLogFile = 'aLdapOperErr_#1.log';
 547     private $_ldapLogRawFile = 'aLdapOperRaw_#1.log';
 548     private function _log($actType = '',$oType = 'normal',array $aPar = array(), array $aNewData = array(), array $aOldData = array()){
 549         if(in_array($actType,array('endToFile','ldap','def','ldapOperLog'))){
 550             $sDir = rtrim(self::$_aCfg['logDir'],'\\/') . '/';
 551             if(!file_exists($sDir)){
 552                 mkdir($sDir,0666,true);
 553             }
 554         }
 555         if ($actType === 'endToFile'){
 556             $sFile = $sDir.str_replace('#1',date('Y'),$this->_cacheLogFile);
 557             file_put_contents($sFile,$this->_cacheLog,FILE_APPEND);
 558             $this->_cacheLog = '';
 559             //$this->_sendMonitorMail();
 560             return true;
 561         } else if ($actType === 'ldap'){
 562             $sFile = $sDir.str_replace('#1',date('Y'),$this->_ldapLogFile);
 563             $sTmp = date('Y-m-d H:i:s').'----Start----'.$this->aOpertor['username'].'('.$this->aOpertor['id'].')';
 564             $sLdapLog = self::$_oLdap->getErrLog();
 565             file_put_contents($sFile,"{$sTmp}\n{$sLdapLog}\n",FILE_APPEND);
 566             return true;
 567         } else if ($actType === 'def'){
 568             file_put_contents($sDir.$aPar['f'],$aPar['s'],FILE_APPEND);
 569             return true;
 570         } else if ($actType === 'ldapOperLog'){
 571             $sFile = $sDir.str_replace('#1',date('Y'),$this->_ldapLogRawFile);
 572             $sTmp = date('Y-m-d H:i:s').'----Start----'.$this->aOpertor['username'].'('.$this->aOpertor['id'].')';
 573             $sLdapLog = self::$_oLdap->getOperLog();
 574             file_put_contents($sFile,"{$sTmp}\n{$sLdapLog}\n",FILE_APPEND);
 575             return true;
 576         }
 577         $this->_logMail($actType,$oType,$aPar,$aNewData,$aOldData);
 578         $aLog = array(
 579             date('Y-m-d H:i:s'),
 580             $actType,
 581             $oType,
 582             fn::serialize($aPar),
 583         );
 584         $this->_cacheLog .= implode('##',$aLog);
 585         $this->_cacheLog .= "\n";
 586         return true;
 587     }
 588     private $_aLogMail = array('group'=>array(),'user'=>array(),'delUserDn'=>array());//离职用户单独通知 删除权限组
 589     private function _logMail($actType,$oType,$aPar,$aNewData,$aOldData){
 590         if(!isset($this->_aLogMail[$oType])){
 591             return false;
 592         }
 593         if(!$aPar['flag']){ //没有更新成功过滤掉
 594             return false;
 595         }
 596         $aFieldMap = array('group'=>$this->_aGroupNodeAttr,'user'=>$this->_aUserNodeAttr);
 597         if($actType === 'move'){
 598             if(($oType === 'user') && isset($aNewData['del']) && ($aNewData['del'] == self::STATUS_USER_LEAVE)){//离职用户单独通知 
 599                 $sTmpDepart = $this->_fixDepartment($aNewData['pid']);
 600                 $this->_aLogMail['delUserDn'][] = "DN:{$aPar['dn']},RTX账号:{$aNewData['rtx']},部门:{$sTmpDepart}";
 601             }
 602             $this->_aLogMail[$oType][$actType][] = "<tr><td>{$aPar['path']}</td></tr>";
 603         }else if (in_array($actType,array('update','add'))){
 604             $sTmp = '<tr>';
 605             $aFieldTmp = $aFieldMap[$oType];
 606             $sTmp .= '<td>'.(isset($aPar['dn']) ? $aPar['dn'] : $aPar['cn']).'</td>';
 607             foreach($aFieldTmp as $k => $v){
 608                 $sK = strtolower($k);
 609                 if(isset($aOldData[$sK]['count'])){
 610                     unset($aOldData[$sK]['count']);
 611                 }
 612                 $sTmpVal = isset($aPar['attr'][$k]) ? ( (isset($aOldData[$sK]) ? implode(',',$aOldData[$sK]).' => ' : '').implode(',',$aPar['attr'][$k]) ) : '';
 613                 $sTmp .= "<td>{$sTmpVal}</td>";
 614             }
 615             $sTmp .= '</tr>';
 616             $this->_aLogMail[$oType][$actType][] = $sTmp;
 617         }
 618         return true;
 619     }
 620     private function _sendMonitorMail(/*$bClear = true*/){
 621         $sTmp = '';
 622         $aFieldMap = array('group'=>$this->_aGroupNodeAttr,'user'=>$this->_aUserNodeAttr);
 623         $aHeadMap = array('group' => '','user'=>'');
 624         foreach($aFieldMap as $k => $v){
 625             $aHeadMap[$k] = '<tr>';
 626             $aHeadMap[$k] .= '<td>DN</td>';
 627             foreach($v as $k2 => $v2){
 628                 $aHeadMap[$k] .= "<td>{$v2}</td>";
 629             }
 630             $aHeadMap[$k] .= '</tr>';
 631         }
 632         foreach($this->_aLogMail as $k => $v){
 633             if(!$v || ($k === 'delUserDn')){
 634                 continue;
 635             }
 636             $sTmp .= "<h1>被操作实体:{$k}</h1>";
 637             foreach($v as $k2 => $v2){
 638                 $sTmp .= "<h5>操作类型:{$k2}</h5>";
 639                 $sTmp .= '<table border=1 cellspacing=1>';
 640                 if($k2 === 'move'){
 641                     $sTmp .= '<tr><td>Old DN => New DN</td></tr>';
 642                 } else {
 643                     $sTmp .= $aHeadMap[$k];
 644                 }
 645                 $sTmp .= implode("\n",$v2);
 646                 $sTmp .= '</table>';
 647             }
 648             $this->_aLogMail[$k] = array();
 649         }
 650         if(!$sTmp){
 651             return true;
 652         }
 653         $title = '【周知】BY后台同步信息到AD域服务器变更通知';
 654         Mail::queue('emails.ldap_notify',['data'=>$sTmp],function($m)use($title){
 655             $m->to( self::$_aCfg['ldapMailTo'] )->subject($title);
 656         });
 657         $this->_isTestSvr && $this->_log('def','def',array('f'=>'aMonitorLog.html','s'=>$sTmp));
 658         if($this->_aLogMail['delUserDn']){ //离职用户单独通知
 659             $title = '【员工离职周知】BY后台员工变更为离职状态';
 660             $content = '离职用户:<br />'.implode('<br />',$this->_aLogMail['delUserDn']);
 661             Mail::queue('emails.ldap_notify',['data'=>$content],function($m) use($title){
 662                 $m->to( self::$_aCfg['ldapMailTo'] )->subject($title);
 663             });
 664             $this->_isTestSvr && $this->_log('def','def',array('f'=>'aMonitorLog.html','s'=>$content));
 665             $this->_aLogMail['delUserDn'] = array();
 666         }
 667         return true;
 668     }
 669     
 670     /**
 671      * @desc 刷新用户节点信息
 672      *
 673      * @to do list 0.用户重要信息设置 1.用户说所在父节点不存在时自动递归创建
 674      *
 675      * @return boolean
 676      */
 677     private function _frmUserData(array &$newData,$bUpdate = false/*true只更新AD中已存在的员工节点*/,$aUpdateField=array()){
 678         $rtx = trim($newData['username']);
 679         $name = trim($newData['cname']);
 680         $uid = intval($newData['id']);
 681         $pid = intval($newData['groupid']); //部门ID
 682         $oldData = array();
 683         if(!$rtx || !$name || !$uid || !$pid){
 684             return $this->_genRet(-1,'参数不正确username,cname,id,groupid有空值---line:'.__LINE__);
 685         }
 686         if(!$this->_checkOpOk($pid)){ //灰度测试 暂时只对部分部门开放
 687             return $this->_genRet(0);
 688         }
 689         //0 深圳总部 8000外包客服 80000泰国分公司 加入域 其他过滤掉
 690         $uType = fn::getUidType($uid);
 691         if(!in_array($uType,array(0,8000,80000))){
 692             return $this->_genRet(0); 
 693         }
 694         //-1.只对AD服务器执行添加 更新 移动操作 不执行删除操作
 695         //0.同一节点下兄弟节点姓名需要唯一
 696         //1.组织架构下RTX需要唯一
 697         //2.节点存在 直接执行更新操作
 698         //    2.0节点存在 工号不相同时 同一父节点下姓名冲突了 提示错误
 699         //3.如果姓名修改了 如果老姓名存在 需要删除节点
 700         //    3.0删除之前需要判断用户ID是否一致啊 一致才能删除啊 
 701         //4.更改了组织怎么处理?
 702         //    4.0即使更改了组织 删除老节点就行了嘛
 703         //    4.1是否可以移动节点呢
 704         //5.RTX名必须唯一 否则会导致用户节点添加不成功
 705         $sFixUid = $this->_fixUid($uid);
 706         $sTopOu = self::$_oLdap->getOption('dnSuffix');
 707         $aTmpEntryList = self::$_oLdap->getEntryList($sTopOu,array('objectClass'=>'user','sAMAccountName'=>$rtx),array('description'),0,0);
 708         if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){
 709             for($i = 0; $i < $aTmpEntryList['count']; $i++){
 710                 $aTmpEntry = $aTmpEntryList[$i];
 711                 if($aTmpEntry['description'][0] !== $sFixUid){
 712                     return $this->_genRet(-2,"RTX:{$rtx}域服务器已存在---line:".__LINE__);
 713                 }
 714             }
 715         }
 716         $dn = $this->_getOuDn($pid);
 717         $aTmpEntryList = self::$_oLdap->getEntryList($dn,array('objectClass'=>'user','name'=>$name),array('description'));
 718         if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){
 719             for($i = 0; $i < $aTmpEntryList['count']; $i++){
 720                 $aTmpEntry = $aTmpEntryList[$i];
 721                 if($aTmpEntry['description'][0] !== $sFixUid){
 722                     return $this->_genRet(-3,"中文名:{$name}同一小组下域服务器已存在---line:".__LINE__);
 723                 }
 724             }
 725         }
 726         $aAttr = $this->_getUserData($newData);
 727         //移动节点
 728         //0.老节点存在 移动老节点到新节点
 729         //    0.0名称修改了 或者部门修改了 才需要移动 否则只需要更新即可
 730         //    0.1先更新老节点信息 再移动到指定的节点
 731         //    0.3AD服务器老节点用户ID跟数据库中用户ID相同时,才能移动老节点,否则移动了其它人的节点,则悲剧了
 732         //1.老节点不存在 
 733         //    1.0 新节点不存在 添加
 734         //    1.1 新节点存在 更新
 735         //2.无论节点属性怎么修改 工号是不会变化的
 736         //    2.0根据工号找到AD服务器已存在的节点
 737         //    2.1根据工号找出了多个用户节点 需要找运维开发人功能核对处理
 738         $sNewCn = "CN={$name},".$dn; //未包含DN根路径
 739         $nameDel = "{$name}{$sFixUid}";
 740         if($newData['status'] == self::STATUS_USER_LEAVE){ //员工离职需要移动到HasLeft节点下
 741             $dn = self::$_aCfg['ldapDnHasLeftUser'];
 742             $sNewCn = "CN={$nameDel},".$dn;
 743         }
 744         $aTmpEntryList = self::$_oLdap->getEntryList($sTopOu,array('objectClass'=>'user','description'=>$sFixUid),array(),0,0);
 745         if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){ //移动或者更新节点
 746             if($aTmpEntryList['count'] > 1){
 747                 return $this->_genRet(-4,"AD服务器ERR:UID工号({$sFixUid})不唯一,请找运维开发人工处理".__LINE__);
 748             }
 749             $sOldCn = $aTmpEntryList[0]['dn']; //完整的DN路径
 750             $sOldCn = str_replace(",$sTopOu",'',$sOldCn); //为包含DN根路径
 751             unset($aAttr['name']);
 752             $sUacField = 'userAccountControl';
 753             $ssUacField = strtolower($sUacField);
 754             if($aTmpEntryList[0][$ssUacField][0] & 0x10000){//如果密码不过期策略存在,一直保留(运维同事要求)
 755                 $aAttr[$sUacField][0] = $aAttr[$sUacField][0] | 0x10000;
 756             }
 757             $sAccountField = 'sAMAccountName';
 758             $ssAccountField = strtolower($sAccountField);
 759             if( strtolower($aTmpEntryList[0][$ssAccountField][0]) === strtolower($aAttr[$sAccountField][0]) ){
 760                 unset($aAttr[$sAccountField],$aAttr['userPrincipalName'],$aAttr['mail']); //当RTX账号字母相同时【不区分大小写】,不更新RTX账号【运维同事要求】
 761             } else { //不同时,不能更新AD账号,会影响深信服账号策略同步,发邮件周知
 762                 $this->_notifyModAdAccount(array('new'=>$aAttr,'old'=>$aTmpEntryList[0],'acc1'=>$aAttr[$sAccountField][0],'acc2'=>$aTmpEntryList[0][$ssAccountField][0]));
 763                 unset($aAttr[$sAccountField],$aAttr['userPrincipalName'],$aAttr['mail']); 
 764             }
 765             $aAttr = $this->_getAttrDiff($aAttr,$aTmpEntryList[0]);
 766             if($bUpdate && $aUpdateField){ //脚本批量更新某些字段
 767                 $aTmpAttr = array();
 768                 foreach($aUpdateField as $k => $v){
 769                     if(isset($aAttr[$v])){
 770                         $aTmpAttr[$v] = $aAttr[$v];
 771                     }
 772                 }
 773                 $aAttr = $aTmpAttr;
 774             }
 775             //域管理员信息无权限修改 发邮件周知
 776             if(isset($aTmpEntryList[0]['admincount']) && ($aTmpEntryList[0]['admincount']['count'] > 0)){ 
 777                 $this->_notifyModAdAdmin(array('attr'=>$aAttr,'dn'=>array($sNewCn,$sOldCn,$sTopOu)));
 778                 return $this->_genRet(0);
 779             }
 780             $bTmp = true;
 781             if($aAttr){ //更新老节点属性
 782                 $bTmp = self::$_oLdap->updateEntry($sOldCn,$aAttr);
 783                 $this->_log('update','user',array('flag'=>$bTmp,'cn'=>$sOldCn,'attr'=>$aAttr),$newData,$aTmpEntryList[0]);
 784             }
 785             if(!$bTmp){
 786                 return $this->_genRet(-5,'工号:'.$uid.'---更新节点属性失败---line:'.__LINE__);
 787             }
 788             if($bUpdate){
 789                 return $this->_genRet(0);
 790             }
 791             if(strpos($sOldCn,$sNewCn) === false){ //节点DN路径修改了 需要移动节点
 792                 $sNewPartDn = "CN={$name}";
 793                 if($newData['status'] == 2){//员工离职需要移动到HasLeft节点下 加上工号 
 794                     $sNewPartDn = "CN={$nameDel}";
 795                 }
 796                 $bTmp = self::$_oLdap->moveEntry($sOldCn,$sNewPartDn,$dn);
 797                 $this->_log('move','user',array('flag'=>$bTmp,'path'=>"{$sOldCn}->{$sNewPartDn},{$dn}",'dn'=>"{$sNewPartDn},{$dn}"),$newData,$oldData);
 798             }
 799             return $this->_genRet($bTmp ? 0 : -6,'工号:'.$uid.'---移动用户节点失败---line:'.__LINE__);
 800         }
 801         if($bUpdate){
 802             return $this->_genRet(0);
 803         }
 804         //走到这一步 只剩下向域服务器添加新节点了
 805         $aAttr = array_merge($this->_getUserDataForAdd(),$aAttr);
 806         $bTmp = self::$_oLdap->addEntry($sNewCn,$aAttr);
 807         $this->_log('add','user',array('flag'=>$bTmp,'cn'=>$sNewCn,'attr'=>$aAttr),$newData,$oldData);
 808         return $this->_genRet($bTmp ? 0 : -7,'工号:'.$uid.'---更新用户节点失败---line:'.__LINE__);
 809     }
 810     
 811     /**
 812      * @param $aPar = array(
 813      *        'dn' => array(new,old,top),
 814      *        'attr' => array(),
 815      * )
 816      */
 817     private function _notifyModAdAdmin(array $aPar = array()){
 818         $sTmpMail = '';
 819         if($aPar['attr']){
 820             $sTmpHead = '<tr><td>DN</td>';
 821             $sTmp .= "<tr><td>{$aPar['dn'][1]},{$aPar['dn'][2]}</td>";
 822             foreach($aPar['attr'] as $k => $v){
 823                 $sTmpHead .= "<td>{$k}</td>";
 824                 $sTmp .= '<td>'.implode(',',$v).'</td>';
 825             }
 826             $sTmpHead .= '</tr>';
 827             $sTmp .= '</tr>';
 828             $sTmpMail .= '<h2>更新属性信息</h2>';
 829             $sTmpMail .= '<table border=1 cellspacing=1>';
 830             $sTmpMail .= $sTmpHead;
 831             $sTmpMail .= $sTmp;
 832             $sTmpMail .= '</table>';
 833         }
 834         if($aPar['dn'] && ($aPar['dn'][0] !== $aPar['dn'][1])){
 835             $sTmpMail .= '<h2>移动节点</h2>';
 836             $sTmpMail .= "<p>从{$aPar['dn'][1]},{$aPar['dn'][2]}移动到{$aPar['dn'][0]},{$aPar['dn'][2]}</p>";
 837         }
 838         $title = 'BY后台尝试修改域管理员信息,请关注';
 839         if($sTmpMail){
 840             $this->_isTestSvr && $this->_log('def','def',array('f'=>__FUNCTION__.'.html','s'=>$sTmpMail));
 841             Mail::queue('emails.ldap_notify',['data'=>$sTmpMail],function($m)use($title){
 842                 $m->to( self::$_aCfg['ldapMailTo'] )->subject($title);
 843             });
 844         }
 845     }
 846     
 847     /**
 848      * @param $aPar = array(
 849      *        'dn' => array(new,old,top),
 850      *        'attr' => array(),
 851      * )
 852      */
 853     private function _notifyModAdAccount(array $aPar = array()){
 854         $sTmpMail = '';
 855         if($aPar['acc1'] && $aPar['acc2']){
 856             $sTmpMail = '<h2>更新账号</h2>';
 857             $sTmpMail .= "<p>老账号:{$aPar['acc2']} => 新账号:{$aPar['acc1']}</p>";
 858         }
 859         $title = 'BY后台尝试修改员工RTX账号,请关注';
 860         if($sTmpMail){
 861             $this->_isTestSvr && $this->_log('def','def',array('f'=>__FUNCTION__.'.html','s'=>$sTmpMail));
 862             Mail::queue('emails.ldap_notify',['data'=>$sTmpMail],function($m)use($title){
 863                 $m->to( self::$_aCfg['ldapMailTo'] )->subject($title);
 864             });
 865         }
 866     }
 867     
 868     private function _getAttrDiff(array $aNewData = array(), array $aOldData = array()){
 869         foreach($aNewData as $k => $v){
 870             $k = strtolower($k);
 871             if(!isset($aOldData[$k])){
 872                 continue;
 873             }
 874             if($v[0] === $aOldData[$k][0]){
 875                 unset($aNewData[$k]);
 876             }
 877         }
 878         return $aNewData;
 879     }
 880     
 881     /**
 882      * @desc 组装用户节点数据
 883      *
 884      * @return array
 885      */
 886     private $_aUserNodeAttr = array(
 887         'givenName' => '名', //
 888         'sn' => '姓', //
 889         'sAMAccountName' => 'RTX', //RTX
 890         'userPrincipalName' => '登陆名', //登陆名
 891         'description' => '工号', //工号
 892         'pager' => '性别', //性别
 893         'title' => '职位头衔', //职位头衔
 894         'displayName' => '显示名', //显示名
 895         'name' => '姓名', //姓名
 896         //'employeeNumber' => array(00000000), //一卡通号
 897         'mail' => '邮箱', //邮箱
 898         //'mailNickname' => array($rtx), //邮箱昵称 启用了邮箱才有这个属性
 899         'department' => '部门', //部门
 900         'manager' => '上级', //上级
 901         'userAccountControl' => '用户账号策略', //用户账号策略 0x0200正常账号 0x0002账号禁用 
 902     );
 903     private function _getUserData(array $aData = array()){
 904         //id,username,cname,sex,groupid,position,mposition,utype,boss,phone,email,status,degree
 905         $rtx = trim($aData['username']);
 906         $name = trim($aData['cname']);
 907         $aNameSplit = $this->_getFirstLastName($name);
 908         $uid = intval($aData['id']);
 909         $sex = trim($aData['sex']);
 910         $pid = intval($aData['groupid']); //部门ID
 911         $jobId = intval($aData['position']);
 912         $jobId2 = intval($aData['mposition']);
 913         $boss = intval($aData['boss']);
 914         $del = intval($aData['status']);
 915         
 916         $sSuffixDn = self::$_oLdap->getOption('dnSuffix');
 917         $sTmpDc = substr($sSuffixDn,strpos($sSuffixDn,'DC=')+3);
 918         $sTmpDc = str_replace(',DC=','.',$sTmpDc);
 919         
 920         $aAttr = array(
 921             'givenName' => array($aNameSplit[1]), //
 922             'sn' => array($aNameSplit[0]), //
 923             'sAMAccountName' => array($rtx), //RTX
 924             'userPrincipalName' => array("{$rtx}@{$sTmpDc}"), //登陆名
 925             'description' => array($this->_fixUid($uid)), //工号
 926             'pager' => array($this->_fixSex($sex)), //性别
 927             'title' => array($this->_fixTitle($jobId,$jobId2)), //职位头衔
 928             'displayName' => array($name), //显示名
 929             'name' => array($name), //姓名
 930             //'employeeNumber' => array(00000000), //一卡通号
 931             'mail' => array($rtx.'@boyaa.com'), //邮箱
 932             //'mailNickname' => array($rtx), //邮箱昵称 启用了邮箱才有这个属性
 933             'department' => array($this->_fixDepartment($pid)/*o('hr')->getGroup($pid, 'name')*/), //部门
 934             'manager' => array($this->_getUserObjectId($boss,true)), //上级
 935             'userAccountControl' => array($this->_fixUAC($del)), //用户账号策略 0x0200正常账号 0x0002账号禁用 0x10000密码永不过期
 936         );
 937         foreach($aAttr as $k => $v){
 938             if(($v[0] === 0) || ($v[0] === '0')){
 939                 continue;
 940             }
 941             if(!$v[0]){
 942                 unset($aAttr[$k]);
 943             }
 944         }
 945         return $aAttr;
 946     }
 947     
 948     private function _getUserDataForAdd(){
 949         return array(
 950             //初始密码
 951             'unicodePwd' => mb_convert_encoding('"'.self::$_aCfg['ldapInitPasswd'].'"', 'utf-16le'), 
 952             //设置新用户第一次登陆更改密码
 953             'pwdLastSet' => array(0),
 954         );
 955     }
 956     
 957     private function _fixUid($uid){
 958         if ($uid < 10000){
 959             $uid = str_pad($uid,5,'0',STR_PAD_LEFT);
 960         }
 961         return (string)$uid;
 962     }
 963     
 964     private function _fixSex($sex){
 965         $sex = strtolower($sex);
 966         $aMap = array(
 967             1 => '0', // male
 968             2 => '1', //female
 969         );
 970         return isset($aMap[$sex]) ? $aMap[$sex] : '-1';
 971     }
 972     
 973     private function _fixTitle($j0 = 0,$j1 = 0){
 974         $o = new User();
 975         $o->position = $j0;
 976         $o->mposition = $j1;
 977         $s1 = $o->defPosition;
 978         $s2 = '';
 979         if($j1){
 980             $s2 = $o->defMpositionName;
 981         }
 982         $title = '';
 983         if($s1){
 984             $title .= $s1;
 985         }
 986         if($s2){
 987             $title .= "/{$s2}";
 988         }
 989         return $title;
 990     }
 991     
 992     private function _fixUAC($status){
 993         //用户账号策略 0x0200正常账号 0x0002账号禁用
 994         //$status 1在职 2离职 0待入职
 995         $ret = '';
 996         $status = intval($status);
 997         if($status === self::STATUS_USER_NOLEAVE){
 998             $ret = 0x0200;
 999         } else {
1000             $ret = 0x0200 | 0x0002;
1001         }
1002         return (string)$ret;
1003     }
1004     
1005     private $_aDepartmentCache = array();
1006     private function _fixDepartment($id){
1007         if($id == 1){ //一级部门OU = Dept;
1008             return '';
1009         }
1010         if(isset($this->_aDepartmentCache[$id])){
1011             return $this->_aDepartmentCache[$id];
1012         }
1013         $aRet = array();
1014         $aOuList = (array)$this->_getOuList();
1015         $aTmpOu = $aOuList[$id];
1016         if (!$aTmpOu){
1017             return '';
1018         }
1019         while($aTmpOu){
1020             $aRet[] = $aTmpOu['name'];
1021             $aTmpOu = isset( $aOuList[$aTmpOu['fid']] ) ? $aOuList[$aTmpOu['fid']] : [];
1022         }
1023         $aRet = (array)array_reverse($aRet);
1024         $this->_aDepartmentCache[$id] = implode('\\',$aRet);
1025         return $this->_aDepartmentCache[$id];
1026     }
1027     
1028     /**
1029      * @desc 中国现存复姓 来自:http://wenku.baidu.com/view/0d7070bc1a37f111f1855b24.html
1030      *
1031      */
1032     private $_aCnDblFamilyName = array(
1033         '欧阳','太史','端木','上官','司马','东方','独孤',
1034         '南宫','万俟','闻人','夏侯','诸葛','尉迟','公羊',
1035         '赫连','澹台','皇甫','宗政','濮阳','公冶','太叔',
1036         '申屠','公孙','慕容','仲孙','钟离','长孙','宇文',
1037         '司徒','鲜于','司空','闾丘','子车','亓官','司寇',
1038         '巫马','公西','颛孙','壤驷','公良','漆雕','乐正',
1039         '宰父','谷梁','拓跋','夹谷','轩辕','令狐','段干',
1040         '百里','呼延','东郭','南门','羊舌','微生','公户',
1041         '公玉','公仪','梁丘','公仲','公上','公门','公山',
1042         '公坚','左丘','公伯','西门','公祖','第五','公乘',
1043         '贯丘','公皙','南荣','东里','东宫','仲长','子书',
1044         '子桑','即墨','达奚','褚师',
1045     );
1046     
1047     /**
1048      * @desc 分离姓和名
1049      *
1050      * @return array
1051      */
1052     private function _getFirstLastName($name = ''){
1053         $name = trim($name);
1054         if(!$name){
1055             return array('',''); //[姓,名]
1056         }
1057         if(strpos($name,' ')){
1058             $name = preg_replace('/\s+/',' ',$name);
1059             $aName = explode(' ',$name);
1060             return array($aName[1],$aName[0]);
1061         }
1062         $family = mb_substr($name,0,1,'utf-8');
1063         $given = mb_substr($name,1,mb_strlen($name,'utf-8')-1,'utf-8');
1064         if(mb_strlen($name) > 2){
1065             $tmp = mb_substr($name,0,2);
1066             if (in_array($tmp,$this->_aCnDblFamilyName)){
1067                 $family = $tmp;
1068                 $given = mb_substr($name,2);
1069             }
1070         }
1071         return array($family,$given);
1072     }
1073     
1074     public function __destruct(){
1075     }
1076     
1077 }
View Code

 查看客户端

ApacheDirectoryStudio-win32-x86_64-2.0.0.v20130628

 

参考资料

[1] LDAPV3协议

http://tools.ietf.org/html/rfc4511

[2] LDAP百度百科

http://baike.baidu.com/view/159263.htm

[3] WIKI LDAP

http://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol

[4] LDAP Data Interchange Format

http://en.wikipedia.org/wiki/LDAP_Data_Interchange_Format

[5] OpenLDAP介绍

http://en.wikipedia.org/wiki/OpenLDAP

[6] OpenLDAP管理员文档

[7] Zend LDAP API

http://framework.zend.com/manual/1.11/en/zend.ldap.api.html

[8] BerkeleyDb以及OPENLDAP安装指南

http://www.openldap.org/lists/openldap-technical/201001/msg00046.html

[9] LDAP环境搭建 OpenLDAP和phpLDAPadmin -- yum版

http://www.cnblogs.com/yafei236/p/4141897.html

[10] phpldapadmin开源项目

http://phpldapadmin.sourceforge.net/wiki/index.php/Main_Page

posted @ 2015-05-28 12:38  huan&ping  阅读(66751)  评论(6编辑  收藏  举报