nodejs: v8.11
ldapjs: v1.0.2
// 看到网上有下面的例子程序,顺便拿来借用(多谢原作者),因为项目调用ldapjs的方式类似于下面的思路
http://blog.cuicc.com/blog/2017/03/26/nodejs-ECONNRESET/
var express = require('express');
var ldap = require('ldapjs');
...
var app = express();
app.post(
'auth',
function(req, res){
var user = {
username: req.body.username,
password: req.body.password,
dn : ''
}
var adminClient = ldap.creatClient({url:url});
adminClient.bind(admin.dn, admin.password, function(err){
assert.ifError(err);
var opts = {
filter: '(&(objectclass=user)(samaccountname=' + userId + '))',
scope: 'sub'
};
adminClient.search(base, opts, function(err, search){
search.on('searchEntry', function(entry){
user.dn = entry.dn;
});
search.on('end', function(err){
if(user.dn !== ''){
adminClient.unbind(err => {
});
var userClient = ldap.createClient({url:url});
userClient.bind(user.dn, user.password, function(err){
userClient.unbind(err => {
});
if(err){
res.json({'status':2, 'message':'invalid password'});
}
res.json({'status':0, 'message':'auth ok'});
});
} else {
res.json({'status': 1, 'message': 'invalid user'});
}
});
});
});
}
);
app.listen(8080, function(){
console.log('listening on port 8080');
});
项目中用到类似上面例子的ldap验证步骤做登录验证,
正常情况下,如果ldap server端,为用户做的配置是类似下面的全英文配置,是可以直接一次bind(mayun, password…)验证的
“dn”:”CN=mayun,OU=alipay,OU=Employee,OU=DEVELOP,DC=develop,DC=alipay,DC=local”
但是有时ldap server端,为用户做的配置是类似下面的全角字符或汉字,
“dn”:”CN=马云,OU=alipay,OU=Employee,OU=DEVELOP,DC=develop,DC=alipay,DC=local”
能想到的一个办法就是先申请个类似于admin_user/admin_pw的用户,配置到后台的配置文件中,先用这个admin做bind(admin_user, admin_pw…),
之后search()查找出用户userId的真正的entry.dn,
之后userClient.bind(user.dn, user.password)去做用户的ldap认证服务。验证成功后,程序继续处理正常的业务逻辑。
uncaughtException err--> Error: read ECONNRESET, err.stack-->
Error: read ECONNRESET
at _errnoException (util.js:1022:11)
at TCP.onread (net.js:628:25)
第一次在nodejs中看到这个异常,没有想到会是ldapjs抛出的异常,跟踪了好长时间,最终确定是ldapjs client的异常,
node –abort-on-uncaught-exception –track-heap-objects –trace_gc –trace_gc_nvp –trace_gc_verbose nodeserver.js
这里跟踪这个异常的过程有些曲折,本想查看nodejs的GC日志,内存占用等,但是并没有找出特别有用的信息,stackoverflow也有ECONNRESET的问题,但一直都以为是nodejs或者mongodb或者业务逻辑的程序的某个地方抛出的这个未捕获的异常,后来在开发环境上加上–abort-on-uncaught-exception后,当产生异常时,直接中断,并打印堆栈,不会抛出到uncaught去捕获(如果在uncaught捕获时,只能看到缩减版的堆栈信息,看不到是哪里抛出的异常), 这时直接打印日志看到是ldap.js的程序,才恍然大悟是ldapjs的问题。
stackoverflow有一篇文章ldapjs - LDAP connection error handling,解释如下:
ECONNRESET error occurs when after some idle time, the connection to LDAP service is reset. This is a common problem since a prolonged connection to ldap cannot be maintained
注: 可以看出ldap客户端到ldap服务端的connection,因为在server端不能维持一个持续很久的连接,所以其在server端idle时,可能就断开了,这时client端可能还需要请求这个已经存在的连接,也可能类似于心跳的服务,但是server端的connection已经关掉,这时会抛出ECONNRESET的异常
ldapjs的维护者pfmooney说:
unbind. (It will close the socket on your behalf after sending a message to the server)
注: 因为ldapjs也有个destroy方法,经过测试,这里释放connction时,把unbind改为destroy也可以。另外ldapjs的源码中destroy调用了unbind。
个人理解如果调用ldapjs时用到连接池的话,如果需要释放连接可能用unbind是正确的选择,因为用destroy释放后,Calling destroy will prevent any further reconnection from occurring,不允许重新连接。
还有一个比较重要的点需要注意:
有些文档中说可以ldapclient.on(‘error’…),加这个监听是正确的,
但是,不要在监听的回调函数里在调用destroy方法,destroy方法里也可能会再次emit触发error的监听,这样on error再次接收,之后再次destroy,形成死循环,耗尽cpu和内存
adminClient.on('error', err => {
// adminClient.destroy(); 这里不需要加destroy函数
});
ldapjs参考文档
http://ldapjs.org/examples.html
https://npmdoc.github.io/node-npmdoc-ldapjs/build/apidoc.html#apidoc.element.ldapjs.Client.prototype.unbind
https://stackoverflow.com/questions/41446941/ldapjs-ldap-connection-error-handling
https://github.com/joyent/node-ldapjs/issues/375
https://github.com/joyent/node-ldapjs/issues/317
https://github.com/joyent/node-ldapjs/issues/318
https://github.com/joyent/node-ldapjs/issues/142