前言
公司管理員工信息以及組織架構的后台系統要和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 }
輔助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 }
組織架構信息更新類
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 }
查看客戶端
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
