如何通过嵌套现有指令为 Akka-Http 2 创建自定义指令?
How can I create a custom directive for Akka-Http 2 by nesting existing Directives?
我正在尝试创建一个自定义指令,稍后我可以使用它来验证用户角色,如下所示:
val route = put & authorize(ADMIN) {
// do sth
}
或
val route = put {
authorize(ADMIN) {
//do sth
}
}
。这是我到目前为止得到的:
def authorize(role: String): Directive0 = {
extractToken { t: String =>
validateToken(t) {
extractRoles(t) { roles: Seq[String] =>
validate(roles.contains(role), s"User does not have the required role")
}
}
}
}
def extractRoles(token: String): Directive1[Seq[String]] = {
token match {
case JsonWebToken(header, claimsSet, signature) => {
val decoded = decodeJWT(token)
decoded match {
case Some(_) => provide(extractRoleCodesFromJWT(decoded.get))
case None => provide(Seq())
}
}
case x =>
provide(Seq())
}
}
def validateToken(token: String): Directive0 = validate(validateJWT(token), INVALID_TOKEN_MESSAGE)
def extractToken: Directive1[Option[String]] = extractToken | extractHeader
def extractCookie: Directive1[Option[String]] = optionalCookie(JWT_COOKIE_NAME).map(_.map(_.value))
def extractHeader: Directive1[Option[String]] = optionalHeaderValueByName(JWT_HEADER_NAME)
所有编译都很好,除了我想稍后使用的实际 Directive0(授权)。最里面的行 validate(...)
显示编译错误 "Expression of type server.Directive0 doesn't conform to expected type server.Route"
如何正确嵌套我的其他指令以形成我的授权指令?
或者我可以用其他方式连接它们,而不是嵌套吗?
不幸的是,关于自定义指令的文档非常薄。
[更新]
感谢 Java Anto 的指点,这就是我想出的。
def authorize(role: String): Directive0 = {
extractToken.flatMap(validateToken)
.flatMap(extractRoles)
.flatMap(roles => validate(roles.contains(role), MISSING_ROLE_MESSAGE))
}
这会编译并有望完成我正确测试它的技巧。
感谢@Java Anto 的指点,下面是对我有用的解决方案。
trait AuthorizationDirectives extends JWTService {
val JWT_COOKIE_NAME = "jwt_cookie_name"
val JWT_HEADER_NAME = "jwt_cookie_header"
val INVALID_TOKEN_MESSAGE = "The provided token is not valid."
val MISSING_ROLE_MESSAGE = "User does not have the required role."
def authorizeRole(role: String): Directive0 = {
extractToken.flatMap(validateToken)
.flatMap(extractRoles)
.flatMap(validateRole(role)(_))
}
def extractRoles(token: String): Directive1[String] = {
// decode the token
val decoded = decodeJWT(token)
// check if decode was successful
decoded match {
case Some(_) => {
// get the role string
val rolesString = extractRoleCodesFromJWT(decoded.get)
rolesString match {
// return rolestring if present
case Some(_) => provide(rolesString.get)
case None => reject(AuthorizationFailedRejection)
}
}
case None => reject(AuthorizationFailedRejection)
}
}
def validateRole(role: String)(roles: String): Directive0 = {
if (roles.contains(role))
pass
else
reject(AuthorizationFailedRejection)
}
def validateToken(token: Option[String]): Directive1[String] = {
token match {
case Some(_) => validate(validateJWT(token.get), INVALID_TOKEN_MESSAGE) & provide(token.get)
case None => reject(AuthorizationFailedRejection)
}
}
def extractToken: Directive1[Option[String]] = {
extractCookie.flatMap {
case None => extractHeader
case t@Some(_) => provide(t)
}
}
def extractCookie: Directive1[Option[String]] = optionalCookie(JWT_COOKIE_NAME).map(_.map(_.value))
def extractHeader: Directive1[Option[String]] = optionalHeaderValueByName(JWT_HEADER_NAME)
}
指令可以这样使用:
trait CRUDServiceRoute[ENTITY, UPDATE] extends BaseServiceRoute with Protocols with AuthorizationDirectives with CorsSupport {
import StatusCodes._
val requiredRoleForEdit = USER
val crudRoute: Route =
pathEndOrSingleSlash {
corsHandler {
get {
complete(getAll().map(entityToJson))
} ~
put {
authorizeRole(requiredRoleForEdit) {
entity(entityUnmarshaller) { t =>
complete(Created -> create(t).map(idToJson))
}
}
}
}
} ~
pathPrefix(IntNumber) { id =>
pathEndOrSingleSlash {
corsHandler {
get {
complete(getById(id).map(entityToJson))
}
} ~
corsHandler {
put {
authorizeRole(requiredRoleForEdit) {
entity(updateUnmarshaller) { tupd =>
complete(update(id, tupd).map(entityToJson))
}
}
}
} ~
delete {
corsHandler {
authorizeRole(requiredRoleForEdit) {
onSuccess(remove(id)) { ignored =>
complete(NoContent)
}
}
}
}
}
}
def entityToJson(value: Seq[ENTITY]): JsValue
def entityToJson(value: Option[ENTITY]): JsValue
def idToJson(value: Option[Long]): JsValue
def entityUnmarshaller: FromRequestUnmarshaller[ENTITY]
def updateUnmarshaller: FromRequestUnmarshaller[UPDATE]
// CRUD service methods. These are implemented by the services used in the concrete routes
def getAll(): Future[Seq[ENTITY]]
def getById(id: Long): Future[Option[ENTITY]]
def create(entity: ENTITY): Future[Option[Long]]
def update(id: Long, noteUpdate: UPDATE): Future[Option[ENTITY]]
def remove(id: Long): Future[Int]
}
我正在尝试创建一个自定义指令,稍后我可以使用它来验证用户角色,如下所示:
val route = put & authorize(ADMIN) {
// do sth
}
或
val route = put {
authorize(ADMIN) {
//do sth
}
}
。这是我到目前为止得到的:
def authorize(role: String): Directive0 = {
extractToken { t: String =>
validateToken(t) {
extractRoles(t) { roles: Seq[String] =>
validate(roles.contains(role), s"User does not have the required role")
}
}
}
}
def extractRoles(token: String): Directive1[Seq[String]] = {
token match {
case JsonWebToken(header, claimsSet, signature) => {
val decoded = decodeJWT(token)
decoded match {
case Some(_) => provide(extractRoleCodesFromJWT(decoded.get))
case None => provide(Seq())
}
}
case x =>
provide(Seq())
}
}
def validateToken(token: String): Directive0 = validate(validateJWT(token), INVALID_TOKEN_MESSAGE)
def extractToken: Directive1[Option[String]] = extractToken | extractHeader
def extractCookie: Directive1[Option[String]] = optionalCookie(JWT_COOKIE_NAME).map(_.map(_.value))
def extractHeader: Directive1[Option[String]] = optionalHeaderValueByName(JWT_HEADER_NAME)
所有编译都很好,除了我想稍后使用的实际 Directive0(授权)。最里面的行 validate(...)
显示编译错误 "Expression of type server.Directive0 doesn't conform to expected type server.Route"
如何正确嵌套我的其他指令以形成我的授权指令? 或者我可以用其他方式连接它们,而不是嵌套吗? 不幸的是,关于自定义指令的文档非常薄。
[更新]
感谢 Java Anto 的指点,这就是我想出的。
def authorize(role: String): Directive0 = {
extractToken.flatMap(validateToken)
.flatMap(extractRoles)
.flatMap(roles => validate(roles.contains(role), MISSING_ROLE_MESSAGE))
}
这会编译并有望完成我正确测试它的技巧。
感谢@Java Anto 的指点,下面是对我有用的解决方案。
trait AuthorizationDirectives extends JWTService {
val JWT_COOKIE_NAME = "jwt_cookie_name"
val JWT_HEADER_NAME = "jwt_cookie_header"
val INVALID_TOKEN_MESSAGE = "The provided token is not valid."
val MISSING_ROLE_MESSAGE = "User does not have the required role."
def authorizeRole(role: String): Directive0 = {
extractToken.flatMap(validateToken)
.flatMap(extractRoles)
.flatMap(validateRole(role)(_))
}
def extractRoles(token: String): Directive1[String] = {
// decode the token
val decoded = decodeJWT(token)
// check if decode was successful
decoded match {
case Some(_) => {
// get the role string
val rolesString = extractRoleCodesFromJWT(decoded.get)
rolesString match {
// return rolestring if present
case Some(_) => provide(rolesString.get)
case None => reject(AuthorizationFailedRejection)
}
}
case None => reject(AuthorizationFailedRejection)
}
}
def validateRole(role: String)(roles: String): Directive0 = {
if (roles.contains(role))
pass
else
reject(AuthorizationFailedRejection)
}
def validateToken(token: Option[String]): Directive1[String] = {
token match {
case Some(_) => validate(validateJWT(token.get), INVALID_TOKEN_MESSAGE) & provide(token.get)
case None => reject(AuthorizationFailedRejection)
}
}
def extractToken: Directive1[Option[String]] = {
extractCookie.flatMap {
case None => extractHeader
case t@Some(_) => provide(t)
}
}
def extractCookie: Directive1[Option[String]] = optionalCookie(JWT_COOKIE_NAME).map(_.map(_.value))
def extractHeader: Directive1[Option[String]] = optionalHeaderValueByName(JWT_HEADER_NAME)
}
指令可以这样使用:
trait CRUDServiceRoute[ENTITY, UPDATE] extends BaseServiceRoute with Protocols with AuthorizationDirectives with CorsSupport {
import StatusCodes._
val requiredRoleForEdit = USER
val crudRoute: Route =
pathEndOrSingleSlash {
corsHandler {
get {
complete(getAll().map(entityToJson))
} ~
put {
authorizeRole(requiredRoleForEdit) {
entity(entityUnmarshaller) { t =>
complete(Created -> create(t).map(idToJson))
}
}
}
}
} ~
pathPrefix(IntNumber) { id =>
pathEndOrSingleSlash {
corsHandler {
get {
complete(getById(id).map(entityToJson))
}
} ~
corsHandler {
put {
authorizeRole(requiredRoleForEdit) {
entity(updateUnmarshaller) { tupd =>
complete(update(id, tupd).map(entityToJson))
}
}
}
} ~
delete {
corsHandler {
authorizeRole(requiredRoleForEdit) {
onSuccess(remove(id)) { ignored =>
complete(NoContent)
}
}
}
}
}
}
def entityToJson(value: Seq[ENTITY]): JsValue
def entityToJson(value: Option[ENTITY]): JsValue
def idToJson(value: Option[Long]): JsValue
def entityUnmarshaller: FromRequestUnmarshaller[ENTITY]
def updateUnmarshaller: FromRequestUnmarshaller[UPDATE]
// CRUD service methods. These are implemented by the services used in the concrete routes
def getAll(): Future[Seq[ENTITY]]
def getById(id: Long): Future[Option[ENTITY]]
def create(entity: ENTITY): Future[Option[Long]]
def update(id: Long, noteUpdate: UPDATE): Future[Option[ENTITY]]
def remove(id: Long): Future[Int]
}