一、前言
原本以为在Qt(Windows平台)中验证 LDAP 账户很简单:集成Open LDAP的开发库即可。结果临了才发现,Open LDAP压根儿不支持Windows平台。沿着重用的原则,考虑迁移Open LDAP的源代码,却发现工作量不小:特别是 socket 部分。
至此,也顾不上重用、跨平台了,先把 Windows 平台的搞定再说。结果发现 Windows 原本就提供了对 LDAP 的支持:Wldap32,且支持C/C++语言。
相关参考:https://learn.microsoft.com/zh-cn/previous-versions/windows/desktop/ldap/lightweight-directory-access-protocol-ldap-api。
二、实现过程
(1)引入Wldap32库(在.pro文件中):
LIBS += -lwldap32
(2)包含相关头文件:
#include <windows.h>
#include <winldap.h>
//必须保证 winldap.h 在 winber.h 前面包含
#include <winber.h>
(3)账户(uid:password)验证过程:
步骤 | 函数 | 依赖 |
---|---|---|
1.初始化会话 | ldap_init | 主机,端口 |
2.设置会话选项 | ldap_set_option | 设置为3.0版本 |
3.连接到LDAP服务器 | ldap_connect | 可设置连接超时 |
4.绑定到服务器 | ldap_bind_s | 绑定用DN,绑定用密码 |
5.按账户uid搜索条目并解析得到密码hash | ldap_search_s ldap_first_entry ldap_first_attribute ldap_get_values | 基础DN,筛选表达式 |
6.验证密码 | SSHA解码/编码 | LDAP中存储的密码hash,账户密码 |
其中主机、端口、绑定用DN、基础DN、搜索筛选表达式、uid等内容和规格参见实例图。
(4)实例图:
三、关键代码
附后。
四、后语(问题&总结)
(1)QString 与宽字符串的相互转换
QString 的方法 toStdWString() 用于将 QString 转换成宽字符串。
QString 的方法 fromStdWString() 用于将宽字符串转换成 QString。
(2)SSHA解密/加密步骤:
① 从 LDAP 所保存的 hash 中抽出 Salt;
② 使用①得到的 Salt 和需验证的密码生成 SHA-1 的 hash 值;
③ 比较②得到的 hash 值与 LDAP 中的hash值。
附录
(1)代码片段:绑定到服务器
int LdapUtil::bind(LDAP *pSession, const QString &bindDNStr, const QString &credStr)
{
ULONG retCode = LDAP_SUCCESS;
//认证信息
std::wstring wBindDnStr = bindDNStr.toStdWString();
std::wstring wCredStr = credStr.toStdWString();
PWSTR bindDN = (PWSTR) wBindDnStr.c_str();
PWSTR cred = (PWSTR) wCredStr.c_str();
ULONG method = LDAP_AUTH_SIMPLE; //识别模式
//向LDAP服务器认证客户端
retCode = ldap_bind_s(pSession, bindDN, cred, method);
if (retCode != LDAP_SUCCESS) {
qDebug() << "Invoke ldap_bind_s fail, error code =" << retCode;
}
return retCode;
}
(2)代码片段:按uid搜索条目并解析得到密码(hash)
QString LdapUtil::getUserPassword(LDAP *pSession,
const QString &baseDNStr,
const QString &uid,
int &retCode)
{
QString templ = "(&(objectClass=person)(objectClass=organizationalPerson)(uid=%1))";
std::wstring wBaseDN = baseDNStr.toStdWString();
PWSTR baseDN = (PWSTR) wBaseDN.c_str();
const QString filterStr = templStr.replace("%1", uid);
std::wstring wFilter = filterStr.toStdWString();
PWSTR filter = (PWSTR) wFilter.c_str();
//查询密码
PWCHAR attrs[2];
attrs[0] = (PWCHAR) L"userPassword";
attrs[1] = NULL;
retCode = LDAP_SUCCESS;
LDAPMessage *pSearchResult = NULL;
QString ret;
retCode = ldap_search_s(pSession, baseDN, LDAP_SCOPE_SUBTREE, filter, attrs, 0,
&pSearchResult);
if (retCode != LDAP_SUCCESS) {
qDebug() << "Invoke ldap_search_s fail, error code ="
<< QString::fromStdWString(ldap_err2string(retCode));
if (pSearchResult != NULL)
ldap_msgfree(pSearchResult);
return ret;
}
ULONG numberOfEntries = ldap_count_entries(pSession, pSearchResult);
if (numberOfEntries < 1) { //检索到的条目为空
retCode = -1;
return ret;
}
LDAPMessage *pEntry = NULL;
pEntry = ldap_first_entry(pSession, pSearchResult);
if (pEntry == NULL) { //Not found any entry
ldap_msgfree(pSearchResult);
retCode = -1;
return ret;
}
BerElement *pBer = NULL;
PWCHAR pAttribute = NULL;
// Get the first attribute name.
pAttribute = ldap_first_attribute(pSession, // Session handle
pEntry, // Current entry
&pBer); // [out] Current BerElement
if (pBer != NULL) {
ber_free(pBer, 0);
pBer = NULL;
}
if (pAttribute == NULL) { //Not found the attribute
ldap_msgfree(pSearchResult);
retCode = -1;
return ret;
}
// Get the string values.
PWCHAR *ppValue = NULL;
ppValue = ldap_get_values(pSession, // Session Handle
pEntry, // Current entry
pAttribute); // Current attribute
if (ppValue == NULL) { //Get attribute's value fail
qDebug() << ": [NO ATTRIBUTE VALUE RETURNED]";
ldap_msgfree(pSearchResult);
retCode = -1;
return ret;
}
// Output the attribute values
ULONG iValue = 0;
iValue = ldap_count_values(ppValue);
if (!iValue) {
qDebug() << ": [BAD VALUE LIST]";
// Free memory.
if (ppValue != NULL)
ldap_value_free(ppValue);
ppValue = NULL;
ldap_memfree(pAttribute);
ldap_msgfree(pSearchResult);
retCode = -1;
return ret;
}
// Output the first attribute value
ret = QString::fromStdWString(*ppValue);
// Free memory.
if (ppValue != NULL) {
ldap_value_free(ppValue);
}
ppValue = NULL;
ldap_memfree(pAttribute);
ldap_msgfree(pSearchResult);
retCode = LDAP_SUCCESS;
return ret;
}
(3)代码片段:校验Salted SHA密码
bool LdapUtil::verifySaltSha(const QString &pass, const QString &storedHash)
{
QString ldapPass;
if (storedHash.startsWith("{SSHA}")) {
ldapPass = storedHash.mid(6);
} else if (storedHash.startsWith("{SHA}")) {
ldapPass = storedHash.mid(5);
}
//按base64解密
QByteArray decodedData = QByteArray::fromBase64(ldapPass.toUtf8());
//从密文中获取Salt
QByteArray salt = __getSalt(decodedData);
//再加密明文(得到密文为base64加密)
QByteArray newHash = sshaEncrypt(pass, salt);
return (QByteArray::fromBase64(newHash) == decodedData);
}
【完】