3K整合系列(三) Ktor 角色權(quán)限控制

3K = Kotlin + Ktor + Ktorm,不要記錯(cuò)了哦

在上一篇里,我們成功整合了 Ktor 和 Ktorm,并完成了一個(gè)簡單的用戶登錄登出。在現(xiàn)實(shí)情況里,用戶的權(quán)限控制一直是非常重要的事情,對于沒有權(quán)限的用戶,某些接口就不能放行。

在 Ktor 里面,我們可以通過一種很簡單的方式來實(shí)現(xiàn)它,比如說:

get("/sample") {
    val u = user
    val perms = UserMapper.getPermissions(u.userId)
    if (!perms.contains("perm:user:info")) {
        call.respond(AjaxResult.error("你沒有權(quán)限訪問這個(gè)接口"))
        return@get
    }
    ... ...
}

是的,這是一般的實(shí)現(xiàn)方法,但是對于大部分接口都要寫這些代碼,就非常的不友好了,代碼量太多,也不好控制,而且重復(fù)的代碼非常讓人厭煩。所以在這里,我們需要用 AOP 的方法去解決,按以往對 Ktor 插件的了解,在這個(gè)場景下,我們也應(yīng)該通過編寫插件來解決問題,那么下面就正式開始吧。


首先我們要知道,Ktor 官方是有一套插件的標(biāo)準(zhǔn)格式的,只有這樣寫,Ktor 才會(huì)承認(rèn)它是一個(gè)合法的插件:

class RoleBasedAuthorization(internal var config: RoleAuthorizationConfig) {

    fun configure(block: RoleAuthorizationConfig.() -> Unit) {
        val newConfig = config.copy()
        block(newConfig)
        config = newConfig.copy()
    }

    companion object : BaseApplicationPlugin<Application, RoleAuthorizationConfig, RoleBasedAuthorization> {
        override val key: AttributeKey<RoleBasedAuthorization> = AttributeKey("RoleAuthorizationHolder")

        override fun install(pipeline: Application, configure: RoleAuthorizationConfig.() -> Unit): RoleBasedAuthorization {
            val cfg = RoleAuthorizationConfig().apply(configure)
            return RoleBasedAuthorization(cfg)
        }
    }
}

由于我們需要在插件里面能夠動(dòng)態(tài)獲取用戶的實(shí)際權(quán)限,因此在 Config 里面,就要把獲取權(quán)限的函數(shù),以及權(quán)限校驗(yàn)失敗的函數(shù)予以寫出:

class RoleAuthorizationConfig(
    var internalGetRoles: (Principal) -> Set<String> = { emptySet() },
    var internalRoleAuthFailed: suspend ApplicationCall.(message: String) -> Unit = {}) {

    fun getRoles(block: (Principal) -> Set<String>) {
        internalGetRoles = block
    }

    fun roleAuthFailed(block: suspend ApplicationCall.(message: String) -> Unit) {
        internalRoleAuthFailed = block
    }

    internal fun copy(): RoleAuthorizationConfig = RoleAuthorizationConfig(internalGetRoles, internalRoleAuthFailed)
}

首次看到這種寫法的同學(xué)請不要驚訝,這只是 Kotlin 強(qiáng)大的語言特性之一,你可以把匿名函數(shù)作為類型來使用。并且,如果你覺得每次都要寫函數(shù)定義很麻煩,也可以將函數(shù)定義成類型:

typealias GetRoleFunc = (Principal) -> Set<String>

好了,到這里配置的部分已經(jīng)完成了,我們可以向 Ktor 注冊這個(gè)插件:

install(RoleBasedAuthorization) {
    getRole {
        it as SessionUser
        UserMapper.getPermissions(it.userId)
    }
    roleAuthFailed {
        respond(AjaxResult.error(it))
    }
}

是不是很簡單,直接在插件里面就可以定義用于獲取權(quán)限的方法,以及在權(quán)限驗(yàn)證失敗后如何返回。下面我們就要將這個(gè)權(quán)限驗(yàn)證掛到某個(gè)路由上,也就是在指定的路由上 AOP 這個(gè)權(quán)限驗(yàn)證。

為了直觀起見,我們先定義好 AOP 的方式吧,就以上面的 get 方法來說,加入權(quán)限驗(yàn)證后,寫法變成這樣:

withRoles("perm:user:info") {
    get("/sample") {
        ... ...
    }
}

這樣看起來就一目了然了,不會(huì)對 get 內(nèi)部的代碼造成侵入。所以現(xiàn)在要寫的,就是這個(gè) withRoles 方法了。同樣的,我們先寫好架子:

