之前推送过一篇文章浅谈企业内多应用系统间账号管理方案 —— LDAP,简要介绍了使用LDAP账号体系的缘由和基本技术概念,本篇将讲述LDAP服务开发过程,希望对其他有需要的业务系统有所帮助。主要技术栈有:firekylin、ThinkJs、NodeJs、MySQL、ReactJs,ldap服务使用ldapjs。
1. 思考
在切实开始写码前,想到的可能会遇到的问题如下:
配置的管理方式,如何处理得体验更好
已有账号体系与LDAP账号体系的兼容处理
已有历史数据的处理
LDAP账号数据变更后的同步处理
开启LDAP后,现有系统对账号的管理权限变化与对应提示
以上问题,是在一个既定系统内开发新服务所要考虑的,“三思而后行”,这些逻辑想清楚后代码实现会变得容易不少。
1.1 配置的管理方式,如何处理得体验更好
一般而言,应用系统的配置管理有两种形式:
一是,在系统后台通过配置文件进行管理,这需要一定的专业知识且一般需要运维人员支撑,管理不方便、配置不灵活
二是,在前端页面操作,在数据库中进行管理;访问系统时读取数据库配置,浏览器做相应的展示,且可以可视化的在页面修改配置;操作方面,管理灵活
firekylin正是使用了第二种方式进行配置管理,这为拓展服务带来了便利。
1.2 已有账号体系与LDAP账号体系的兼容处理
产品都是日趋完善的,业务系统间的管理也是这样,很可能已经运行一段时间,有属于自己的账号体系和其他业务数据,这时候为了更好的管理和协调资源会加入新的服务。新服务产生或依赖的数据与已有数据就可能会有冲突,需要做兼容处理。
通常情况下,企业内部都是以员工姓名拼音全拼做用户名,这样对两种账号体系的兼容带来了便利,大致的方案是:
两个账号体系中有相同用户名时,用LDAP中的数据覆盖已有用户数据;
仅LDAP中存在的用户,当用户首次登录时新增到业务系统的账号体系中;
仅业务系统中有的账号,如果还需在开启LDAP服务时继续使用,那么可以配置白名单账号,在白名单内的账号可以使用原账号密码进行登录
1.3 已有历史数据的处理
类似问题2的原因,已有数据也许做兼容处理。但是,业务系统中的数据是与账号体系强相关的,只要处理好账号体系,历史数据问题就解决了。
1.4 LDAP账号数据变更后的同步处理
开启LDAP服务是为了统一管理业务系统间使用的账号信息,因此系统会回收用户的新增、删除、编辑(角色类型可编辑)、密码重置权限。当LDAP中的账号信息变更时,就应该同步到业务系统中。这里不需要推送那么复杂的操作,只需要每次用户登录的时候将LDAP中最新的信息更新到业务系统中即可。
1.5 开启LDAP后,现有系统对账号的管理权限变化与对应提示
权限变化后,应该在页面有所提示,这些优化操作可以放在服务开发完成后进行。白名单、非白名单用户需要做不同的处理。
2. LDAP底层实现
LDAP从诞生至今,已经相对成熟,各种语言都有比较好的实现。因此,底层的实现,可以直接使用开源技术,不需要自己再去实现。
由于博客系统是基于NodeJs的,所以经过对比分析,最终选择了ldapjs。值得一提的是ldapjs同时支持Server端和Client端的ldap实现,可以对服务内的账号进行增、删、改、查操作。
博客系统的server端使用基于NodeJs的ThinkJs框架,此时ldapjs在其中是客户端(Client)的角色,因此使用的是ldapjs的Client API;ldap的服务端(Server)由运维侧部署的OpenLDAP实现。
ldapjs支持: APIDesc ----- Server API Reference for implementing LDAP servers. Client API Reference for implementing LDAP clients. DN API API reference for the DN class. Filter API API reference for LDAP search filters. Error API Listing of all ldapjs Error objects.
3. ldapjs的客户端实现
前提:已有部署好的LDAP Server,且知道地址和baseDn
查看官方文档之后,发现ldapjs的客户端实现只需要按照如下步骤使用即可,前期开发可以另起新项目只安装ldapjs的依赖,在nodejs环境中调试。
3.1 安装依赖
npminstallldapjs
3.2 创建客户端
varldap =require('ldapjs');
varclient = ldap.createClient({
url:'ldap://127.0.0.1:1389'
});
注意:创建方法还有其他可选参数,socketPath、log、timeout、connectTimeout、tlsOptions、idleTimeout、strictDN
3.3 使用客户端方法
有如下方法:
client.bind(dn, password, controls, callback);
client.add(dn, entry, controls, callback);
client.compare(dn, attribute, value, controls, callback);
client.del(dn, controls, callbak);
client.exop(name, value, controls, callback);
client.modify(name, changes, controls, callback);
client.modifyDN(dn, newDN, controls, callback);
client.search(base, options, controls, callback);
client.starttls(options, controls, callback);
client.unbind(callback);
3.4 ldap登录认证实现
根据ldapjs提供的以上方法实现
asyncvalidate(username, password) {
let{ url, ldap_baseDn, log, ldap_connect_timeout } =this.config;
if(!url) {
thrownewError('ldap url must setup!');
}
//创建LDAP client,把服务器url传入
letclient = ldap.createClient(
{
url,
ldap_connect_timeout: ldap_connect_timeout20000
}
);
log && think.log(`connecting$`,'LDAP');
letldapCn =`cn=$,$`;
log && think.log(`ldapCn:$`,'LDAP');
letres =newPromise((resolve, reject) =>{
// 将client绑定LDAP Server 第一个参数:是用户,必须是从根节点到用户节点的全路径 第二个参数:用户密码
client.bind(ldapCn, password,function(err){
if(!err) {
log && think.log('认证成功!','LDAP');
resolve(true);
}else{
log && think.log(`认证失败, errmsg:${JSON.stringify(err)}`,'LDAP');
resolve(false);
}
client.unbind(function(error){
if(error) {
log && think.log(error.message,'LDAP');
}else{
log && think.log('client disconnected','LDAP');
}
});
});
setTimeout(()=>{
log && think.log('connect timeout','LDAP');
reject('timeout');
}, ldap_connect_timeout);
}).catch(error=>{
returnerror;
});
returnres;
}
3.5 ldap获取用户信息实现
asyncgetUserInfo(username) {
letsession =this.session;
let{ url, ldap_baseDn, log, ldap_connect_timeout } =this.config;
if(!url !ldap_baseDn) {
thrownewError('ldap config missing!');
}
//创建LDAP client,把服务器url传入
letclient = ldap.createClient(
{
url,
ldap_connect_timeout: ldap_connect_timeout20000
}
);
log && think.log(`connecting$`,'LDAP');
log && think.log(`search:$`,'LDAP');
letres =newPromise((resolve, reject) =>{
// 创建LDAP查询选项 filter的作用就是相当于SQL的条件
letopts = {
filter:`(cn=$)`,// 查询条件过滤器,查找uid=kxh的用户节点
scope:'sub',// 查询范围,sub表示没有深度限制
timeLimit:500// 查询超时
};
client.search(ldap_baseDn, opts,function(err, res1){
//查询结果事件响应
res1.on('searchEntry',function(entry){
//获取查询的对象
letuser = entry.object;
letuserText =JSON.stringify(user);
session = user;
log && think.log(`search result:$`,'LDAP');
resolve(user);
});
//查询错误事件
res1.on('error',function(err){
log && think.error(`error:$`,'LDAP');
//unbind操作,必须要做
client.unbind(function(error){
if(error) {
log && think.log(error.message,'LDAP');
}else{
log && think.log('client disconnected','LDAP');
}
});
reject(err);
});
//查询结束
res1.on('end',function(result){
log && think.log(`search status:$`,'LDAP');
// 校验是否有结果
if(!session.dn) {
log && think.log('result: No such user','LDAP');
}
//unbind操作,必须要做
client.unbind(function(error){
if(error) {
log && think.log(error.message,'LDAP');
}else{
log && think.log('client disconnected','LDAP');
}
});
});
});
setTimeout(()=>{
log && think.log('connect timeout','LDAP');
reject('timeout');
}, ldap_connect_timeout);
}).catch(error=>{
returnerror;
});
returnres;
}
4. 博客系统中ldap服务开发
4.1 阅读firekylin源码,分析服务创建方案
阅读源码可知,firekylin博客系统采用的是MySQL存储配置的形式进行配置管理的,系统的服务配置均由前端页面可视化操作完成。笔者在第一版ldapjs的实现中用的是系统内配置的形式,配置麻烦且不够安全,目前已采用页面操作+MySQL存储的形式实现。
在系统中,两步验证服务提供了一个很好的模板,可以参考。逻辑是:当用户访问后台登录页时,查询配置,查看是否有配置了两步验证,如果没有则正常登录逻辑;如果有则进入两步验证逻辑,在登录逻辑中加入两步验证方案,通过则登录成功,反之失败;登录成功后,管理员用户可在系统配置-两步验证菜单中进行可视化配置操作。
同理,实现ldap配置,则需进行类似的开发。
4.2 添加页面路由、菜单
修改www/static/src/admin/page/options/index.js
...
getChildRoutes(nextState, callback) {
callback(null, [
require('./general'),
require('./reading'),
require('./two_factor_auth'),
require('./ldap'),// 添加此句
require('./comment'),
require('./upload'),
require('./analytic'),
require('./push'),
require('./import'),
require('./export')
]);
}
...
新建www/static/src/admin/page/options/ldap.js
module.exports = {
path:'ldap',
getComponent(nextState, callback) {
callback(null,require('../../component/options_ldap'));
}
}
修改www/static/src/admin/component/sidebar.jsx,在系统设置中添加ldap设置菜单
4.3 添加页面
新建www/static/src/admin/component/options_ldap.jsx,编写页面逻辑,将ldap所必须或可填的参数在页面展示
4.4 添加ldap服务
新建src/admin/service/ldap/index.js
'use strict';
importldapfrom'ldapjs';
exportdefaultclassLdap{
constructor(conf = {}) {
// ldap配置
// {
// url: 'ldap://x.x.x.x:389',
// ldap_baseDn: 'dc=ldap,dc=example,dc=com'
// }
this.config = {
url: conf.ldap_url'',
log: conf.ldap_log ==='1'?true:false,
...conf
};
this.session = {};
}
asyncgetUserInfo(username) {
...
}
asyncvalidate(username, password) {
...
}
}
4.5 调用ldap服务
修改src/admin/controller/user.js,在登录逻辑中加入ldap判断,详细请查看链接代码。
4.6 优化
优化登录页,屏蔽开启了LDAP服务的系统的找回密码功能
优化用户管理页,屏蔽开启了LDAP服务的系统管理员对非白名单用户的邮箱、密码修改功能,以及用户的新增、删除功能
优化个人密码修改页,屏蔽开启了LDAP服务的系统的密码修改功能
已实现的源码,请参见GitHub:
https://github.com/gitshan/firekylin/tree/custom
觉得本文有收获?请转发分享给更多人
关注「 物联网技术交流平台 」,了解更多知识
领取专属 10元无门槛券
私享最新 技术干货