制作用戶帳戶體系時,使用 LDAP 有時是一個好的選擇,因為它讀性能優(yōu)異,并且?guī)趔w系下寫入數(shù)據(jù)的情況較少。最近我就遇到了需要集成 LDAP 用戶體系的場景。
還是先打開 Idea,建立一個 Ktor 工程,加入 LDAP 相關(guān)的配置,這里需要大家搞明白的是,連接 LDAP 就和連接數(shù)據(jù)庫一樣,是需要提供 LDAP 帳號密碼的,而不是簡單的一個 URL 連上后就可以直接調(diào)用帳號密碼登錄,這一點在網(wǎng)上很多文章內(nèi)都沒提及(或是故意被忽略了)。
配置內(nèi)容寫在 application.conf 內(nèi),寫法如下:
ldap {
url = "ldap://<ip or domain>"
baseDC = "ou=XXX,dc=XXX,dc=XXX"
factory = "com.sun.jndi.ldap.LdapCtxFactory"
authLevel = "simple"
principal = "cn=XXX,dc=XXX,dc=XXX"
password = "<password>"
timeout = 3000
}
這里的 ou, dc, cn 等值均是在 LDAP 內(nèi)進行設(shè)定的,其中 cn 即是用于連接 LDAP 的帳號。
然后我們可以寫一個簡單的類,來連接 LDAP:
class LDAPConnection(private val app: Application) {
private val baseDC: String = app.config("ktor.ldap.baseDC")
private val ctx: DirContext
init {
ctx = InitialLdapContext(Hashtable<String, String>().apply {
this[Context.PROVIDER_URL] = app.config("ktor.ldap.url")
this[Context.SECURITY_PRINCIPAL] = app.config("ktor.ldap.principal")
this[Context.SECURITY_CREDENTIALS] = app.config("ktor.ldap.password")
this[Context.SECURITY_AUTHENTICATION] = app.config("ktor.ldap.authLevel")
this[Context.INITIAL_CONTEXT_FACTORY] = app.config("ktor.ldap.factory")
this["com.sun.jndi.ldap.connect.timeout"] = app.config("ktor.ldap.timeout")
}, null)
}
}
此時就得到了一個 DirContext 的實例,下面對于登錄用戶的查詢也是由此實例進行的。
那么就直接寫一個登錄函數(shù)吧:
fun login(account: String, password: String): Pair<String, String> {
val nameList = ctx.search(baseDC, "uid=$account", SearchControls().apply { searchScope = SearchControls.SUBTREE_SCOPE }).toList()
if (nameList.isEmpty()) return "" to ""
val name = nameList[0]
val realName = name.attributes.get("cn")[0] as String
val pwd = name.attributes.get("userpassword")[0] as ByteArray
val pwdStr = pwd.map { it.toChar().toString() }.reduce { acc, s -> "$acc$s" }
val pwdCorrect = verifySHA(pwdStr, password)
return if (pwdCorrect) account to realName else "" to ""
}
對上面這段代碼簡單的作一個解釋,首先從 DirContext 內(nèi)查詢到指定 account 對應(yīng)的用戶,并組裝成列表。然后判斷列表是否為空,如果是空的,即表示沒有找到用戶,所以 account 是錯誤的。當(dāng) account 正確,可以找到條目時,獲取該用戶的真實姓名以及密碼,最后對密碼進行校驗,并判斷登錄是否成功。
關(guān)鍵代碼解析:
val pwdStr = pwd.map { it.toChar().toString() }.reduce { acc, s -> "$acc$s" }
由于從 LDAP 內(nèi)讀取的密碼內(nèi)容是 ByteArray ,并且在校驗時是按該 ByteArray 按位轉(zhuǎn)為十六進制字符串,所以在此先進行對該字符串的拼接。
verifySHA 用于實現(xiàn)密碼的校驗,具體實現(xiàn)如下:
private fun verifySHA(ldapPwd: String, inputPwd: String): Boolean {
val tmp = when {
ldapPwd.startsWith("{SSHA}") -> ldapPwd.substring(6)
ldapPwd.startsWith("{SHA}") -> ldapPwd.substring(5)
else -> ldapPwd
}
val bLdapPwd = Base64.getDecoder().decode(tmp)
val (shaCode, salt) = if (bLdapPwd.size <= 20) {
bLdapPwd to ByteArray(0)
} else {
ByteArray(20) { bLdapPwd[it] } to ByteArray(bLdapPwd.size - 20) { bLdapPwd[20 + it] }
}
val bInputPwd = MessageDigest.getInstance("SHA-1").run {
update(inputPwd.toByteArray())
update(salt)
digest()
}
return MessageDigest.isEqual(shaCode, bInputPwd)
}
LDAP 的密碼采用 SSHA 算法,其實就是在 SHA 算法上,加入了一個 salt 校驗段,通過上面的算法就可以計算出來了。
最后就可以實現(xiàn)登錄了,寫一個簡單的路由就完事了:
fun Routing.userRouting() {
post("/login") {
val params = call.requestParameters()
val account = params["account"] ?: ""
val password = params["password"] ?: ""
val (acc, realName) = application.ldap.login(account, password)
if (acc == "" || realName == "") {
localSession.account = ""
localSession.realName = ""
call.respondText { COMMON_FAILED }
} else {
localSession.account = acc
localSession.realName = realName
call.respondText { COMMON_SUCC }
}
}
}