fun Route.withRoles(vararg roles: String, build: Route.() -> Unit): Route {
    val authorizedRoute = createChild(RoleAuthorizationRouteSelector(roles.joinToString(",")))
    authorizedRoute.install(RoleAuthenticationInterceptors) {
        this.roles = roles.toSet()
    }
    authorizedRoute.build()
    return authorizedRoute
}

這些代碼也很好理解,為當(dāng)前的路由創(chuàng)建一個(gè)子路由,然后向該子路由安裝一個(gè)插件,并給定插件的參數(shù),最后構(gòu)建并返回該子路由。這里可以很清晰的看到 AOP 的過程了,不同于以往我們用注解寫 AOP,Ktor 的插件可是實(shí)打?qū)嵉拇a,每一步都讓你看得清清楚楚。

關(guān)于這個(gè) RoleAuthorizationRouteSelector,它是一個(gè)路由的選擇器,有幾種可選的模式,請參考表格:

選擇器模式 含義
Failed 路由未找到時(shí)可選
FailedPath 路由未找到時(shí)可選(與Failed相同)
FailedMethod 請求方式不允許時(shí)可選
FailedParameter 請求參數(shù)錯(cuò)誤時(shí)可選
Missing 有可選參數(shù)未填時(shí)可選
Constant 有靜態(tài)值被傳入時(shí)可選
Transparent 不會(huì)改變原有路由的入?yún)⑶闆r時(shí)可選(通常是選這個(gè))
ConstantPath 針對單個(gè)路由傳入靜態(tài)值時(shí)可選
WildcardPath 針對通配路由時(shí)可選

在這個(gè)表的基礎(chǔ)上,我們可以很輕松的寫出一個(gè)路由選擇器:

private class RoleAuthorizationRouteSelector(private val description: String) : RouteSelector() {
    override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation =
        RouteSelectorEvaluation.Transparent

    override fun toString(): String = "(authorize ${description})"
}

然后,RoleAuthenticationInterceptors 才是真正處理 AOP 動(dòng)作的插件,從它的命名上也可以看出來,這是個(gè)攔截器,把送往路由的請求先進(jìn)行攔截,經(jīng)過一系列操作后,再予以放行或者不放行。攔截器的代碼也是基本上固定的套路,如下所示:

private val RoleAuthenticationInterceptors: RouteScopedPlugin<RouteRoleAuthorizationConfig> =
    createRouteScopedPlugin("RoleAuthenticationInterceptors", ::RouteRoleAuthorizationConfig) {
        on(AuthenticationChecked) { call ->
            val reqRoles = pluginConfig.roles
            val authConfig = call.application.plugin(RoleBasedAuthorization).config
            val reqGetRoles = authConfig.internalGetRoles
            val reqOnFail = authConfig.internalRoleAuthFailed
            val user = call.principal<Principal>()
            if (user == null) {
                reqOnFail(call, "Unauthenticated User")
                return@on
            }
            val roles = reqGetRoles(user)
            val missing = reqRoles - roles
            if (missing.isNotEmpty()) {
                reqOnFail(call, "Principal $user lacks required role(s) ${missing.joinToString(" and ")}")
            }
        }
    }

Ktor 提供了兩種獲取配置的方式,一種是通過 pluginConfig,這種方式可以獲得在路由上動(dòng)態(tài)掛載的配置內(nèi)容,另一種是通過 call.application.plugin 來獲取,這種方式可以獲得通過靜態(tài)的 install 安裝的插件里所配置的內(nèi)容。

好了,基本上也算是寫完了,那就簡單的測試一下吧:

$ curl http://0.0.0.0:8080/sample -b cookie1
{
    "code": 500,
    "message": "請求失敗",
    "data": "Principal (userId=1, userName=admin) lacks required role(s) perm:user:info"
}

$ curl http://0.0.0.0:8080/sample -b cookie2
{
    "code": 200,
    "message": "請求成功",
    "data": {
        "userId": 1,
        "userName": "admin"
    }
}

似乎到了這里就已經(jīng)完成了對用戶權(quán)限的控制,但是我們是要精益求精的,比如說這段代碼,你看著沒覺得不舒服么:

install(RoleBasedAuthorization) {
    getRole {
        it as SessionUser
        UserMapper.getPermissions(it.userId)
    }
}

為什么還要 it as SessionUser 呢,為什么不是直接輸出一個(gè) SessionUser 類型的對象?

可能你會(huì)說,看了上面的代碼,似乎也沒有哪個(gè)地方可以讓我塞進(jìn)一個(gè)泛型呀。比如說想實(shí)現(xiàn)以下代碼是不能的:

