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
相关产品推荐
相关产品推荐

