You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ktor角色授权问题:全局配置覆盖与路由插件重复安装

问题描述

我用Ktor开发API,尝试实现角色授权,核心代码如下:

internal class RoleBaseConfiguration(
    var requiredRoles: Set<String> = emptySet()
)

internal val globalconfig = RoleBaseConfiguration()

internal val RoleAuthorizationPlugin = createApplicationPlugin(
    name = "RoleAuthorizationPlugin",
    createConfiguration = ::RoleBaseConfiguration
) {
    pluginConfig.apply {
        on(AuthenticationChecked) { call ->
            val jwtToken = call.request.headers["Authorization"]?.toJWT() ?: throw Exception("Missing principal")
            val roles = JWTConfig.verifier.verify(jwtToken).getClaim("role").toString().roleToSet()

            println("${globalconfig.requiredRoles} - $roles")

            if (roles.intersect(globalconfig.requiredRoles).isEmpty()) {
                call.respondText("You don`t have access to this resource.", status = HttpStatusCode.Unauthorized)
            }
        }
    }
}

fun Route.withRole(role: String, build: Route.() -> Unit) = withRoles(role, build = build)

fun Route.withRoles(vararg roles: String, build: Route.() -> Unit) {
    val authenticatedRoute = createChild(AuthorizationRouteSelector)

    globalconfig.requiredRoles = roles.toSet()

    authenticatedRoute.build()
    return authenticatedRoute
}

object AuthorizationRouteSelector : RouteSelector() {
    override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
        return RouteSelectorEvaluation.Transparent
    }

    override fun toString(): String = "(authorize \"default\" )"
}

fun String.roleToSet(): Set<String> {
    return split(",").map{ it.trim().replace("\"", "") }.toSet()
}

fun String.toJWT() = removePrefix("Bearer ")

路由调用方式:

route("/auth") {
    authenticate {
        withRoles("user", "admin"){
            get("register"){
                //Implementar el registro
            }
        }
    }
    withRole("admin"){
        get("test"){
            call.respond("Test")
        }
    }
}

问题现象:以user角色请求/auth/register接口时返回401,控制台输出[admin] - [user],可见所有路由复用了最后一次调用withRole/withRoles的角色配置。尝试改用createRouteScopePlugin替代createApplicationPlugin,但多次调用时会抛出插件已安装的异常,无法正常使用。

解决方案

核心问题是使用全局变量共享角色配置,导致所有路由分支复用同一规则。以下是两种可行的修复方案:

方案1:基于Route扩展属性+拦截器实现

无需插件,直接给每个路由分支绑定独立的角色要求,通过拦截器完成校验:

// 给Route添加扩展属性,存储当前路由分支需要的角色
private val Route.requiredRoles: MutableSet<String> by mutableStateOf(mutableSetOf())

fun Route.withRole(role: String, build: Route.() -> Unit) = withRoles(role, build = build)

fun Route.withRoles(vararg roles: String, build: Route.() -> Unit) {
    val authRoute = createChild(AuthorizationRouteSelector)
    // 给当前路由分支设置专属角色要求
    authRoute.requiredRoles.addAll(roles)

    // 添加认证后拦截器,校验角色
    authRoute.intercept(ApplicationCallPipeline.Authentication) {
        val jwtToken = call.request.headers["Authorization"]?.toJWT() ?: run {
            call.respondText("Missing principal", status = HttpStatusCode.Unauthorized)
            finish()
            return@intercept
        }

        val userRoles = JWTConfig.verifier.verify(jwtToken).getClaim("role").toString().roleToSet()
        
        if (userRoles.intersect(authRoute.requiredRoles).isEmpty()) {
            call.respondText("You don't have access to this resource.", status = HttpStatusCode.Unauthorized)
            finish()
        }
    }

    authRoute.build()
}

object AuthorizationRouteSelector : RouteSelector() {
    override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
        return RouteSelectorEvaluation.Transparent
    }

    override fun toString(): String = "(authorize)"
}

fun String.roleToSet(): Set<String> {
    return split(",").map { it.trim().replace("\"", "") }.toSet()
}

fun String.toJWT() = removePrefix("Bearer ")

方案2:正确使用createRouteScopePlugin

createRouteScopePlugin是路由范围的插件,需给每个子路由独立安装,避免全局复用:

internal class RoleBaseConfiguration(
    var requiredRoles: Set<String> = emptySet()
)

internal val RoleAuthorizationPlugin = createRouteScopePlugin(
    name = "RoleAuthorizationPlugin",
    createConfiguration = ::RoleBaseConfiguration
) {
    on(AuthenticationChecked) { call ->
        val jwtToken = call.request.headers["Authorization"]?.toJWT() ?: run {
            call.respondText("Missing principal", status = HttpStatusCode.Unauthorized)
            return@on
        }

        val userRoles = JWTConfig.verifier.verify(jwtToken).getClaim("role").toString().roleToSet()
        
        if (userRoles.intersect(pluginConfig.requiredRoles).isEmpty()) {
            call.respondText("You don't have access to this resource.", status = HttpStatusCode.Unauthorized)
        }
    }
}

fun Route.withRole(role: String, build: Route.() -> Unit) = withRoles(role, build = build)

fun Route.withRoles(vararg roles: String, build: Route.() -> Unit) {
    val authRoute = createChild(AuthorizationRouteSelector)
    // 给每个子路由独立安装插件并配置角色
    authRoute.install(RoleAuthorizationPlugin) {
        requiredRoles = roles.toSet()
    }
    authRoute.build()
}

object AuthorizationRouteSelector : RouteSelector() {
    override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
        return RouteSelectorEvaluation.Transparent
    }

    override fun toString(): String = "(authorize)"
}

fun String.roleToSet(): Set<String> {
    return split(",").map { it.trim().replace("\"", "") }.toSet()
}

fun String.toJWT() = removePrefix("Bearer ")

路由调用修正

确保所有需要认证的路由都嵌套在authenticate块内:

route("/auth") {
    authenticate {
        withRoles("user", "admin") {
            get("register") {
                call.respond("Register success")
            }
        }

        withRole("admin") {
            get("test") {
                call.respond("Test")
            }
        }
    }
}

关键修复点

  • 移除全局变量globalconfig,避免路由间配置污染
  • 每个路由分支独立存储/配置角色要求
  • 用路由拦截器或路由范围插件实现独立的角色校验逻辑

内容的提问来源于stack exchange,提问作者bruno Diaz martin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 19:24:56