因为 Ktor 本身只有实作 Authentication 机制,不像 Spring Security 有定义类似 UserDetails
, GrantedAuthority
的类别,也没有 @Secured
, @RoleAllowed
…检查使用者角色的方式,所以必须要自己设计实作 Role-Based Authorization 机制。
当 Ktor 验证请求成功後,就可以拿到 UserPrincipal 物件,里面包含使用者的 UserType 及 UserRole 资料,然後我们就可以继续验证使用者角色权限。
open class UserType(val projectId: String, val name: String) {
override val id: String = "${projectId}_${name}"
open val roles: Set<UserRole>? = null
}
class UserRole(
private val userTypeId: String,
override val name: String,
@Transient private val parent: UserRole? = null
) {
override val id: String = "${userTypeId}_$name"
}
class UserPrincipal(
val userType: UserType,
val userId: UUID,
val roles: Set<UserRole>? = null,
override val source: PrincipalSource,
var session: UserSession? = null
) : MyPrincipal()
我的想法是在 Ktor Authentication 的基础上扩展实作 Authoriation。以下面的例子而言,Ktor 是使用 authenticate function 指定 provider name auth-basic
,建立 scope 保护里面所有巢状阶层的 route。同样的作法,我想实作 authorization function 指定允许的角色 admin
,建立 scope 保护里面所有巢状阶层的 routes。
// ktor authentication function
routing {
authenticate("auth-basic") {
get("/login") {
// ...
}
}
}
// 自己实作 authorization function 取代 ktor authentication function
routing {
authorize(ClubAuth.Admin) {
post("/user") {
// ...
}
}
}
authorization function 是 Route 的 extension function,可接受多个不定长参数的 PrincipalAuth
物件,PrincipalAuth 是我们事先定义好的物件,含有 authentication provider name 及 user role 资讯,然後我们在 Authentication Pipeline 加上指定 provider name 保护的 authenticatedRoute
,这部分的实作是使用既有 Ktor Authentication Plugin 的机制,所以剩下检查 user role-based authorization 的部分需要自行设计实作。
验证顺序上是先做 authentication 再 authorization,所以我们在 Authentication.ChallengePhase
阶段已经可以从 Principal 取得使用者角色,接下来呼叫每一个 PrincipalAuth 的 allow()
function,如果没有任何一个 PrincipalAuth 检查通过,就丢出例外回传 AUTH_ROLE_FORBIDDEN
回应码。
fun Route.authorize(
vararg principalAuths: PrincipalAuth,
build: Route.() -> Unit
): Route {
val configurationNames = principalAuths.map { it.id }.toMutableList()
val authenticatedRoute = createChild(AuthorizationRouteSelector(configurationNames, principalAuths.toList()))
application.feature(Authentication).interceptPipeline(authenticatedRoute, configurationNames, false)
authenticatedRoute.intercept(Authentication.ChallengePhase) {
val principal = call.authentication.principal
if (principal == null) {
if (call.response.status() != null) finish()
else error("principal is null and no response in authorize challenge phase")
} else {
if (principalAuths.none { it.allow(principal, call) }) {
throw RequestException(InfraResponseCode.AUTH_ROLE_FORBIDDEN, "$principal is forbidden unable to access this api")
} else {
logger.debug("$principal authenticated")
}
}
}
authenticatedRoute.build()
return authenticatedRoute
}
PrincipalAuth 包含两种子类别 Service
及 User
,Service 使用於只需要验证呼叫端来源的情况,效果等於 authentication,至於 User 就要再多检查使用者角色。
我设计一个使用者 User 是属於某一种 UserType
,而且可以拥有该 UserType 的多个 UserRole
,例如後台人员有 admin, employee 角色,同时前台使用者有 admin, member 角色。只要 UserPrincipal 的 roles 符合任何一个 PrincipalAuth 的 roles,那麽 allow()
就会回传 true。
sealed class PrincipalAuth(
override val id: String,
val allowSources: Set<PrincipalSource>
) {
abstract fun allow(principal: MyPrincipal, call: ApplicationCall): Boolean
class Service(
providerName: String,
allowSources: Set<PrincipalSource>,
private val allowPredicate: ((ServicePrincipal, ApplicationCall) -> Boolean)? = null
) : PrincipalAuth(providerName, allowSources) {
override fun allow(principal: MyPrincipal, call: ApplicationCall): Boolean {
return if (principal is ServicePrincipal) {
if (!allowSources.contains(principal.source)) return false
allowPredicate?.invoke(principal, call) ?: true
} else false
}
}
class User(
providerName: String,
allowSources: Set<PrincipalSource>,
val typeRolesMap: Map<UserType, Set<UserRole>?>,
private val allowPredicate: ((UserPrincipal) -> Boolean)? = null
) : PrincipalAuth(providerName, allowSources) {
override fun allow(principal: MyPrincipal, call: ApplicationCall): Boolean {
return if (principal is UserPrincipal) {
if (!allowSources.contains(principal.source)) return false
if (!typeRolesMap.containsKey(principal.userType)) return false
val roles = typeRolesMap[principal.userType]
if (roles.isNullOrEmpty()) return true
if (principal.roles.isNullOrEmpty() || principal.roles.none { it in roles }) return false
allowPredicate?.invoke(principal) ?: true
} else false
}
}
}
UserType 及 UserRole 使用 enum class 实作而非字串,之後在 authorization function 指定角色时,才不会因为打错字而出错。
enum class ClubUserType(val value: UserType) {
User(object : UserType(ClubConst.projectId, "user") {
override val roles: Set<UserRole> = setOf(UserRole(id, "admin"), UserRole(id, "member"))
}
}
enum class ClubUserRole(val value: UserRole) {
Admin(ClubUserType.User.value.roles!!.first { it.name == "admin" }),
Member(ClubUserType.User.value.roles!!.first { it.name == "member" })
}
以下面的程序码为例,必须要满足以下条件才可以呼叫建立使用者的 API
User
,而且角色为 Admin
的使用者routing {
authorize(ClubAuth.Admin) {
post("/user") {
// ...
}
}
}
val Admin = PrincipalAuth.User(
userAuthProviderName, App,
mapOf(ClubUserType.User.value to setOf(ClubUserRole.Admin.value))
)
<<: [Day18] Esp32用STA mode + Relay - (程序码讲解)
>>: Day05: 05 - Django架构规划、资料库规划、商品资料准备
在Two Sum 中 我们一开始最初的想法是用2次的loop检查,那换做这3 Sum我们当然可以用三...
Intro UVa 一颗星选集 UVa Online Judge (wiki) 为线上自动评断系统,...
Toast元件可以短暂的在画面跳出提示讯息,并且不会影响Activity处理程序,当达到短暂秒数後便...
上一篇我们大致介绍了FOREIGN KEY的作用,那我们现在直接在MYSQL上操作给大家看喽! 首先...
本篇文章同步发表在 HKT 线上教室 部落格,线上影音教学课程已上架至 Udemy 和 Youtu...