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 }
辅助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
出处:http://www.cnblogs.com/wadeyu/
本文版权归本人和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。