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

LDAP目录服务折腾之后的总结,第1张

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库实现 完全面向对象

 <?php
/**
* @description LDAP客户端类
*
* @author WadeYu
* @date 2015-04-28
* @version 0.0.1
*/
class o_ldap{
private $_conn = NULL;
private $_sErrLog = '';
private $_sOperLog = '';
private $_aOptions = array(
'host' => 'ldap://xxx.com',
'port' => '389',
'dnSuffix' => 'OU=xx,OU=xx,DC=xx,DC=com',
'loginUser' => '',
'loginPass' => '',
);
private $_aAllowAttrName = array(
'objectClass',
'objectGUID', //AD对象ID
'userPassword', //AD密码不是这个字段 密码暂时不能通过程序设置
'unicodePwd', //AD密码专用字段 $unicodePwd = mb_convert_encoding('"' . $newPassword . '"', 'utf-16le');
'cn', //comman name 兄弟节点不能相同
'ou', //organizationalUnit
'description', //员工填工号
'displayName', //中文名
'name', //姓名
'sAMAccountName', //英文名(RTX账号,唯一)
'userPrincipalName', //登陆用户名 和 英文名一致
'ProtectedFromAccidentalDeletion', //对象删除保护
'givenName', //姓
'sn', //名
'employeeNumber', //一卡通卡号
'mail',
'mailNickname',
'manager', //上级 (节点路径 示例:CN=Texas Poker9,OU=Texas Poker,OU=Dept,OU=BoyaaSZ,DC=by,DC=com)
'title', //头衔
'pager', //性别 0男 1女 -1未知
'userAccountControl', //用户账号策略(暂时不能设置) 资料说明地址:https://support.microsoft.com/en-gb/kb/305144
'department',
'managedBy',//部门负责人
'distinguishedName',
'pwdLastSet', //等于0时 下次登录时需要修改密码
); public function __construct(array $aOptions = array()){
if (!extension_loaded('ldap')){
$this->_log('LDAP extension not be installed.',true);
}
$this->setOption($aOptions);
} /**
* @return exit || true
*/
public function connect($force = false){
if (!$this->_conn || $force){
$host = $this->_aOptions['host'];
$port = $this->_aOptions['port'];
$this->_conn = ldap_connect($host,$port);
if ($this->_conn === false){
$this->_log("Connect LDAP SERVER Failure.[host:{$post}:{$port}]",true);
}
ldap_set_option($this->_conn, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($this->_conn, LDAP_OPT_REFERRALS, 3);
$this->_bind();
}
return true;
} /**
* @return exit || true
*/
private function _bind(){
$u = $this->_aOptions['loginUser'];
$p = $this->_aOptions['loginPass'];
$ret = @ldap_bind($this->_conn,$u,$p);
if ($ret === false){
$this->_log(__FUNCTION__.'----'.$this->_getLastExecErrLog().'----'."u:{$u},p:{$p}",true);
}
return $ret;
} public function setOption(array $aOptions = array()){
foreach($this->_aOptions as $k => $v){
if (isset($aOptions[$k])){
$this->_aOptions[$k] = $aOptions[$k];
}
}
} public function getOption($field,$default = ''){
return isset($this->_aOptions[$field]) ? $this->_aOptions[$field] : $default;
} /**
* @description 查询$dn下符合属性条件的节点 返回$limit条
*
* @return array [count:x,[[prop:[count:xx,[],[]]],....]]
*/
public function getEntryList($dn,$aAttrFilter,array $aField=array(),$limit = 0,$bFixedDn = true){
if (!$dn = trim($dn)){
return array();
}
if (!$this->_checkDn($dn)){
return array();
}
$limit = max(0,intval($limit));
$this->connect();
if ($bFixedDn){
$dn = $this->_getFullDn($dn);
}
$aOldTmp = $aAttrFilter;
$this->_checkAttr($aAttrFilter);
if (!$aAttrFilter){
$this->_log(__FUNCTION__.'---无效的搜索属性---'.json_encode($aOldTmp));
return array();
}
$sAttrFilter = $this->_mkAttrFilter($aAttrFilter);
$attrOnly = 0;
$this->_log(__FUNCTION__."---DN:{$dn}---sAttr:{$sAttrFilter}",false,'oper');
$rs = @ldap_search($this->_conn,$dn,$sAttrFilter,$aField,$attrOnly,$limit);
if ($rs === false){
$this->_log(__FUNCTION__."---dn:{$dn}---sAttr:{$sAttrFilter}---" . $this->_getLastExecErrLog());
return array();
}
$aRet = @ldap_get_entries($this->_conn,$rs);
ldap_free_result($rs);
if ($aRet === false){
$this->_log(__FUNCTION__.'---'.$this->_getLastExecErrLog());
return array();
}
return $aRet;
} /**
* @description 删除节点 暂时不考虑递归删除
*
* @return boolean
*/
public function delEntry($dn,$bFixedDn = true,$force = 0){
if (!$dn = trim($dn)){
return false;
}
if (!$this->_checkDn($dn)){
return false;
}
if ($bFixedDn){
$dn = $this->_getFullDn($dn);
}
$this->_log(__FUNCTION__."---DN:{$dn}",false,'oper');
$this->connect();
/*if($force){
$aEntryList = $this->getEntryList($dn,array('objectClass'=>'*'),array('objectClass'));
if ($aEntryList && ($aEntryList['count'] > 0)){
for($i = 0; $i < $aEntryList['count']; $i++){
$aDel[] = $aEntryList[$i]['dn'];
}
}
$aDel = array_reverse($aDel); //默认顺序 祖先->子孙 需要先删除子孙节点
$ret = true;
foreach($aDel as $k => $v){
$ret &= @ldap_delete($this->_conn,$v);
}
if ($ret === false){
$this->_log(__FUNCTION__.'dn(recursive):'.$dn.'----'.$this->_getLastExecErrLog());
}
return $ret;
}*/
$ret = @ldap_delete($this->_conn,$dn);
if ($ret === false){
$this->_log(__FUNCTION__.'----dn:'.$dn.'-----'.$this->_getLastExecErrLog());
}
return $ret;
} /**
* @description 更新节点
*
* @return boolean
*/
public function updateEntry($dn,$aAttr = array(),$bFixedDn = true){
if (!$dn = trim($dn)){
return false;
}
$this->_checkAttr($aAttr);
if (!$aAttr){
return false;
}
if (!$this->_checkDn($dn)){
return false;
}
if ($bFixedDn){
$dn = $this->_getFullDn($dn);
}
$this->_log(__FUNCTION__."---DN:{$dn}---aAttr:".str_replace("\n",'',var_export($aAttr,true)),false,'oper');
$this->connect();
$ret = @ldap_modify($this->_conn,$dn,$aAttr);
if ($ret === false){
$this->_log(__FUNCTION__.'---'.$this->_getLastExecErrLog().'---dn:'.$dn.'---attr:'.json_encode($aAttr));
}
return $ret;
} /**
* @description 添加节点
*
* @return boolean
*/
public function addEntry($dn,$aAttr = array(), $type = 'employee'/*employee,group*/){
if (!$dn = trim($dn)){
return false;
}
$this->_checkAttr($aAttr);
if (!$aAttr){
return false;
}
if (!$this->_checkDn($dn)){
return false;
}
$aAttr['objectClass'] = (array)$this->_getObjectClass($type);
$this->_log(__FUNCTION__."---DN:{$dn}---aAttr:".str_replace("\n",'',var_export($aAttr,true)),false,'oper');
$this->connect();
$dn = $this->_getFullDn($dn);
$ret = @ldap_add($this->_conn,$dn,$aAttr);
if ($ret === false){
$this->_log(__FUNCTION__.'----dn:'.$dn.'----aAttr:'.json_encode($aAttr).'-----'.$this->_getLastExecErrLog());
}
return $ret;
} /**
* @description 移动叶子节点 v3版才支持此方法
*
* @param $newDn 相对于$parentDn
* @param $parentDn 完整DN
* @param $bMoveRecur
*
* @return boolean
*/
public function moveEntry($oldDn,$newDn,$parentDn,$bDelOld = true,$bFixDn = true,$bMoveRecur = true){
//对于AD服务器 此方法可以移动用户节点以及组织节点
//$newDn只能包含一个 比如OU=xxx
$oldDn = trim($oldDn);
$newDn = trim($newDn);
$parentDn = trim($parentDn);
if(!$oldDn || !$newDn || ($bFixDn && !$parentDn)){
return false;
}
if(!$this->_checkDn($oldDn) || !$this->_checkDn($newDn) || !$this->_checkDn($parentDn)){
return false;
}
$this->connect();
if($bFixDn){
$oldDn = $this->_getFullDn($oldDn);
$parentDn = $this->_getFullDn($parentDn);
}
$this->_log(__FUNCTION__."---DN:{$oldDn} -> {$newDn},{$parentDn}",false,'oper');
$aTmpMove = $aDelDn = array();
$aTmpMove[] = array('old'=>$oldDn,'new'=>$newDn);
/*if($bMoveRecur){
$aDelDn[] = $oldDn;
$aTmpList = $this->getEntryList($oldDn,array('objectClass'=>'*'),array('objectClass'),0,0);
if($aTmpList && ($aTmpList['count'] > 1)){
for($i = 1; $i < $aTmpList['count']; $i++){
if(!in_array('user',$aTmpList[$i]['objectclass'])){ //$bDelOld=true时,用户节点移动时会自动删除
$aDelDn[] = $aTmpList[$i]['dn'];
}
$aTmpSep = explode($oldDn,$aTmpList[$i]['dn']);
$aTmpMove[] = array(
'old' => $aTmpList[$i]['dn'],
'new' => $aTmpSep[0] . $newDn,
);
}
}
}*/
$bFlag = true;
foreach($aTmpMove as $k => $v){
$bTmpFlag = ldap_rename($this->_conn,$v['old'],$v['new'],$parentDn,(boolean)$bDelOld);
if(!$bTmpFlag){
$this->_log(__FUNCTION__."---o:{$v['old']}-n:{$v['new']}-p:{$parentDn}-recur:{$bMoveRecur}-----".$this->_getLastExecErrLog());
}
$bFlag &= $bTmpFlag;
}
/*if(!$bFlag){
$this->_log(__FUNCTION__."---o:{$oldDn}-n:{$newDn}-p:{$parentDn}-recur:{$bMoveRecur}-----".$this->_getLastExecErrLog());
}*/
/*if($bFlag && $bDelOld && $aDelDn){
$aDelDn = array_reverse($aDelDn);
foreach($aDelDn as $k => $v){
$this->delEntry($v,false);
}
}*/
return $bFlag;
} public function modEntry($dn,$act = 'add',$aAttr = array()){
return false;
$dn = $this->_getFullDn($dn);
$this->_log(__FUNCTION__."---DN:{$dn}---Act:{$act}---aAttr:".str_replace("\n",'',var_export($aAttr,true)),false,'oper');
$this->connect();
$ret = false;
switch($act){
case 'add': $ret = ldap_mod_add($this->_conn,$dn,$aAttr); break;
case 'replace': $ret = ldap_mod_replace($this->_conn,$dn,$aAttr); break;
case 'del': $ret = ldap_mod_del($this->_conn,$dn,$aAttr); break;
}
if(!$ret){
$this->_log(__FUNCTION__."---dn:{$dn}---act:{$act}---attr:".json_encode($aAttr).'---'.$this->_getLastExecErrLog());
}
return $ret;
} /**
* @description 批量添加节点
*
* @return boolean
*/
public function addBatchEntry($aNodeList = array()){
} public function getAttrKv(array $aAttr = array()){
if(!isset($aAttr['count']) || ($aAttr['count'] < 1)){
return array();
}
$aRet = array();
for($i = 0; $i < $aAttr['count']; $i++){
$field = $aAttr[$i];
if (!isset($aAttr[$field])){
return array();
}
unset($aAttr[$field]['count']);
$aRet[$field] = $aAttr[$field];
}
if(isset($aAttr['dn'])){ //dn是字符串
$aRet['dn'] = $aAttr['dn'];
}
return $aRet;
} private function _getObjectClass($type = 'employee'){
$aRet = array();
switch($type){
case 'employee' : $aRet = array('top','person','organizationalPerson','user'); break;
case 'group' : $aRet = array('top','organizationalUnit'); break;
}
return $aRet;
} public function getFullDn($partDn = ''){
return $this->_getFullDn($partDn);
} private function _getFullDn($partDn = ''){
$partDn = trim($partDn);
$partDn = rtrim($partDn,',');
return "{$partDn},{$this->_aOptions['dnSuffix']}";
} private function _checkDn($dn = ''){
$dn = trim($dn,',');
$aDn = explode(',',$dn);
foreach($aDn as $k => $v){
$aTmp = explode('=',$v);
$aTmp[0] = strtolower(trim($aTmp[0]));
$aTmp[1] = trim($aTmp[1]);
$flag = false;
switch($aTmp[0]){ //distingushed name 暂时只允许这3个field
case 'dc': $flag = $this->_checkDc($aTmp[1]); break;
case 'ou': $flag = $this->_checkOu($aTmp[1]); break;
case 'cn': $flag = $this->_checkCn($aTmp[1]); break;
}
if (!$flag){
$this->_log(__FUNCTION__.'----无效的节点路径----dn:'.$dn);
return false;
}
}
return true;
} private function _checkOu($ou = ''){
if (!$ou){
return false;
}
if (preg_match('/[^a-zA-Z\s\d\.&\'\d]/',$ou)){
$this->_log(__FUNCTION__.'----OU只能包含字母数字空格以及点');
return false;
}
return true;
} private function _checkCn($cn = ''){
if (!$cn){
return false;
}
return true;
} private function _checkDc($dc = ''){
if (!$dc){
return false;
}
if (preg_match('/[^a-zA-Z]/',$dc)){
$this->_log(__FUNCTION__.'----DC只能包含英文字母');
return false;
}
return true;
} private function _mkAttrFilter(array $aAttrFilter = array()){
$sStr = '(&';
foreach($aAttrFilter as $k => $v){
$v = (string)$v;
if($k === 'objectGUID'){
$v = $this->_GUIDtoStr($v);
}
$v = addcslashes($v,'()=');
$sStr .= "({$k}={$v})";
}
$sStr .= ')';
return $sStr;
} //来自PHP.NET http://php.net/manual/en/function.ldap-search.php
//http://php.net/manual/en/function.ldap-get-values-len.php
//GUID关键字
private function _GUIDtoStr($binary_guid){
$hex_guid = unpack("H*hex", $binary_guid);
$hex = $hex_guid["hex"];
$j = 0;$str = '\\';
for($i = 0; $i < strlen($hex); $i++){
if($j == 2){
$str .= '\\';
$j = 0;
}
$str .= $hex[$i];
$j++;
}
return $str;
/*$hex1 = substr($hex, -26, 2) . substr($hex, -28, 2) . substr($hex, -30, 2) . substr($hex, -32, 2);
$hex2 = substr($hex, -22, 2) . substr($hex, -24, 2);
$hex3 = substr($hex, -18, 2) . substr($hex, -20, 2);
$hex4 = substr($hex, -16, 4);
$hex5 = substr($hex, -12, 12);
$guid_str = $hex1 . "-" . $hex2 . "-" . $hex3 . "-" . $hex4 . "-" . $hex5;
return $guid_str;*/
} private function _checkAttr(& $aAttr = array()){
foreach((array)$aAttr as $k => $v){
if (!in_array($k,$this->_aAllowAttrName)){
unset($aAttr[$k]);
}
}
return true;
} public function getErrLog(){
return $this->_sErrLog;
} public function getOperLog(){
return $this->_sOperLog;
} private function _log($str = '',$bExit = false,$type = 'err'/*err,oper*/){
if ($bExit){
die($str);
}
$date = date('Y-m-d H:i:s');
if($type === 'err'){
$this->_sErrLog .= "{$date}----{$str}\n";
} else if ($type === 'oper'){
$this->_sOperLog .= "{$date}----{$str}\n";
}
} public function close(){
ldap_close($this->_conn);
} private function _getLastExecErrLog(){
$no = ldap_errno($this->_conn);
$err = ldap_error($this->_conn);
return "---exec Error:{$no}---{$err}";
}
}

辅助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. *** 作系统