class RoleAuthorizationConfig<T: Principal>(
    var internalGetRoles: (T) -> Set<String> = { emptySet() },
    fun getRoles(block: (T) -> Set<String>) {
        internalGetRoles = block
    }
    internal fun copy(): RoleAuthorizationConfig = RoleAuthorizationConfig(internalGetRoles, internalRoleAuthFailed)
}

這個(gè)代碼看起來是可行的,但是卻沒有辦法塞在 RoleBasedAuthorization 里,這意味著要讓 RoleBasedAuthorization 也帶上泛型,然而 Ktor 對于插件的要求又是不允許帶有泛型,這可如何是好?

答案是,在 Kotlin 里面是可以變魔術(shù)的,插件本體不允許帶泛型,可沒說插件里面的各個(gè)東西不能帶吧,正是在這個(gè)指導(dǎo)思想下,我們可以實(shí)現(xiàn)大魔術(shù)。以下的代碼是完整的角色權(quán)限插件,大家也可以親自體會(huì)一下這個(gè)魔術(shù)的原理:

enum class RoleAuthorizationType { ALL, ANY, NONE }

class RoleBasedAuthorization(internal var config: RoleBasedAuthorizationConfig) {
    fun configure(block: RoleBasedAuthorizationConfig.() -> Unit) {
        val newConfiguration = config.copy()
        block(newConfiguration)
        config = newConfiguration
    }

    companion object : BaseApplicationPlugin<Application, RoleBasedAuthorizationConfig, RoleBasedAuthorization> {
        override val key: AttributeKey<RoleBasedAuthorization> = AttributeKey("RoleBasedAuthorizationHolder")

        override fun install(pipeline: Application, configure: RoleBasedAuthorizationConfig.() -> Unit): RoleBasedAuthorization {
            val config = RoleBasedAuthorizationConfig().apply(configure)
            return RoleBasedAuthorization(config)
        }
    }
}

class RoleBasedAuthorizationConfig(
        var type: RoleAuthorizationType = RoleAuthorizationType.ANY,
        var roles: Set<String> = emptySet(),
        var provider: BaseRoleBasedAuthorizationProvider? = null) {
    internal fun copy(): RoleBasedAuthorizationConfig = RoleBasedAuthorizationConfig(type, roles, provider)
}

val RoleAuthenticationInterceptors: RouteScopedPlugin<RoleBasedAuthorizationConfig> =
        createRouteScopedPlugin("RoleAuthenticationInterceptors", ::RoleBasedAuthorizationConfig) {
            // 這種方式獲取真實(shí)使用時(shí)的配置內(nèi)容
            val reqRoles = pluginConfig.roles
            val reqType = pluginConfig.type
            // 這種方式獲取 install 時(shí)注冊的配置內(nèi)容
            val config = application.plugin(RoleBasedAuthorization).config
            on(AuthenticationChecked) { call ->
                if (call.isHandled) {
                    return@on
                }
                config.provider?.onRoleAuthorization(call, reqType, reqRoles)
            }
        }

private class RoleAuthorizationRouteSelector(private val description: Set<String>) : RouteSelector() {
    override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation =
            RouteSelectorEvaluation.Transparent

    override fun toString(): String = "(authorize ${description.joinToString(",")})"
}

private fun Route.withRoles(roles: Set<String>, roleAuthType: RoleAuthorizationType, build: Route.() -> Unit): Route {
    require(roles.isNotEmpty()) { "At least one role name need to be provided" }
    val roleAuthRoute = createChild(RoleAuthorizationRouteSelector(roles))
    roleAuthRoute.install(RoleAuthenticationInterceptors) {
        this.roles = roles
        this.type = roleAuthType
    }
    roleAuthRoute.build()
    return roleAuthRoute
}


abstract class BaseRoleBasedAuthorizationProvider {
    abstract suspend fun onRoleAuthorization(call: ApplicationCall, reqType: RoleAuthorizationType, reqRoles: Set<String>)
    open class Config protected constructor()
}

typealias RoleBasedAuthorizationGetRoleFunc<T> = suspend ApplicationCall.(T) -> Set<String>
typealias RoleBasedAuthorizationRoleAuthFailedFunc = suspend ApplicationCall.(String) -> Unit

class RoleBasedAuthorizationProvider<T : Any>(config: Config<T>) : BaseRoleBasedAuthorizationProvider() {

    private val type: KClass<T> = config.type
    private val getRoleFunc: RoleBasedAuthorizationGetRoleFunc<T> = config.getRoleFunc
    private val roleAuthFailedFunc: RoleBasedAuthorizationRoleAuthFailedFunc = config.roleAuthFailedFunc

