[Ktor] 實現(xiàn)LDAP登錄

制作用戶帳戶體系時,使用 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 }
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容