 [root@vm ldap]# lsb_release -a
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
Distributor ID: CentOS
Description: CentOS release 5.4 (Final)
Release: 5.4
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 []:[email protected]
[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命令添加根节点 未启动前

 [root@vm ~]# cd ~
[root@vm ~]# vim root.ldif
dn: dc=test,dc=com
dc: test
objectClass: dcObject
objectClass: organizationalUnit
ou: test.com
[root@vm ~]# slapadd -v -n -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基础类

 <?php
/**
* @description LDAP客户端类
*
* @author WadeYu
* @date 2015-04-28
* @version 0.0.1
*/
namespace App\Lib;
class Ldap{
private $_conn = NULL;
private $_sErrLog = '';
private $_sOperLog = '';
private $_aOptions = array(
'host' => 'ldap://xxx.com',
'port' => '389',
'dnSuffix' => 'OU=xx,OU=xx,DC=xx,DC=com',
'loginUser' => '',
'loginPass' => '',
'logDir' => '',
);
private $_aAllowAttrName = array(
'objectClass',
'objectGUID', //AD对象ID
'userPassword', //AD密码不是这个字段 密码暂时不能通过程序设置
'unicodePwd', //AD密码专用字段 $unicodePwd = mb_convert_encoding('"' . $newPassword . '"', 'utf-16le');
'cn', //comman name 兄弟节点不能相同
'ou', //organizationalUnit
'description', //员工填工号
'displayName', //中文名
'name', //姓名
'sAMAccountName', //英文名(RTX账号,唯一)
'userPrincipalName', //登陆用户名 和 英文名一致
'ProtectedFromAccidentalDeletion', //对象删除保护
'givenName', //姓
'sn', //名
'employeeNumber', //一卡通卡号
'mail',
'mailNickname',
'manager', //上级 (节点路径 示例:CN=Texas Poker9,OU=Texas Poker,OU=Dept,OU=BoyaaSZ,DC=by,DC=com)
'title', //头衔
'pager', //性别 0男 1女 -1未知
'userAccountControl', //用户账号策略(暂时不能设置) 资料说明地址:https://support.microsoft.com/en-gb/kb/305144
'department',
'managedBy',//部门负责人
'distinguishedName',
'pwdLastSet', //等于0时 下次登录时需要修改密码
'memberOf', //用户所属组
'member',//组成员
); public function __construct(array $aOptions = array()){
if (!extension_loaded('ldap')){
$this->_log('LDAP extension not be installed.',true);
}
$this->setOption($aOptions);
} /**
* @return exit || true
*/
public function connect($force = false){
if (!$this->_conn || $force){
$host = $this->_aOptions['host'];
$port = $this->_aOptions['port'];
$this->_conn = ldap_connect($host,$port);
if ($this->_conn === false){
$this->_log("Connect LDAP SERVER Failure.[host:{$post}:{$port}]",true);
}
ldap_set_option($this->_conn, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($this->_conn, LDAP_OPT_REFERRALS, 3);
$this->_bind();
}
return $this->_conn;
} /**
* @return exit || true
*/
private function _bind(){
$u = $this->_aOptions['loginUser'];
$p = $this->_aOptions['loginPass'];
$ret = @ldap_bind($this->_conn,$u,$p);
if ($ret === false){
$this->_log(__FUNCTION__.'----'.$this->_getLastExecErrLog().'----'."u:{$u},p:{$p}",true);
}
return $ret;
} public function setOption(array $aOptions = array()){
foreach($this->_aOptions as $k => $v){
if (isset($aOptions[$k])){
$this->_aOptions[$k] = $aOptions[$k];
}
}
} public function getOption($field,$default = ''){
return isset($this->_aOptions[$field]) ? $this->_aOptions[$field] : $default;
} /**
* @description 查询$dn下符合属性条件的节点 返回$limit条
*
* @return array [count:x,[[prop:[count:xx,[],[]]],....]]
*/
public function getEntryList($dn,$aAttrFilter,array $aField=array(),$limit = 0,$bFixedDn = true){
if (!$dn = trim($dn)){
return array();
}
if (!$this->_checkDn($dn)){
return array();
}
$limit = max(0,intval($limit));
$this->connect();
if ($bFixedDn){
$dn = $this->_getFullDn($dn);
}
$aOldTmp = $aAttrFilter;
$this->_checkAttr($aAttrFilter);
if (!$aAttrFilter){
$this->_log(__FUNCTION__.'---无效的搜索属性---'.json_encode($aOldTmp));
return array();
}
$sAttrFilter = $this->_mkAttrFilter($aAttrFilter);
$attrOnly = 0;
$this->_log(__FUNCTION__."---DN:{$dn}---sAttr:{$sAttrFilter}",false,'oper');
$aRet = [];
for($try = 1; $try <= 3; $try++){
$rs = @ldap_search($this->_conn,$dn,$sAttrFilter,$aField,$attrOnly,$limit);
if ($rs === false){
$this->_log(__FUNCTION__."---dn:{$dn}--try:{$try}---sAttr:{$sAttrFilter}---" . $this->_getLastExecErrLog());
if($this->_getLastErrNo() == -1){ //未连接上LDAP服务器至多重试3次
$this->connect(true);
continue;
} else { //其它情况直接退出
break;
}
}
$aRet = @ldap_get_entries($this->_conn,$rs);
ldap_free_result($rs);
if ($aRet === false){
$this->_log(__FUNCTION__.'---try:{$try}---'.$this->_getLastExecErrLog());
if($this->_getLastErrNo() == -1){
$this->connect(true);
continue;
} else {
break;
}
} else {
break;
}
}
return $aRet;
} /**
* @description 删除节点 暂时不考虑递归删除
*
* @return boolean
*/
public function delEntry($dn,$bFixedDn = true,$force = 0){
return false;
if (!$dn = trim($dn)){
return false;
}
if (!$this->_checkDn($dn)){
return false;
}
if ($bFixedDn){
$dn = $this->_getFullDn($dn);
}
$this->_log(__FUNCTION__."---DN:{$dn}",false,'oper');
$this->connect();
/*if($force){
$aEntryList = $this->getEntryList($dn,array('objectClass'=>'*'),array('objectClass'));
if ($aEntryList && ($aEntryList['count'] > 0)){
for($i = 0; $i < $aEntryList['count']; $i++){
$aDel[] = $aEntryList[$i]['dn'];
}
}
$aDel = array_reverse($aDel); //默认顺序 祖先->子孙 需要先删除子孙节点
$ret = true;
foreach($aDel as $k => $v){
$ret &= @ldap_delete($this->_conn,$v);
}
if ($ret === false){
$this->_log(__FUNCTION__.'dn(recursive):'.$dn.'----'.$this->_getLastExecErrLog());
}
return $ret;
}*/
$ret = @ldap_delete($this->_conn,$dn);
if ($ret === false){
$this->_log(__FUNCTION__.'----dn:'.$dn.'-----'.$this->_getLastExecErrLog());
}
return $ret;
} /**
* @description 更新节点
*
* @return boolean
*/
public function updateEntry($dn,$aAttr = array(),$bFixedDn = true){
if (!$dn = trim($dn)){
return false;
}
$this->_checkAttr($aAttr);
if (!$aAttr){
return false;
}
if (!$this->_checkDn($dn)){
return false;
}
if ($bFixedDn){
$dn = $this->_getFullDn($dn);
}
$this->_log(__FUNCTION__."---DN:{$dn}---aAttr:".str_replace("\n",'',var_export($aAttr,true)),false,'oper');
$this->connect();
$ret = false;
for($try = 1; $try <= 3; $try++){
$ret = @ldap_modify($this->_conn,$dn,$aAttr);
if ($ret === false){
$this->_log(__FUNCTION__.'---try:{$try}---'.$this->_getLastExecErrLog().'---dn:'.$dn.'---attr:'.json_encode($aAttr));
if($this->_getLastErrNo() == -1){ //未连上服务器至多重试3次
$this->connect(true);
continue;
}else{
break;
}
} else {
break;
}
}
return $ret;
} /**
* @description 添加节点
*
* @return boolean
*/
public function addEntry($dn,$aAttr = array(), $type = 'employee'/*employee,group*/){
if (!$dn = trim($dn)){
return false;
}
$this->_checkAttr($aAttr);
if (!$aAttr){
return false;
}
if (!$this->_checkDn($dn)){
return false;
}
$aAttr['objectClass'] = (array)$this->_getObjectClass($type);
$this->_log(__FUNCTION__."---DN:{$dn}---aAttr:".str_replace("\n",'',var_export($aAttr,true)),false,'oper');
$this->connect();
$dn = $this->_getFullDn($dn);
$ret = false;
for($try = 1; $try <= 3; $try++){
$ret = @ldap_add($this->_conn,$dn,$aAttr);
if ($ret === false){
$this->_log(__FUNCTION__.'----dn:'.$dn.'----aAttr:'.json_encode($aAttr).'-----'.$this->_getLastExecErrLog());
if($this->_getLastErrNo() == -1){ //未连上服务器至多重试3次
$this->connect(true);
continue;
} else {
break;
}
} else {
break;
}
}
return $ret;
} /**
* @description 移动叶子节点 v3版才支持此方法
*
* @param $newDn 相对于$parentDn
* @param $parentDn 完整DN
* @param $bMoveRecur
*
* @return boolean
*/
public function moveEntry($oldDn,$newDn,$parentDn,$bDelOld = true,$bFixDn = true,$bMoveRecur = true){
//对于AD服务器 此方法可以移动用户节点以及组织节点
//$newDn只能包含一个 比如OU=xxx
$oldDn = trim($oldDn);
$newDn = trim($newDn);
$parentDn = trim($parentDn);
if(!$oldDn || !$newDn || ($bFixDn && !$parentDn)){
return false;
}
if(!$this->_checkDn($oldDn) || !$this->_checkDn($newDn) || !$this->_checkDn($parentDn)){
return false;
}
$this->connect();
if($bFixDn){
$oldDn = $this->_getFullDn($oldDn);
$parentDn = $this->_getFullDn($parentDn);
}
$this->_log(__FUNCTION__."---DN:{$oldDn} -> {$newDn},{$parentDn}",false,'oper');
$aTmpMove = $aDelDn = array();
$aTmpMove[] = array('old'=>$oldDn,'new'=>$newDn);
/*if($bMoveRecur){
$aDelDn[] = $oldDn;
$aTmpList = $this->getEntryList($oldDn,array('objectClass'=>'*'),array('objectClass'),0,0);
if($aTmpList && ($aTmpList['count'] > 1)){
for($i = 1; $i < $aTmpList['count']; $i++){
if(!in_array('user',$aTmpList[$i]['objectclass'])){ //$bDelOld=true时,用户节点移动时会自动删除
$aDelDn[] = $aTmpList[$i]['dn'];
}
$aTmpSep = explode($oldDn,$aTmpList[$i]['dn']);
$aTmpMove[] = array(
'old' => $aTmpList[$i]['dn'],
'new' => $aTmpSep[0] . $newDn,
);
}
}
}*/
$bFlag = true;
foreach($aTmpMove as $k => $v){
$bTmpFlag = ldap_rename($this->_conn,$v['old'],$v['new'],$parentDn,(boolean)$bDelOld);
if(!$bTmpFlag){
$this->_log(__FUNCTION__."---o:{$v['old']}-n:{$v['new']}-p:{$parentDn}-recur:{$bMoveRecur}-----".$this->_getLastExecErrLog());
}
$bFlag &= $bTmpFlag;
}
/*if(!$bFlag){
$this->_log(__FUNCTION__."---o:{$oldDn}-n:{$newDn}-p:{$parentDn}-recur:{$bMoveRecur}-----".$this->_getLastExecErrLog());
}*/
/*if($bFlag && $bDelOld && $aDelDn){
$aDelDn = array_reverse($aDelDn);
foreach($aDelDn as $k => $v){
$this->delEntry($v,false);
}
}*/
return $bFlag;
} public function modEntry($dn,$act = 'add',$aAttr = array()){
//return false;
$dn = $this->_getFullDn($dn);
$this->_log(__FUNCTION__."---DN:{$dn}---Act:{$act}---aAttr:".str_replace("\n",'',var_export($aAttr,true)),false,'oper');
$this->connect();
$ret = false;
switch($act){
case 'add': $ret = ldap_mod_add($this->_conn,$dn,$aAttr); break;
case 'replace': $ret = ldap_mod_replace($this->_conn,$dn,$aAttr); break;
case 'del': $ret = ldap_mod_del($this->_conn,$dn,$aAttr); break;
}
if(!$ret){
$this->_log(__FUNCTION__."---dn:{$dn}---act:{$act}---attr:".json_encode($aAttr).'---'.$this->_getLastExecErrLog());
}
return $ret;
} /**
* @description 批量添加节点
*
* @return boolean
*/
public function addBatchEntry($aNodeList = array()){
} public function getAttrKv(array $aAttr = array()){
if(!isset($aAttr['count']) || ($aAttr['count'] < 1)){
return array();
}
$aRet = array();
for($i = 0; $i < $aAttr['count']; $i++){
$field = $aAttr[$i];
if (!isset($aAttr[$field])){
return array();
}
unset($aAttr[$field]['count']);
$aRet[$field] = $aAttr[$field];
}
if(isset($aAttr['dn'])){ //dn是字符串
$aRet['dn'] = $aAttr['dn'];
}
return $aRet;
} private function _getObjectClass($type = 'employee'){
$aRet = array();
switch($type){
case 'employee' : $aRet = array('top','person','organizationalPerson','user'); break;
case 'group' : $aRet = array('top','organizationalUnit'); break;
}
return $aRet;
} public function getFullDn($partDn = ''){
return $this->_getFullDn($partDn);
} private function _getFullDn($partDn = ''){
$partDn = trim($partDn);
$partDn = rtrim($partDn,',');
return "{$partDn},{$this->_aOptions['dnSuffix']}";
} private function _checkDn($dn = ''){
$dn = trim($dn,',');
$aDn = explode(',',$dn);
foreach($aDn as $k => $v){
$aTmp = explode('=',$v);
$aTmp[0] = strtolower(trim($aTmp[0]));
$aTmp[1] = trim($aTmp[1]);
$flag = false;
switch($aTmp[0]){ //distingushed name 暂时只允许这3个field
case 'dc': $flag = $this->_checkDc($aTmp[1]); break;
case 'ou': $flag = $this->_checkOu($aTmp[1]); break;
case 'cn': $flag = $this->_checkCn($aTmp[1]); break;
}
if (!$flag){
$this->_log(__FUNCTION__.'----无效的节点路径----dn:'.$dn);
return false;
}
}
return true;
} private function _checkOu($ou = ''){
if (!$ou){
return false;
}
if (preg_match('/[^a-zA-Z\s\d\.&\'\d]/',$ou)){
$this->_log(__FUNCTION__.'----OU只能包含字母数字空格以及点');
return false;
}
return true;
} private function _checkCn($cn = ''){
if (!$cn){
return false;
}
return true;
} private function _checkDc($dc = ''){
if (!$dc){
return false;
}
if (preg_match('/[^a-zA-Z]/',$dc)){
$this->_log(__FUNCTION__.'----DC只能包含英文字母');
return false;
}
return true;
} private function _mkAttrFilter(array $aAttrFilter = array()){
$sStr = '(&';
foreach($aAttrFilter as $k => $v){
$v = (string)$v;
if($k === 'objectGUID'){
$v = $this->_GUIDtoStr($v);
}
$v = addcslashes($v,'()=');
$sStr .= "({$k}={$v})";
}
$sStr .= ')';
return $sStr;
} //来自PHP.NET http://php.net/manual/en/function.ldap-search.php
//http://php.net/manual/en/function.ldap-get-values-len.php
//GUID关键字
private function _GUIDtoStr($binary_guid){
$hex_guid = unpack("H*hex", $binary_guid);
$hex = $hex_guid["hex"];
$j = 0;$str = '\\';
for($i = 0; $i < strlen($hex); $i++){
if($j == 2){
$str .= '\\';
$j = 0;
}
$str .= $hex[$i];
$j++;
}
return $str;
/*$hex1 = substr($hex, -26, 2) . substr($hex, -28, 2) . substr($hex, -30, 2) . substr($hex, -32, 2);
$hex2 = substr($hex, -22, 2) . substr($hex, -24, 2);
$hex3 = substr($hex, -18, 2) . substr($hex, -20, 2);
$hex4 = substr($hex, -16, 4);
$hex5 = substr($hex, -12, 12);
$guid_str = $hex1 . "-" . $hex2 . "-" . $hex3 . "-" . $hex4 . "-" . $hex5;
return $guid_str;*/
} private function _checkAttr(& $aAttr = array()){
foreach((array)$aAttr as $k => $v){
if (!in_array($k,$this->_aAllowAttrName)){
unset($aAttr[$k]);
}
}
return true;
} public function getErrLog(){
return $this->_sErrLog;
} public function getOperLog(){
return $this->_sOperLog;
} public function clearLog($type = 'err'){
if($type == 'err'){
$this->_sErrLog = '';
} else {
$this->_sOperLog = '';
}
} private function _log($str = '',$bExit = false,$type = 'err'/*err,oper*/){
$date = date('Y-m-d H:i:s');
if ($bExit){
if($this->_aOptions['logDir']){
if(!file_exists($this->_aOptions['logDir'])){
mkdir($this->_aOptions['logDir'],0666,true);
}
$file = rtrim($this->_aOptions['logDir'],'\\/') . '/ldap_exit_' . date('Ym') . '.log';
file_put_contents($file,"{$date}---type:{$type}---{$str}\n",FILE_APPEND);
}
die($str);
}
if($type === 'err'){
$this->_sErrLog .= "{$date}----{$str}\n";
} else if ($type === 'oper'){
$this->_sOperLog .= "{$date}----{$str}\n";
}
} public function close(){
ldap_close($this->_conn);
} private function _getLastExecErrLog(){
$no = $this->_getLastErrNo();
$err = ldap_error($this->_conn);
return "---exec Error:{$no}---{$err}";
} public function getLastExecErrLog(){
return $this->_getLastExecErrLog();
} private function _getLastErrNo(){
return ldap_errno($this->_conn);
}
}

组织架构信息更新类

 <?php
/**
* @description 测试用例说明
*
* @组织节点
* 0.添加是否正常
* 1.修改节点是否正常
* 1.0修改了中文名称是否正常
* 1.1修改了其它属性是否正常
* 1.2修改了上级组织是否正常
* 1.3修改了需要移动节点的属性也修改了其它属性是否正常
* 2.移动节点是否正常
* 2.0参考编号1
* 3.删除节点是否正常
* 3.0暂时未提供删除入口
* 4.没有修改时是否有更新 *** 作
* 4.0原则上没有修改不应该更新AD服务器
*
* @用户节点
* 0.添加是否正常
* 0.0敏感属性设置是否正常 例如用户密码 用户账号策略控制
* 0.1RTX账号不能重复
* 0.2没有填的项不能设置 否则会覆盖掉老数据
* 0.3同一父节点下中文名称不能相同
* 1.修改或者移动是否正常
* 1.0RTX账号必须唯一
* 1.1姓名同一父节点下必须唯一
* 1.2修改了姓名或者修改了所在部门需要移动节点
* 1.3修改了其它属性 执行更新 *** 作
* 1.4没有属性修改 原则上不更新AD服务器
* 1.5设置敏感属性信息是否正常
* 2.删除是否正常
* 2.0暂时未提供入口
*
* @description 碰到的问题说明
*
* 0.敏感属性只能通过加密连接设置
* 1.解决中文乱码 移动节点 只有LDAPV3协议支持
* 2.运维建议尽量只进行更新 *** 作 域服务器OBJECTGUID数量有限制 超过了域服务器就不能使用了
*/
namespace App\Lib;
use App\Lib\Ldap;
use App\Lib\Pinyin;
use App\Lib\fn;
use DB;
use App\Http\Model\Group;
use App\Http\Model\User;
use Mail;
class Hrldap{
private static $_aCfg = array();
private static $_oLdap = null;
private $_adGuidField = 'adguid';
private $_tbluser = 'pb_user';
private $_tblgroup = 'pb_group';
private $_isTestSvr = false;
public $aOpertor = array();
const STATUS_USER_LEAVE = 2; //员工离职状态
const STATUS_USER_NOLEAVE = 1; //员工在职状态
const STATUS_GROUP_DEL = 1; //组织架构删除状态 public function __construct(){
if (!self::$_aCfg){
self::$_aCfg = config('ldap');
}
if (self::$_oLdap === null){
self::$_oLdap = new Ldap($this->_getReMapCfg());
}
$this->_isTestSvr = (env('APP_ENV') == 'local');
$this->aOpertor = ['username'=>'cron','id'=>0]; //设置一个默认值
} private function _getReMapCfg(){
return array(
'host' => self::$_aCfg['ldapHost'],
'port' => self::$_aCfg['ldapPort'],
'dnSuffix' => self::$_aCfg['ldapDnSuffix'],
'loginUser' => self::$_aCfg['ldapUser'],
'loginPass' => self::$_aCfg['ldapPass'],
'logDir' => self::$_aCfg['logDir'],
);
} public function getCfg($field = ''){
if($field && isset(self::$_aCfg[$field])){
return self::$_aCfg[$field];
}
return self::$_aCfg;
} /**
* @desc 通用入口
*
*/
public function apiHr(array &$newData, $type = 'group'/*group,user*/,$aOpertor = [],$bLog = true){
//$newData 数据库最新的数据
$this->aOpertor = $aOpertor;
try{
switch($type){
case 'group':
$aRet = $this->_frmGroupData($newData);
break;
case 'user':
$aRet = $this->_frmUserData($newData);
break;
}
if($bLog){
//$this->_log('endToFile');
$this->_log('ldap');
//$this->_log('ldapOperLog');
$this->_sendMonitorMail();
}
return $aRet;
}catch(Exception $ex){
if($bLog){
//$this->_log('endToFile');
$this->_log('ldap');
$this->_sendMonitorMail();
}
return $this->_genRet(-9999,$ex->getMessage());
}
} public function afterHandleCb(){
//$this->_log('endToFile');
$this->_log('ldap');
//$this->_log('ldapOperLog');
$this->_sendMonitorMail();
} /**
* @desc 批量更新通用入口
*
* @param $aData [type:[id:{},...,]]
*
*/
public function apiHrBat(array &$aData,$aOpertor = []){
if(!$aData || !is_array($aData)){
return false;
}
$this->aOpertor = $aOpertor;
$aType = array('group' => $this->_tblgroup,'user' => $this->_tbluser);
$aColumns = array(
'group'=>'*',
'user'=>'id,username,cname,sex,groupid,position,mposition,utype,boss,phone,email,status,degree',
);
$bTmp = true;
$st = microtime(true);
foreach($aData as $k => $v){
if(!isset($aType[$k])){
continue;
}
$aTmpIds = array_keys($v);
if(!$aTmpIds){
continue;
}
$sTmpIds = implode(',',$aTmpIds);
$aTmpList = DB::select( "SELECT {$aColumns[$k]} FROM {$aType[$k]} WHERE id in ({$sTmpIds})" );
foreach((array)$aTmpList as $k2 => $v2){
$aTmpRet = array();
switch($k){
case 'group': $aTmpRet = $this->_frmGroupData((array)$v2); break;
case 'user': $aTmpRet = $this->_frmUserData((array)$v2); break;
}
$bTmp &= (($aTmpRet['sts'] === 0) ? true : false);
}
}
$et = microtime(true);
//$this->_log('endToFile');
$this->_log('ldap');
$this->_log('def','def',array('f'=>'aApiHrBatCnt.log','s'=>date('Y-m-d H:i:s').'---批量更新消耗时间:'.($et-$st)."---更新状态:{$bTmp}\n"));
$this->_log('def','def',array('f'=>'aApiHrBatData.log','s'=>date('Y-m-d H:i:s').json_encode($aData)."\n"));
//$this->_log('ldapOperLog');
$this->_sendMonitorMail();
return $bTmp;
} /**
* @desc 根据数据库数据批量更新信息到AD服务器(组织节点)
*
* @return boolean
*/
public function apiSyncOuToAd(){
} /**
* @desc 根据数据库数据批量更新信息到AD服务器(员工节点)
*
* @return boolean
*/
public function apiSyncUserToAd($bOnlyUpdate = false,$aUpdateField = array(),$aUid = array()){
} public function getLdapObj(){
return self::$_oLdap;
} /**
* @desc 灰度开放部门
*
*/
private function _checkOpOk($id = 1){//组织ID
return true; //开启全部部门
$aOuList = $this->_getOuList();
if(!isset($aOuList[$id])){
return false;
}
$aIdLvl = array();
$aTmpOu = $aOuList[$id];
while($aTmpOu){
$aIdLvl[] = $aTmpOu['id'];
$aTmpOu = $aOuList[$aTmpOu['fid']];
}
$topId = array_pop($aIdLvl);
if(in_array($topId,array(328,/*业务支持中心*/))){ //暂时开放这些部门
return true;
}
return false;
} private $_aNewestAdguidMap = array();
private function _frmGroupData(array &$newData){
$sNewDes = $newData['name'] = trim($newData['name']);
$iBoss = $newData['boss'] = intval($newData['boss']);
$id = $newData['id'] = intval($newData['id']);
if( !$sNewDes || !$id /*|| !$iBoss*/){
return $this->_genRet(-1);
}
if(!$this->_checkOpOk($id)){ //灰度测试 暂时只对部分部门开放
return $this->_genRet(0);
}
if($newData['del'] == self::STATUS_GROUP_DEL){ //关闭节点 有在职员工 AD不移动到LEFTOUT
if( count(User::getOcolsByGroup($id)) > 0 ){
return $this->_genRet(0);
}
}
//关闭节点 移动节点到OU=OU,OU=HasLeft
/*if($newData['del'] == 1){//关闭节点 AD服务器不做处理
return $this->_genRet(0);
}*/
//AD服务器老节点存在 执行更新移动 *** 作
//否则走添加新节点流程
//新添加节点业务机需要保存节点GUID
if(isset($this->_aNewestAdguidMap[$id])){ //循环更新的话ADGUID可能会变化
$newData[$this->_adGuidField] = $this->_aNewestAdguidMap[$id];
}
$sNewAdName = $this->_getOuPinYin($sNewDes,$id);
$this->_getOuList($newData);//更新OU LIST 节点缓存
$aNewAttr = $this->_getGroupData($sNewDes,$sNewAdName,$iBoss);
$sGuidField = $this->_adGuidField;
$sAdGuid = $newData[$sGuidField] ? $this->_decodeAdGuid($newData[$sGuidField]) : ''; //AD服务器节点唯一标识
$aOldEntry = array();
$sTopOu = self::$_oLdap->getOption('dnSuffix');
if($sAdGuid){ //没找到是否需要退出 ?
$aTmpEntryList = self::$_oLdap->getEntryList($sTopOu,array('objectClass'=>'organizationalUnit','objectGUID'=>$sAdGuid),array(),0,0);
if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){
$aOldEntry = $aTmpEntryList[0];
}
}
if(($newData['del'] == self::STATUS_GROUP_DEL) && !$aOldEntry){ //关闭节点 域中节点不存在的话 不做任何处理
return $this->_genRet(0);
}
if($aOldEntry){ //AD服务器节点已存在 更新或者移动
$sTmpOldDn = str_replace(",{$sTopOu}",'',$aOldEntry['dn']);
$aTmpAttr = $this->_getAttrDiff($aNewAttr,$aOldEntry);
unset($aTmpAttr['name']); //恶魔属性 不能更新
$bTmp = true;
if($aTmpAttr && ($newData['del'] != self::STATUS_GROUP_DEL)){
$bTmp = self::$_oLdap->updateEntry($sTmpOldDn,$aTmpAttr);
$this->_log('update','group',array('flag'=>$bTmp,'dn'=>$sTmpOldDn,'attr'=>$aTmpAttr),$newData,$aOldEntry);
}
if(!$bTmp){
return $this->_genRet(-2,'部门ID:'.$id.'---节点更新失败---line:'.__LINE__);
}
$sTmp2 = "OU={$sNewAdName}";
if($newData['del'] == self::STATUS_GROUP_DEL){ //关闭节点 把节点移动到OU=OU,OU=HasLeft
$sTmp4 = self::$_aCfg['ldapDnHasLeftOu'];
}else{
$sTmp4 = $this->_getOuDn($newData['fid']);
}
$sTmp5 = $sTmp2 . ($sTmp4 ? ",{$sTmp4}" : '');
if(strpos($sTmpOldDn,$sTmp5) === false){ //DN变了 需要移动节点
$bTmp = self::$_oLdap->moveEntry($sTmpOldDn,$sTmp2,$sTmp4);
$this->_log('move','group',array('flag'=>$bTmp,'path'=>"{$sTmpOldDn}->{$sTmp5}"),$newData,$aOldEntry);
}
return $this->_genRet($bTmp ? 0 : -3,'部门ID:'.$id.'---移动节点失败---line:'.__LINE__);
}
//走到这一步只剩下添加节点 *** 作了
//父级节点不存在自动递归创建
//否则新组织节点也创建不了
$aAdd = array();
$aOuList = $this->_getOuList(); //数据库中的组织架构列表
$aTmpOu = $newData;
do{
if($aTmpOu[$sGuidField]){ //数据库中存在域对象的GUID AD域中存在的概率99% 还需要走AD服务器验证下?
$bTmpBreak = true;
if($this->_isTestSvr){
$aTmpFilter = array('objectClass'=>'organizationalUnit','objectGUID'=>$this->_decodeAdGuid($aTmpOu[$sGuidField]));
$aTmpEntryList = self::$_oLdap->getEntryList($sTopOu,$aTmpFilter,array(),0,0);
if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){
} else {
$bTmpBreak = false;
}
}
if($bTmpBreak){
break;
}
}
$aAdd[] = $aTmpOu;
$aTmpOu = isset($aOuList[$aTmpOu['fid']]) ? $aOuList[$aTmpOu['fid']] : [];
}while($aTmpOu);
if(!$aAdd){
return $this->_genRet(0);
}
$aAdd = array_reverse($aAdd); //祖先节点要先添加
$bTmpFlag = true;
$sPreFixDn = $this->_getOuDn($aAdd[0]['fid']);
if(!$sPreFixDn){
return $this->_genRet(-6,'部门ID:'.$id.'---新建节点失败---取第一个父节点(fid:'.$aAdd[0]['fid'].')DN失败');
}
foreach($aAdd as $k => $v){
$sTmpOuPinYin = $this->_getOuPinYin($v['name'],$v['id']);
$aTmpAttr = $this->_getGroupData($v['name'],$sTmpOuPinYin,$v['boss']);
//$sTmpDn = $this->_getOuDn($v['id']);
$sTmpDn = "OU={$sTmpOuPinYin}," . $sPreFixDn;
$bTmpFlag2 = self::$_oLdap->addEntry($sTmpDn,$aTmpAttr,'group');
$bTmpFlag = $bTmpFlag2;
$this->_log('add','group',array('flag'=>$bTmpFlag2,'dn'=>$sTmpDn,'attr'=>$aTmpAttr),$v,array());
if($bTmpFlag2){ //更新业务机组织表AD节点objectGUID
$this->_addAdGuidMap($sTmpDn,$v['id'],array('name'=>$aTmpAttr['name'][0]),'group',true);
} else { //父节点都更新失败,子节点无地方落脚啊,果断不往下走
break;
}
$sPreFixDn = $sTmpDn;
}
return $this->_genRet($bTmpFlag ? 0 : -5,'部门ID:'.$id.'---新建节点失败');
} private function _encodeAdGuid($str){
return base64_encode($str);
} private function _decodeAdGuid($str){
return base64_decode($str);
} private $_aCachePinYin = array();
private function _getOuPinYin($name,$id){
if(isset($this->_aCachePinYin[$id])){
return $this->_aCachePinYin[$id];
}
$this->_aCachePinYin[$id] = (new Pinyin())->Pinyin($name,1) . " {$id}"; //加上ID后缀 避免名称冲突
return $this->_aCachePinYin[$id];
} private function _addAdGuidMap($dn,$id,$aAttrFilter = array(),$type = 'group'/*group,user*/,$bUpdateGuid = false){
$adGuid = '';
$sErrFile = 'aLdapAddAdGuidMapErr.log';
$aTmpEntryList = self::$_oLdap->getEntryList($dn,$aAttrFilter,array('objectGUID'));
if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){
$adGuid = $aTmpEntryList[0]['objectguid'][0];
}
if(!$adGuid){
$this->_debug(date('Y-m-d H:i:s')."---dn:{$dn}---id:{$id}---type:{$type}---取objectGUID失败\n",$sErrFile);
return false;
}
$aMapTbl = array(
'group' => $this->_tblgroup,
'user' => $this->_tbluser,
);
if(!isset($aMapTbl[$type])){
return false;
}
$tbl = $aMapTbl[$type];
$adGuid = $this->_encodeAdGuid($adGuid);
$ret = DB::update( "UPDATE {$tbl} SET {$this->_adGuidField} = '{$adGuid}' WHERE id = '{$id}'" );
if(!$ret){
$this->_debug(date('Y-m-d H:i:s')."---dn:{$dn}---id:{$id}---type:{$type}---guid:{$adGuid}---objectGUID入库失败\n",$sErrFile);
}
if($bUpdateGuid && $ret){
switch($type){
case 'group':
$aTmpOuList = $this->_getOuList();
$aTmpOu = $aTmpOuList[$id];
$aTmpOu[$this->_adGuidField] = $adGuid;
$this->_getOuList($aTmpOu);
$this->_aNewestAdguidMap[$id] = $adGuid;
break;
}
}
return $ret;
} private function _debug($msg,$sFile){
$this->_log('def','def',array('f'=>$sFile,'s'=>$msg));
} /**
* @desc 拼接域节点路径
*
*/
private $_aCacheOuDn = array();
private function _getOuDn($id){
if($id == 1){ //一级部门OU = Dept;
return self::$_aCfg['ldapDnDept']; //组织架构DN专有后缀;
}
$aRet = array();
$aOuList = (array)$this->_getOuList();
$aTmpOu = $aOuList[$id];
if (!$aTmpOu){
throw new Exception(__FUNCTION__.'----非法的组织ID:'.$id.'---line:'.__LINE__);
}
$adguid = 'x';
if($aTmpOu[$this->_adGuidField]){
$adguid = $aTmpOu[$this->_adGuidField];
}
if(isset($this->_aCacheOuDn[$id][$adguid])){
return $this->_aCacheOuDn[$id][$adguid];
}
if(!isset($this->_aCacheOuDn[$id])){
$this->_aCacheOuDn[$id] = array();
}
if($aTmpOu[$this->_adGuidField]){ //通过GUID找到当前节点DN 不通过规则拼接
$sTopOu = self::$_oLdap->getOption('dnSuffix');
$sAdGuid = $this->_decodeAdGuid($aTmpOu[$this->_adGuidField]);
$aTmpEntryList = self::$_oLdap->getEntryList($sTopOu,array('objectClass'=>'organizationalUnit','objectGUID'=>$sAdGuid),array('objectGUID'),0,0);
if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){
$sTmpOldDn = str_replace(",{$sTopOu}",'',$aTmpEntryList[0]['dn']);
$this->_aCacheOuDn[$id][$adguid] = $sTmpOldDn;
} else {
$this->_aCacheOuDn[$id][$adguid] = '';
}
return $this->_aCacheOuDn[$id][$adguid];
}
while($aTmpOu){
$ou = $this->_getOuPinYin($aTmpOu['name'],$aTmpOu['id']);
$aRet[] = "OU={$ou}";
$aTmpOu = isset( $aOuList[$aTmpOu['fid']] ) ? $aOuList[$aTmpOu['fid']] : [];
}
$aRet[] = self::$_aCfg['ldapDnDept']; //组织架构DN专有后缀
$this->_aCacheOuDn[$id][$adguid] = implode(',',$aRet);
return $this->_aCacheOuDn[$id][$adguid];
} /**
* @desc 返回单个节点信息
*
*/
private function _getOneEntry($dn,$aFilter){
$aTmp = array();
$aEntryList = self::$_oLdap->getEntryList($dn,$aFilter);
if ($aEntryList && ($aEntryList['count'] > 0)){
$aTmp = $aEntryList[0];
}
return $aTmp;
} /**
* @desc 组织架构节点信息
*
* @return array
*/
private $_aGroupNodeAttr = array( //组织节点属性
'description' => '中文名', //组织中文名
'name' => '拼音',//组织英文名称
'managedBy' => '负责人', //部门负责人
);
private function _getGroupData($desc,$adname,$iBoss){
$aRet = array(
'description' => array($desc), //组织中文名
'name' => array($adname),//组织英文名称
);
$sManagedBy = $this->_getUserObjectId($iBoss,true); //部门负责人域对象ID
if($sManagedBy){
$aRet['managedBy'] = array($sManagedBy); //部门负责人
}
return $aRet;
} /**
* @desc 用户节点对象ID
*
* @return String
*/
private $_aCacheUserObjDn = array();
private function _getUserObjectId($uid,$bChecked = false){
if(isset($this->_aCacheUserObjDn[$uid])){
return $this->_aCacheUserObjDn[$uid];
}
//if(LOCAL){
$aUser = DB::select( "SELECT id,groupid AS pid,cname AS name FROM {$this->_tbluser} WHERE id = '{$uid}'" );
//} else {
//$aUser = o('hr')->getUser($uid);
//}
if (!$aUser){
$this->_aCacheUserObjDn[$uid] = '';
return '';
}
$aUser = (array)$aUser[0];
$sOuDn = $this->_getOuDn($aUser['pid']);
if(!$sOuDn){
$this->_aCacheUserObjDn[$uid] = '';
return '';
}
$dn = "CN={$aUser['name']},{$sOuDn}";
if ($bChecked){
$exists = $this->_getOneEntry($dn,array('objectClass'=>'user'));
if (!$exists){
$dn = '';
}
}
if ($dn){
$dn = self::$_oLdap->getFullDn($dn);
}
$this->_aCacheUserObjDn[$uid] = $dn;
return $dn;
} /**
* @desc 组织架构信息
*
* @return array
*/
private function _getOuList(array $aNewNode = array()){
static $aCache = NULL;
if ($aCache === NULL){
$aCache = array();
$aList = DB::select("SELECT * FROM {$this->_tblgroup} WHERE fid > 0");
foreach((array)$aList as $k => $v){
$v = (array)$v;
$aCache[$v['id']] = $v;
}
}
if($aNewNode && ($aNewNode['fid'] > 0)){
$aCache[$aNewNode['id']] = $aNewNode;
return true;
}
return $aCache;
} private function _genRet($errno = 0,$errMsg = ''){
return array('sts' => $errno, 'msg' => $errMsg);
} private $_cacheLog = '';
private $_cacheLogFile = 'aLdapOper_#1.log';
private $_ldapLogFile = 'aLdapOperErr_#1.log';
private $_ldapLogRawFile = 'aLdapOperRaw_#1.log';
private function _log($actType = '',$oType = 'normal',array $aPar = array(), array $aNewData = array(), array $aOldData = array()){
if(in_array($actType,array('endToFile','ldap','def','ldapOperLog'))){
$sDir = rtrim(self::$_aCfg['logDir'],'\\/') . '/';
if(!file_exists($sDir)){
mkdir($sDir,0666,true);
}
}
if ($actType === 'endToFile'){
$sFile = $sDir.str_replace('#1',date('Y'),$this->_cacheLogFile);
file_put_contents($sFile,$this->_cacheLog,FILE_APPEND);
$this->_cacheLog = '';
//$this->_sendMonitorMail();
return true;
} else if ($actType === 'ldap'){
$sFile = $sDir.str_replace('#1',date('Y'),$this->_ldapLogFile);
$sTmp = date('Y-m-d H:i:s').'----Start----'.$this->aOpertor['username'].'('.$this->aOpertor['id'].')';
$sLdapLog = self::$_oLdap->getErrLog();
file_put_contents($sFile,"{$sTmp}\n{$sLdapLog}\n",FILE_APPEND);
return true;
} else if ($actType === 'def'){
file_put_contents($sDir.$aPar['f'],$aPar['s'],FILE_APPEND);
return true;
} else if ($actType === 'ldapOperLog'){
$sFile = $sDir.str_replace('#1',date('Y'),$this->_ldapLogRawFile);
$sTmp = date('Y-m-d H:i:s').'----Start----'.$this->aOpertor['username'].'('.$this->aOpertor['id'].')';
$sLdapLog = self::$_oLdap->getOperLog();
file_put_contents($sFile,"{$sTmp}\n{$sLdapLog}\n",FILE_APPEND);
return true;
}
$this->_logMail($actType,$oType,$aPar,$aNewData,$aOldData);
$aLog = array(
date('Y-m-d H:i:s'),
$actType,
$oType,
fn::serialize($aPar),
);
$this->_cacheLog .= implode('##',$aLog);
$this->_cacheLog .= "\n";
return true;
}
private $_aLogMail = array('group'=>array(),'user'=>array(),'delUserDn'=>array());//离职用户单独通知 删除权限组
private function _logMail($actType,$oType,$aPar,$aNewData,$aOldData){
if(!isset($this->_aLogMail[$oType])){
return false;
}
if(!$aPar['flag']){ //没有更新成功过滤掉
return false;
}
$aFieldMap = array('group'=>$this->_aGroupNodeAttr,'user'=>$this->_aUserNodeAttr);
if($actType === 'move'){
if(($oType === 'user') && isset($aNewData['del']) && ($aNewData['del'] == self::STATUS_USER_LEAVE)){//离职用户单独通知
$sTmpDepart = $this->_fixDepartment($aNewData['pid']);
$this->_aLogMail['delUserDn'][] = "DN:{$aPar['dn']},RTX账号:{$aNewData['rtx']},部门:{$sTmpDepart}";
}
$this->_aLogMail[$oType][$actType][] = "<tr><td>{$aPar['path']}</td></tr>";
}else if (in_array($actType,array('update','add'))){
$sTmp = '<tr>';
$aFieldTmp = $aFieldMap[$oType];
$sTmp .= '<td>'.(isset($aPar['dn']) ? $aPar['dn'] : $aPar['cn']).'</td>';
foreach($aFieldTmp as $k => $v){
$sK = strtolower($k);
if(isset($aOldData[$sK]['count'])){
unset($aOldData[$sK]['count']);
}
$sTmpVal = isset($aPar['attr'][$k]) ? ( (isset($aOldData[$sK]) ? implode(',',$aOldData[$sK]).' => ' : '').implode(',',$aPar['attr'][$k]) ) : '';
$sTmp .= "<td>{$sTmpVal}</td>";
}
$sTmp .= '</tr>';
$this->_aLogMail[$oType][$actType][] = $sTmp;
}
return true;
}
private function _sendMonitorMail(/*$bClear = true*/){
$sTmp = '';
$aFieldMap = array('group'=>$this->_aGroupNodeAttr,'user'=>$this->_aUserNodeAttr);
$aHeadMap = array('group' => '','user'=>'');
foreach($aFieldMap as $k => $v){
$aHeadMap[$k] = '<tr>';
$aHeadMap[$k] .= '<td>DN</td>';
foreach($v as $k2 => $v2){
$aHeadMap[$k] .= "<td>{$v2}</td>";
}
$aHeadMap[$k] .= '</tr>';
}
foreach($this->_aLogMail as $k => $v){
if(!$v || ($k === 'delUserDn')){
continue;
}
$sTmp .= "<h1>被 *** 作实体:{$k}</h1>";
foreach($v as $k2 => $v2){
$sTmp .= "<h5> *** 作类型:{$k2}</h5>";
$sTmp .= '<table border=1 cellspacing=1>';
if($k2 === 'move'){
$sTmp .= '<tr><td>Old DN => New DN</td></tr>';
} else {
$sTmp .= $aHeadMap[$k];
}
$sTmp .= implode("\n",$v2);
$sTmp .= '</table>';
}
$this->_aLogMail[$k] = array();
}
if(!$sTmp){
return true;
}
$title = '【周知】BY后台同步信息到AD域服务器变更通知';
Mail::queue('emails.ldap_notify',['data'=>$sTmp],function($m)use($title){
$m->to( self::$_aCfg['ldapMailTo'] )->subject($title);
});
$this->_isTestSvr && $this->_log('def','def',array('f'=>'aMonitorLog.html','s'=>$sTmp));
if($this->_aLogMail['delUserDn']){ //离职用户单独通知
$title = '【员工离职周知】BY后台员工变更为离职状态';
$content = '离职用户:<br />'.implode('<br />',$this->_aLogMail['delUserDn']);
Mail::queue('emails.ldap_notify',['data'=>$content],function($m) use($title){
$m->to( self::$_aCfg['ldapMailTo'] )->subject($title);
});
$this->_isTestSvr && $this->_log('def','def',array('f'=>'aMonitorLog.html','s'=>$content));
$this->_aLogMail['delUserDn'] = array();
}
return true;
} /**
* @desc 刷新用户节点信息
*
* @to do list 0.用户重要信息设置 1.用户说所在父节点不存在时自动递归创建
*
* @return boolean
*/
private function _frmUserData(array &$newData,$bUpdate = false/*true只更新AD中已存在的员工节点*/,$aUpdateField=array()){
$rtx = trim($newData['username']);
$name = trim($newData['cname']);
$uid = intval($newData['id']);
$pid = intval($newData['groupid']); //部门ID
$oldData = array();
if(!$rtx || !$name || !$uid || !$pid){
return $this->_genRet(-1,'参数不正确username,cname,id,groupid有空值---line:'.__LINE__);
}
if(!$this->_checkOpOk($pid)){ //灰度测试 暂时只对部分部门开放
return $this->_genRet(0);
}
//0 深圳总部 8000外包客服 80000泰国分公司 加入域 其他过滤掉
$uType = fn::getUidType($uid);
if(!in_array($uType,array(0,8000,80000))){
return $this->_genRet(0);
}
//-1.只对AD服务器执行添加 更新 移动 *** 作 不执行删除 *** 作
//0.同一节点下兄弟节点姓名需要唯一
//1.组织架构下RTX需要唯一
//2.节点存在 直接执行更新 *** 作
// 2.0节点存在 工号不相同时 同一父节点下姓名冲突了 提示错误
//3.如果姓名修改了 如果老姓名存在 需要删除节点
// 3.0删除之前需要判断用户ID是否一致啊 一致才能删除啊
//4.更改了组织怎么处理?
// 4.0即使更改了组织 删除老节点就行了嘛
// 4.1是否可以移动节点呢
//5.RTX名必须唯一 否则会导致用户节点添加不成功
$sFixUid = $this->_fixUid($uid);
$sTopOu = self::$_oLdap->getOption('dnSuffix');
$aTmpEntryList = self::$_oLdap->getEntryList($sTopOu,array('objectClass'=>'user','sAMAccountName'=>$rtx),array('description'),0,0);
if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){
for($i = 0; $i < $aTmpEntryList['count']; $i++){
$aTmpEntry = $aTmpEntryList[$i];
if($aTmpEntry['description'][0] !== $sFixUid){
return $this->_genRet(-2,"RTX:{$rtx}域服务器已存在---line:".__LINE__);
}
}
}
$dn = $this->_getOuDn($pid);
$aTmpEntryList = self::$_oLdap->getEntryList($dn,array('objectClass'=>'user','name'=>$name),array('description'));
if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){
for($i = 0; $i < $aTmpEntryList['count']; $i++){
$aTmpEntry = $aTmpEntryList[$i];
if($aTmpEntry['description'][0] !== $sFixUid){
return $this->_genRet(-3,"中文名:{$name}同一小组下域服务器已存在---line:".__LINE__);
}
}
}
$aAttr = $this->_getUserData($newData);
//移动节点
//0.老节点存在 移动老节点到新节点
// 0.0名称修改了 或者部门修改了 才需要移动 否则只需要更新即可
// 0.1先更新老节点信息 再移动到指定的节点
// 0.3AD服务器老节点用户ID跟数据库中用户ID相同时,才能移动老节点,否则移动了其它人的节点,则悲剧了
//1.老节点不存在
// 1.0 新节点不存在 添加
// 1.1 新节点存在 更新
//2.无论节点属性怎么修改 工号是不会变化的
// 2.0根据工号找到AD服务器已存在的节点
// 2.1根据工号找出了多个用户节点 需要找运维开发人功能核对处理
$sNewCn = "CN={$name},".$dn; //未包含DN根路径
$nameDel = "{$name}{$sFixUid}";
if($newData['status'] == self::STATUS_USER_LEAVE){ //员工离职需要移动到HasLeft节点下
$dn = self::$_aCfg['ldapDnHasLeftUser'];
$sNewCn = "CN={$nameDel},".$dn;
}
$aTmpEntryList = self::$_oLdap->getEntryList($sTopOu,array('objectClass'=>'user','description'=>$sFixUid),array(),0,0);
if($aTmpEntryList && ($aTmpEntryList['count'] > 0)){ //移动或者更新节点
if($aTmpEntryList['count'] > 1){
return $this->_genRet(-4,"AD服务器ERR:UID工号({$sFixUid})不唯一,请找运维开发人工处理".__LINE__);
}
$sOldCn = $aTmpEntryList[0]['dn']; //完整的DN路径
$sOldCn = str_replace(",$sTopOu",'',$sOldCn); //为包含DN根路径
unset($aAttr['name']);
$sUacField = 'userAccountControl';
$ssUacField = strtolower($sUacField);
if($aTmpEntryList[0][$ssUacField][0] & 0x10000){//如果密码不过期策略存在,一直保留(运维同事要求)
$aAttr[$sUacField][0] = $aAttr[$sUacField][0] | 0x10000;
}
$sAccountField = 'sAMAccountName';
$ssAccountField = strtolower($sAccountField);
if( strtolower($aTmpEntryList[0][$ssAccountField][0]) === strtolower($aAttr[$sAccountField][0]) ){
unset($aAttr[$sAccountField],$aAttr['userPrincipalName'],$aAttr['mail']); //当RTX账号字母相同时【不区分大小写】,不更新RTX账号【运维同事要求】
} else { //不同时,不能更新AD账号,会影响深信服账号策略同步,发邮件周知
$this->_notifyModAdAccount(array('new'=>$aAttr,'old'=>$aTmpEntryList[0],'acc1'=>$aAttr[$sAccountField][0],'acc2'=>$aTmpEntryList[0][$ssAccountField][0]));
unset($aAttr[$sAccountField],$aAttr['userPrincipalName'],$aAttr['mail']);
}
$aAttr = $this->_getAttrDiff($aAttr,$aTmpEntryList[0]);
if($bUpdate && $aUpdateField){ //脚本批量更新某些字段
$aTmpAttr = array();
foreach($aUpdateField as $k => $v){
if(isset($aAttr[$v])){
$aTmpAttr[$v] = $aAttr[$v];
}
}
$aAttr = $aTmpAttr;
}
//域管理员信息无权限修改 发邮件周知
if(isset($aTmpEntryList[0]['admincount']) && ($aTmpEntryList[0]['admincount']['count'] > 0)){
$this->_notifyModAdAdmin(array('attr'=>$aAttr,'dn'=>array($sNewCn,$sOldCn,$sTopOu)));
return $this->_genRet(0);
}
$bTmp = true;
if($aAttr){ //更新老节点属性
$bTmp = self::$_oLdap->updateEntry($sOldCn,$aAttr);
$this->_log('update','user',array('flag'=>$bTmp,'cn'=>$sOldCn,'attr'=>$aAttr),$newData,$aTmpEntryList[0]);
}
if(!$bTmp){
return $this->_genRet(-5,'工号:'.$uid.'---更新节点属性失败---line:'.__LINE__);
}
if($bUpdate){
return $this->_genRet(0);
}
if(strpos($sOldCn,$sNewCn) === false){ //节点DN路径修改了 需要移动节点
$sNewPartDn = "CN={$name}";
if($newData['status'] == 2){//员工离职需要移动到HasLeft节点下 加上工号
$sNewPartDn = "CN={$nameDel}";
}
$bTmp = self::$_oLdap->moveEntry($sOldCn,$sNewPartDn,$dn);
$this->_log('move','user',array('flag'=>$bTmp,'path'=>"{$sOldCn}->{$sNewPartDn},{$dn}",'dn'=>"{$sNewPartDn},{$dn}"),$newData,$oldData);
}
return $this->_genRet($bTmp ? 0 : -6,'工号:'.$uid.'---移动用户节点失败---line:'.__LINE__);
}
if($bUpdate){
return $this->_genRet(0);
}
//走到这一步 只剩下向域服务器添加新节点了
$aAttr = array_merge($this->_getUserDataForAdd(),$aAttr);
$bTmp = self::$_oLdap->addEntry($sNewCn,$aAttr);
$this->_log('add','user',array('flag'=>$bTmp,'cn'=>$sNewCn,'attr'=>$aAttr),$newData,$oldData);
return $this->_genRet($bTmp ? 0 : -7,'工号:'.$uid.'---更新用户节点失败---line:'.__LINE__);
} /**
* @param $aPar = array(
* 'dn' => array(new,old,top),
* 'attr' => array(),
* )
*/
private function _notifyModAdAdmin(array $aPar = array()){
$sTmpMail = '';
if($aPar['attr']){
$sTmpHead = '<tr><td>DN</td>';
$sTmp .= "<tr><td>{$aPar['dn'][1]},{$aPar['dn'][2]}</td>";
foreach($aPar['attr'] as $k => $v){
$sTmpHead .= "<td>{$k}</td>";
$sTmp .= '<td>'.implode(',',$v).'</td>';
}
$sTmpHead .= '</tr>';
$sTmp .= '</tr>';
$sTmpMail .= '<h2>更新属性信息</h2>';
$sTmpMail .= '<table border=1 cellspacing=1>';
$sTmpMail .= $sTmpHead;
$sTmpMail .= $sTmp;
$sTmpMail .= '</table>';
}
if($aPar['dn'] && ($aPar['dn'][0] !== $aPar['dn'][1])){
$sTmpMail .= '<h2>移动节点</h2>';
$sTmpMail .= "<p>从{$aPar['dn'][1]},{$aPar['dn'][2]}移动到{$aPar['dn'][0]},{$aPar['dn'][2]}</p>";
}
$title = 'BY后台尝试修改域管理员信息,请关注';
if($sTmpMail){
$this->_isTestSvr && $this->_log('def','def',array('f'=>__FUNCTION__.'.html','s'=>$sTmpMail));
Mail::queue('emails.ldap_notify',['data'=>$sTmpMail],function($m)use($title){
$m->to( self::$_aCfg['ldapMailTo'] )->subject($title);
});
}
} /**
* @param $aPar = array(
* 'dn' => array(new,old,top),
* 'attr' => array(),
* )
*/
private function _notifyModAdAccount(array $aPar = array()){
$sTmpMail = '';
if($aPar['acc1'] && $aPar['acc2']){
$sTmpMail = '<h2>更新账号</h2>';
$sTmpMail .= "<p>老账号:{$aPar['acc2']} => 新账号:{$aPar['acc1']}</p>";
}
$title = 'BY后台尝试修改员工RTX账号,请关注';
if($sTmpMail){
$this->_isTestSvr && $this->_log('def','def',array('f'=>__FUNCTION__.'.html','s'=>$sTmpMail));
Mail::queue('emails.ldap_notify',['data'=>$sTmpMail],function($m)use($title){
$m->to( self::$_aCfg['ldapMailTo'] )->subject($title);
});
}
} private function _getAttrDiff(array $aNewData = array(), array $aOldData = array()){
foreach($aNewData as $k => $v){
$k = strtolower($k);
if(!isset($aOldData[$k])){
continue;
}
if($v[0] === $aOldData[$k][0]){
unset($aNewData[$k]);
}
}
return $aNewData;
} /**
* @desc 组装用户节点数据
*
* @return array
*/
private $_aUserNodeAttr = array(
'givenName' => '名', //名
'sn' => '姓', //姓
'sAMAccountName' => 'RTX', //RTX
'userPrincipalName' => '登陆名', //登陆名
'description' => '工号', //工号
'pager' => '性别', //性别
'title' => '职位头衔', //职位头衔
'displayName' => '显示名', //显示名
'name' => '姓名', //姓名
//'employeeNumber' => array(00000000), //一卡通号
'mail' => '邮箱', //邮箱
//'mailNickname' => array($rtx), //邮箱昵称 启用了邮箱才有这个属性
'department' => '部门', //部门
'manager' => '上级', //上级
'userAccountControl' => '用户账号策略', //用户账号策略 0x0200正常账号 0x0002账号禁用
);
private function _getUserData(array $aData = array()){
//id,username,cname,sex,groupid,position,mposition,utype,boss,phone,email,status,degree
$rtx = trim($aData['username']);
$name = trim($aData['cname']);
$aNameSplit = $this->_getFirstLastName($name);
$uid = intval($aData['id']);
$sex = trim($aData['sex']);
$pid = intval($aData['groupid']); //部门ID
$jobId = intval($aData['position']);
$jobId2 = intval($aData['mposition']);
$boss = intval($aData['boss']);
$del = intval($aData['status']); $sSuffixDn = self::$_oLdap->getOption('dnSuffix');
$sTmpDc = substr($sSuffixDn,strpos($sSuffixDn,'DC=')+3);
$sTmpDc = str_replace(',DC=','.',$sTmpDc); $aAttr = array(
'givenName' => array($aNameSplit[1]), //名
'sn' => array($aNameSplit[0]), //姓
'sAMAccountName' => array($rtx), //RTX
'userPrincipalName' => array("{$rtx}@{$sTmpDc}"), //登陆名
'description' => array($this->_fixUid($uid)), //工号
'pager' => array($this->_fixSex($sex)), //性别
'title' => array($this->_fixTitle($jobId,$jobId2)), //职位头衔
'displayName' => array($name), //显示名
'name' => array($name), //姓名
//'employeeNumber' => array(00000000), //一卡通号
'mail' => array($rtx.'@boyaa.com'), //邮箱
//'mailNickname' => array($rtx), //邮箱昵称 启用了邮箱才有这个属性
'department' => array($this->_fixDepartment($pid)/*o('hr')->getGroup($pid, 'name')*/), //部门
'manager' => array($this->_getUserObjectId($boss,true)), //上级
'userAccountControl' => array($this->_fixUAC($del)), //用户账号策略 0x0200正常账号 0x0002账号禁用 0x10000密码永不过期
);
foreach($aAttr as $k => $v){
if(($v[0] === 0) || ($v[0] === '0')){
continue;
}
if(!$v[0]){
unset($aAttr[$k]);
}
}
return $aAttr;
} private function _getUserDataForAdd(){
return array(
//初始密码
'unicodePwd' => mb_convert_encoding('"'.self::$_aCfg['ldapInitPasswd'].'"', 'utf-16le'),
//设置新用户第一次登陆更改密码
'pwdLastSet' => array(0),
);
} private function _fixUid($uid){
if ($uid < 10000){
$uid = str_pad($uid,5,'0',STR_PAD_LEFT);
}
return (string)$uid;
} private function _fixSex($sex){
$sex = strtolower($sex);
$aMap = array(
1 => '0', // male
2 => '1', //female
);
return isset($aMap[$sex]) ? $aMap[$sex] : '-1';
} private function _fixTitle($j0 = 0,$j1 = 0){
$o = new User();
$o->position = $j0;
$o->mposition = $j1;
$s1 = $o->defPosition;
$s2 = '';
if($j1){
$s2 = $o->defMpositionName;
}
$title = '';
if($s1){
$title .= $s1;
}
if($s2){
$title .= "/{$s2}";
}
return $title;
} private function _fixUAC($status){
//用户账号策略 0x0200正常账号 0x0002账号禁用
//$status 1在职 2离职 0待入职
$ret = '';
$status = intval($status);
if($status === self::STATUS_USER_NOLEAVE){
$ret = 0x0200;
} else {
$ret = 0x0200 | 0x0002;
}
return (string)$ret;
} private $_aDepartmentCache = array();
private function _fixDepartment($id){
if($id == 1){ //一级部门OU = Dept;
return '';
}
if(isset($this->_aDepartmentCache[$id])){
return $this->_aDepartmentCache[$id];
}
$aRet = array();
$aOuList = (array)$this->_getOuList();
$aTmpOu = $aOuList[$id];
if (!$aTmpOu){
return '';
}
while($aTmpOu){
$aRet[] = $aTmpOu['name'];
$aTmpOu = isset( $aOuList[$aTmpOu['fid']] ) ? $aOuList[$aTmpOu['fid']] : [];
}
$aRet = (array)array_reverse($aRet);
$this->_aDepartmentCache[$id] = implode('\\',$aRet);
return $this->_aDepartmentCache[$id];
} /**
* @desc 中国现存复姓 来自:http://wenku.baidu.com/view/0d7070bc1a37f111f1855b24.html
*
*/
private $_aCnDblFamilyName = array(
'欧阳','太史','端木','上官','司马','东方','独孤',
'南宫','万俟','闻人','夏侯','诸葛','尉迟','公羊',
'赫连','澹台','皇甫','宗政','濮阳','公冶','太叔',
'申屠','公孙','慕容','仲孙','钟离','长孙','宇文',
'司徒','鲜于','司空','闾丘','子车','亓官','司寇',
'巫马','公西','颛孙','壤驷','公良','漆雕','乐正',
'宰父','谷梁','拓跋','夹谷','轩辕','令狐','段干',
'百里','呼延','东郭','南门','羊舌','微生','公户',
'公玉','公仪','梁丘','公仲','公上','公门','公山',
'公坚','左丘','公伯','西门','公祖','第五','公乘',
'贯丘','公皙','南荣','东里','东宫','仲长','子书',
'子桑','即墨','达奚','褚师',
); /**
* @desc 分离姓和名
*
* @return array
*/
private function _getFirstLastName($name = ''){
$name = trim($name);
if(!$name){
return array('',''); //[姓,名]
}
if(strpos($name,' ')){
$name = preg_replace('/\s+/',' ',$name);
$aName = explode(' ',$name);
return array($aName[1],$aName[0]);
}
$family = mb_substr($name,0,1,'utf-8');
$given = mb_substr($name,1,mb_strlen($name,'utf-8')-1,'utf-8');
if(mb_strlen($name) > 2){
$tmp = mb_substr($name,0,2);
if (in_array($tmp,$this->_aCnDblFamilyName)){
$family = $tmp;
$given = mb_substr($name,2);
}
}
return array($family,$given);
} public function __destruct(){
} }

查看客户端

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

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/588410.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-12
下一篇 2022-04-12

发表评论

登录后才能评论

评论列表(0条)

保存