美文网首页
3K整合系列(三) Ktor 角色权限控制

3K整合系列(三) Ktor 角色权限控制

作者: 何晓杰Dev | 来源:发表于2023-08-26 15:17 被阅读0次

3K = Kotlin + Ktor + Ktorm,不要记错了哦

在上一篇里,我们成功整合了 Ktor 和 Ktorm,并完成了一个简单的用户登录登出。在现实情况里,用户的权限控制一直是非常重要的事情,对于没有权限的用户,某些接口就不能放行。

在 Ktor 里面,我们可以通过一种很简单的方式来实现它,比如说:

get("/sample") {
    val u = user
    val perms = UserMapper.getPermissions(u.userId)
    if (!perms.contains("perm:user:info")) {
        call.respond(AjaxResult.error("你没有权限访问这个接口"))
        return@get
    }
    ... ...
}

是的,这是一般的实现方法,但是对于大部分接口都要写这些代码,就非常的不友好了,代码量太多,也不好控制,而且重复的代码非常让人厌烦。所以在这里,我们需要用 AOP 的方法去解决,按以往对 Ktor 插件的了解,在这个场景下,我们也应该通过编写插件来解决问题,那么下面就正式开始吧。


首先我们要知道,Ktor 官方是有一套插件的标准格式的,只有这样写,Ktor 才会承认它是一个合法的插件:

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)
        }
    }
}

由于我们需要在插件里面能够动态获取用户的实际权限,因此在 Config 里面,就要把获取权限的函数,以及权限校验失败的函数予以写出:

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)
}

首次看到这种写法的同学请不要惊讶,这只是 Kotlin 强大的语言特性之一,你可以把匿名函数作为类型来使用。并且,如果你觉得每次都要写函数定义很麻烦,也可以将函数定义成类型:

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

好了,到这里配置的部分已经完成了,我们可以向 Ktor 注册这个插件:

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

是不是很简单,直接在插件里面就可以定义用于获取权限的方法,以及在权限验证失败后如何返回。下面我们就要将这个权限验证挂到某个路由上,也就是在指定的路由上 AOP 这个权限验证。

为了直观起见,我们先定义好 AOP 的方式吧,就以上面的 get 方法来说,加入权限验证后,写法变成这样:

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

这样看起来就一目了然了,不会对 get 内部的代码造成侵入。所以现在要写的,就是这个 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
}

这些代码也很好理解,为当前的路由创建一个子路由,然后向该子路由安装一个插件,并给定插件的参数,最后构建并返回该子路由。这里可以很清晰的看到 AOP 的过程了,不同于以往我们用注解写 AOP,Ktor 的插件可是实打实的代码,每一步都让你看得清清楚楚。

关于这个 RoleAuthorizationRouteSelector,它是一个路由的选择器,有几种可选的模式,请参考表格:

选择器模式 含义
Failed 路由未找到时可选
FailedPath 路由未找到时可选(与Failed相同)
FailedMethod 请求方式不允许时可选
FailedParameter 请求参数错误时可选
Missing 有可选参数未填时可选
Constant 有静态值被传入时可选
Transparent 不会改变原有路由的入参情况时可选(通常是选这个)
ConstantPath 针对单个路由传入静态值时可选
WildcardPath 针对通配路由时可选

在这个表的基础上,我们可以很轻松的写出一个路由选择器:

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 动作的插件,从它的命名上也可以看出来,这是个拦截器,把送往路由的请求先进行拦截,经过一系列操作后,再予以放行或者不放行。拦截器的代码也是基本上固定的套路,如下所示:

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,这种方式可以获得在路由上动态挂载的配置内容,另一种是通过 call.application.plugin 来获取,这种方式可以获得通过静态的 install 安装的插件里所配置的内容。

好了,基本上也算是写完了,那就简单的测试一下吧:

$ 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"
    }
}

似乎到了这里就已经完成了对用户权限的控制,但是我们是要精益求精的,比如说这段代码,你看着没觉得不舒服么:

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

为什么还要 it as SessionUser 呢,为什么不是直接输出一个 SessionUser 类型的对象?

可能你会说,看了上面的代码,似乎也没有哪个地方可以让我塞进一个泛型呀。比如说想实现以下代码是不能的:

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)
}

这个代码看起来是可行的,但是却没有办法塞在 RoleBasedAuthorization 里,这意味着要让 RoleBasedAuthorization 也带上泛型,然而 Ktor 对于插件的要求又是不允许带有泛型,这可如何是好?

答案是,在 Kotlin 里面是可以变魔术的,插件本体不允许带泛型,可没说插件里面的各个东西不能带吧,正是在这个指导思想下,我们可以实现大魔术。以下的代码是完整的角色权限插件,大家也可以亲自体会一下这个魔术的原理:

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) {
            // 这种方式获取真实使用时的配置内容
            val reqRoles = pluginConfig.roles
            val reqType = pluginConfig.type
            // 这种方式获取 install 时注册的配置内容
            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)

这里通过一个不带泛型的 Base Provider 和一个带泛型的实体 Provider 来实现了将泛型带进插件里。于是我们可以通过这样的代码来写:

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

为了进一步方便起见,可以封装一个函数:

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("您没有权限访问这个接口"))
    }
}

好了,用户角色权限控制到这里就结束了,愉快的使用封装好的代码吧,如果后续还需要其他的 AOP,也可以通过同样的方式来实现插件,熟悉 Ktor 的插件机制,对于灵活使用这个框架有着相当大的好处。

下一篇将讲述如何在 Ktor 内使用 JNI,从而实现与原生层交互,同时也将讲述如何使用 Kotlin 本身的代码来编写一个标准的 JNI 库。

相关文章

  • SpringBoot整合Shiro实现基于角色的权限访问控制(R

    SpringBoot整合Shiro实现基于角色的权限访问控制(RBAC)系统简单设计从零搭建 技术栈 : Spri...

  • Flask 构建微电影视频网站(5)

    基于角色的访问控制 权限管理 添加权限 权限列表 删除权限 编辑权限 修改对应的前端文件 角色管理 添加角色 角色...

  • RabbitMQ用户角色及权限控制

    RabbitMQ用户角色及权限控制 ####################### #用户角色 #########...

  • rbac权限管理

    概述 RBAC : 基于角色的权限访问控制(Role-Based Access Control),通过角色绑定权限...

  • Spring Boot+Spring Security基于RBA

    一:RBAC:基于角色的权限访问控制,用5张表实现,用户、角色、权限、用户角色多对多关联表、角色权限多对多关联表。...

  • SpringBoot之整合Spring Security,为自己

    需求: 1.权限控制:角色有多种角色,每个角色对应多个用户,每个角色又对应不同的菜单权限 2.资源...

  • WebGoat<二> Access Conreol Flaws

    在基于角色的访问控制方案中,角色表示一组访问权限和权限。用户可以被分配一个或多个角色。基于角色的访问控制方案通常由...

  • 权限控制

    RBAC模式进行权限控制,即(Role-Based Access Control)基于角色的访问控制。实现权限访问...

  • 21_shiro总结

    关于shiro总结 1 一般来说,实现权限控制至少要五张表:用户 --- 角色 --- 权限 ==> 三张实...

  • RBAC权限控制

    RBAC权限控制 RBAC: Role Based Access Controller ,即基于角色的访问权限...

网友评论

      本文标题:3K整合系列(三) Ktor 角色权限控制

      本文链接:https://www.haomeiwen.com/subject/mnqimdtx.html