    override suspend fun onRoleAuthorization(call: ApplicationCall, reqType: RoleAuthorizationType, reqRoles: Set<String>) {
        val user = call.sessions.get(type)
        if (user == null) {
            roleAuthFailedFunc(call, "Unauthenticated User")
            return
        }
        val roles = getRoleFunc(call, user)
        val denyReasons = mutableListOf<String>()
        when (reqType) {
            RoleAuthorizationType.ALL -> {
                val missing = reqRoles - roles
                if (missing.isNotEmpty()) {
                    denyReasons += "Principal $user lacks required role(s) ${missing.joinToString(" and ")}"
                }
            }

            RoleAuthorizationType.ANY -> {
                if (roles.none { it in reqRoles }) {
                    denyReasons += "Principal $user has none of the sufficient role(s) ${reqRoles.joinToString(" or ")}"
                }
            }

            RoleAuthorizationType.NONE -> {
                if (roles.any { it in reqRoles }) {
                    denyReasons += "Principal $user has forbidden role(s) ${reqRoles.intersect(roles).joinToString(" and ")}"
                }
            }
        }
        if (denyReasons.isNotEmpty()) {
            val message = denyReasons.joinToString(". ")
            roleAuthFailedFunc(call, message)
        }
    }

    class Config<T : Any>(internal val type: KClass<T>) : BaseRoleBasedAuthorizationProvider.Config() {
        internal var getRoleFunc: RoleBasedAuthorizationGetRoleFunc<T> = { emptySet() }
        internal var roleAuthFailedFunc: RoleBasedAuthorizationRoleAuthFailedFunc = {}
        fun getRole(block: RoleBasedAuthorizationGetRoleFunc<T>) {
            getRoleFunc = block
        }

        fun roleAuthFailed(block: RoleBasedAuthorizationRoleAuthFailedFunc) {
            roleAuthFailedFunc = block
        }

        fun buildProvider(): RoleBasedAuthorizationProvider<T> = RoleBasedAuthorizationProvider(this)
    }

}

inline fun <reified T : Any> RoleBasedAuthorizationConfig.roleSession(configure: RoleBasedAuthorizationProvider.Config<T>.() -> Unit) {
    val provider = RoleBasedAuthorizationProvider.Config(T::class).apply(configure).buildProvider()
    this.provider = provider
}

fun Route.withRoles(vararg roles: String, build: Route.() -> Unit) =
        withRoles(roles.toSet(), RoleAuthorizationType.ALL, build)

fun Route.withAnyRole(vararg roles: String, build: Route.() -> Unit) =
        withRoles(roles.toSet(), RoleAuthorizationType.ANY, build)

fun Route.withoutRoles(vararg roles: String, build: Route.() -> Unit) =
        withRoles(roles.toSet(), RoleAuthorizationType.NONE, build)

這里通過一個(gè)不帶泛型的 Base Provider 和一個(gè)帶泛型的實(shí)體 Provider 來實(shí)現(xiàn)了將泛型帶進(jìn)插件里。于是我們可以通過這樣的代碼來寫:

install(RoleBasedAuthorization) {
    roleSession<SessionUser> {
        getRole {
            UserMapper.getPermissions(it.userId)
        }
    }
}

為了進(jìn)一步方便起見,可以封裝一個(gè)函數(shù):

inline fun <reified T : Principal> Application.pluginRoleAuthorization(
        crossinline configure: RoleBasedAuthorizationProvider.Config<T>.() -> Unit
) = install(RoleBasedAuthorization) {
    roleSession<T>(configure)
}

這樣我們就可以用一種非常輕松的方法來使用插件了:

pluginRoleAuthorization<SessionUser> {
    getRole {
        UserMapper.getInfo(it.userId)?.perms ?: setOf()
    }
    roleAuthFailed {
        respond(AjaxResult.error("您沒有權(quán)限訪問這個(gè)接口"))
    }
}

好了,用戶角色權(quán)限控制到這里就結(jié)束了,愉快的使用封裝好的代碼吧,如果后續(xù)還需要其他的 AOP,也可以通過同樣的方式來實(shí)現(xiàn)插件,熟悉 Ktor 的插件機(jī)制,對于靈活使用這個(gè)框架有著相當(dāng)大的好處。

下一篇將講述如何在 Ktor 內(nèi)使用 JNI,從而實(shí)現(xiàn)與原生層交互,同時(shí)也將講述如何使用 Kotlin 本身的代碼來編寫一個(gè)標(biāo)準(zhǔn)的 JNI 庫。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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