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 庫。