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

Scala Http4s:如何正确组合两个不同角色的AuthMiddleware?

问题:组合不同角色的Basic Auth中间件失败

我为学生和管理员两个身份验证角色分别实现了两个Basic Auth中间件,代码如下:

学生中间件代码

private val basicStudentAuthMethod = Kleisli.apply[IO, Request[IO], Either[String, Student]] { req =>
val authHeader = req.headers.get[Authorization]
authHeader match {
  case Some(Authorization(BasicCredentials(creds))) if creds._1.trim == studentLogin && creds._2.trim == studentPassword =>
    IO(Right(Student(creds._1)))
  case Some(_) => IO(Left("Wrong creds"))
  case None => IO(Left("Auth error"))
 }
}

val studentBasicAuthMiddleware: AuthMiddleware[IO, Student] = AuthMiddleware(basicStudentAuthMethod, onFailure)

val authStudentRoutes = AuthedRoutes.of[Student, IO] {
case GET -> Root / "auth" as _ =>
    Ok("Welcome, student")
} 

管理员中间件代码

private val basicAdminAuthMethod = Kleisli.apply[IO, Request[IO], Either[String, Admin]] { req =>
val authHeader = req.headers.get[Authorization]
authHeader match {
  case Some(Authorization(BasicCredentials(creds))) if creds._1.trim == adminLogin && creds._2.trim == adminPassword =>
    IO(Right(Admin(creds._1)))
  case Some(_) => IO(Left("Wrong creds"))
  case None => IO(Left("Auth error"))
}
}

val adminBasicAuthMiddleware: AuthMiddleware[IO, Admin] = AuthMiddleware(basicAdminAuthMethod, onFailure)

val authAdminRoutes = AuthedRoutes.of[Admin, IO] {
  case GET -> Root / "auth" as _ =>
     Ok("Welcome, admin")
  }

通用失败处理

val onFailure: AuthedRoutes[String, IO] = Kleisli { (req: AuthedRequest[IO, String]) =>
     OptionT.pure[IO](Response(status = Status.Unauthorized))
}

我尝试用<+>组合两个中间件:

import cats.MonoidK.ops.toAllMonoidKOps

 val authRoute =  studentBasicAuthMiddleware(authStudentRoutes) <+> adminBasicAuthMiddleware(authAdminRoutes)

当前问题

传入正确学生凭证时功能正常,但传入正确管理员凭证时返回401 Unauthorized。预期逻辑是<+>先执行学生验证,失败后执行管理员验证,验证成功则返回对应响应;但实际流程是学生验证失败后,管理员验证成功,却又重复执行学生验证导致最终失败。

需要找到正确的方式组合这两个中间件。


解决方案

问题核心在于当前onFailure的实现:它直接返回了401响应,而不是让后续路由继续处理。<+>组合的是路由实例,而非验证逻辑,所以第一个中间件验证失败时,onFailure直接终止了请求流程,不会走到第二个路由。

方案1:先组合验证逻辑,再统一应用中间件

先把学生和管理员的验证逻辑组合成一个统一的验证流程,再应用到路由上:

// 定义统一的User特质,用来兼容学生和管理员类型
sealed trait User
case class Student(name: String) extends User
case class Admin(name: String) extends User

// 组合验证逻辑:先试学生验证,失败则尝试管理员验证
private val combinedAuthMethod: Kleisli[IO, Request[IO], Either[String, User]] = 
  basicStudentAuthMethod.map(_.map(_.asInstanceOf[User])).flatMapF {
    case Right(user) => IO.pure(Right(user))
    case Left(_) => basicAdminAuthMethod.map(_.map(_.asInstanceOf[User]))
  }

// 创建统一的中间件
val combinedAuthMiddleware: AuthMiddleware[IO, User] = AuthMiddleware(combinedAuthMethod, onFailure)

// 定义统一的路由,根据用户类型分支处理
val combinedAuthRoutes: AuthedRoutes[User, IO] = AuthedRoutes.of[User, IO] {
  case GET -> Root / "auth" as _: Student =>
    Ok("Welcome, student")
  case GET -> Root / "auth" as _: Admin =>
    Ok("Welcome, admin")
}

// 最终路由
val authRoute = combinedAuthMiddleware(combinedAuthRoutes)

方案2:修改onFailure,让验证失败时传递请求到下一个路由

如果不想统一用户类型,可以修改onFailure的实现,让它返回None而非直接返回401,这样<+>会继续尝试下一个路由的验证:

// 修改失败处理:返回None表示让后续路由继续处理
val onFailure: AuthedRoutes[String, IO] = Kleisli { _ =>
  OptionT.none[IO, Response[IO]]
}

// 保持原有的路由组合方式
val authRoute = studentBasicAuthMiddleware(authStudentRoutes) <+> adminBasicAuthMiddleware(authAdminRoutes)

这种方式下,第一个中间件验证失败时,onFailure不会直接返回响应,而是让<+>继续尝试第二个路由的验证;如果两者都验证失败,需要在最外层的路由组合中添加兜底的401处理。


内容的提问来源于stack exchange,提问作者Jelly

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 10:04:51