在前面的两篇文章中,我详细的介绍了使用ldap与window AD服务集成,实现ToB项目中的身份认证集成方案,包括技术方案介绍、环境配置:
ToB项目身份认证AD集成(一):基于目录的用户管理、LDAP和Active Directory简述
ToB项目身份认证AD集成(二):一分钟搞定window server 2003部署AD域服务并支持ssl加密
在本文中,我将详细介绍如何利用 ldapjs
库使之一个 Node.js 服务类 LdapService
,该类实现了与 之前搭建的Windows AD 交互,包括用户搜索、身份验证、密码修改等功能。
也算是AD集成系列的完结吧,后续可能出其它客户端的对接,但目前工作核心在AI那块儿,大概率也不会继续了
一、实现方案和LdapService类概述
LdapService
类的核心是通过 LDAP(轻量级目录访问协议)与 AD 进行交互,提供用户搜索、认证、密码修改、重置等功能。下图是该类的基本结构,后续将一步步的介绍如何实现各个方法。
class LdapService {
client: Promise<ldap.Client>;
private config: MustProperty<LdapServiceConfig>;
constructor(config: LdapServiceConfig) {
this.config = {
...defaultConfig,
...config,
};
this.client = this.init();
}
async findUsers(
filter = this.config.userSearchFilter,
attributes: string[] = ["sAMAccountName", "userPrincipalName", "memberOf"]
) {
}
// 关闭连接
async close() {
(await this.client).destroy();
}
async findUser() {
}
// 修改用户密码的方法
async changePassword(
user: LdapUserSimInfo,
newPassword: string,
oldPassword: string
) {
}
// 用户认证的方法 - 检查密码是否正确
async checkPassword(user: LdapUserSimInfo, password: string) {
}
/*重置密码 */
async resetPassword(user: LdapUserSimInfo, resetPassword: string) {
}
private async init() {
const conf = this.config;
const client = ldap.createClient({
url: conf.url,
tlsOptions: {
minVersion: "TLSv1.2",
rejectUnauthorized: false,
},
});
await promisify(client.bind).call(client, conf.adminDN, conf.adminPassword);
return client; // 返回绑定后的客户端
}
private mergeSearchEntryObjectAttrs(entry: ldap.SearchEntryObject) {
}
private doSearch(client: ldap.Client, opts: ldap.SearchOptions) {
}
private encodePassword(password) {
}
private safeDn(dn: string) {
}
}
二、中文字段的特殊patch
ldap.js对于数据的字段进行了escape操作,会导致中文输入被转化成\xxx的形式,无论是接收的数据还是发送的请求,这时候会导致cn包含中文会出现错。需要用如下方法进行patch,通过在出现问题的rdn上配置unescaped参数控制是否对字符串进行escape(如果不知道啥是escape,参见十六进制转义escape介绍
const oldString = ldap.RDN.prototype.toString;
ldap.RDN.prototype.toString = function () {
return oldString.call(this, {
unescaped: this.unescaped });
};
加了这个补丁后,就可以控制rdn的转义情况了。
三、用户搜索功能
findUsers()
方法用于在 AD 中搜索用户,返回用户的基本信息。
async findUsers(
filter = this.config.userSearchFilter,
attributes: string[] = ["sAMAccountName", "userPrincipalName", "memberOf"]
): Promise<LdapUserSimInfo[]> {
await this.bindAsAdmin();
const opts = {
filter,
scope: "sub",
attributes: Array.from(new Set(["distinguishedName", "cn"].concat(attributes))),
};
const searchResult = await this.doSearch(await this.client, opts);
return searchResult.map((user) => {
return this.mergeSearchEntryObjectAttrs(user) as LdapUserSimInfo;
});
}
filter
是用于搜索的 LDAP 过滤器,默认为查找所有用户的(objectClass=user)
过滤器。attributes
参数允许指定返回哪些用户属性,默认返回sAMAccountName
、userPrincipalName
和memberOf
等属性。- 该方法调用了
doSearch()
进行搜索,并通过mergeSearchEntryObjectAttrs()
整理和转换 AD 返回的用户数据。
doSearch()
方法是实际进行 LDAP 搜索的地方:
private doSearch(client: ldap.Client, opts: ldap.SearchOptions) {
return new Promise<ldap.SearchEntryObject[]>((resolve, reject) => {
const entries = [] as ldap.SearchEntryObject[];
client.search(this.config.userSearchBase, opts, (err, res) => {
if (err) {
return reject(err);
}
res.on("searchEntry", (entry) => {
entries.push(entry.pojo);
});
res.on("end", (result) => {
if (result?.status !== 0) {
return reject(new Error(`Non-zero status from LDAP search: ${
result?.status}`));
}
resolve(entries);
});
res.on("error", (err) => {
reject(err);
});
});
});
}
client.search()
是ldapjs
提供的一个方法,用于执行搜索操作。搜索结果通过事件searchEntry
逐条返回,最终在end
事件时完成。
四、用户认证功能
checkPassword()
方法用于用户身份验证,检查用户输入的密码是否正确。
async checkPassword(user: LdapUserSimInfo, password: string) {
const userDN = user.objectName;
const client = await this.client;
await promisify(client.bind).call(client, userDN, password);
}
- 通过 LDAP 的
bind()
方法,可以尝试使用用户的 DN 和密码进行绑定。如果绑定成功,表示密码正确;否则,会抛出错误,表示认证失败。
五、密码修改功能
changePassword()
方法允许用户修改自己的密码。
async changePassword(user: LdapUserSimInfo, newPassword: string, oldPassword: string) {
await this.bindAsAdmin();
const userDN = this.safeDn(user.objectName);
const changes = [
new ldap.Change({
operation: "delete",
modification: new ldap.Attribute({
type: "unicodePwd",
values: [this.encodePassword(oldPassword)],
}),
}),
new ldap.Change({
operation: "add",
modification: new ldap.Attribute({
type: "unicodePwd",
values: [this.encodePassword(newPassword)],
}),
}),
];
const client = await this.client;
await promisify(client.modify).call(client, userDN, changes);
}
- 在修改密码时,LDAP 需要先删除旧密码,再添加新密码。这里使用
ldap.Change
创建修改操作,通过client.modify()
方法应用到 AD。
六、密码重置功能
resetPassword()
方法允许管理员重置用户的密码:
async resetPassword(user: LdapUserSimInfo, resetPassword: string) {
await this.bindAsAdmin();
const client = await this.client;
const userDN = this.safeDn(user.objectName);
const changes = new ldap.Change({
operation: "replace",
modification: new ldap.Attribute({
type: "unicodePwd",
values: [this.encodePassword(resetPassword)],
}),
});
await promisify(client.modify).call(client, userDN, changes);
}
- 与修改密码不同,重置密码直接使用
replace
操作,替换用户的现有密码。
七、结语
通过对 LdapService
类的逐步解析,相信你已经学会了如何利用 ldapjs
库与 Windows AD 进行交互。在实际使用中,还可以根据业务需求对这个类进行扩展,从而满足大规模企业系统中的用户管理需求。
另外这个中文的问题,暂时还只能是如此打补丁,期待社区修复可能不会那么及时