42

我目前正在使用 Compojure(以及 Ring 和相关中间件)在 Clojure 中编写 API。

我正在尝试根据路由应用不同的身份验证代码。考虑以下代码:

(defroutes public-routes
  (GET "/public-endpoint" [] ("PUBLIC ENDPOINT")))

(defroutes user-routes
  (GET "/user-endpoint1" [] ("USER ENDPOINT 1"))
  (GET "/user-endpoint2" [] ("USER ENDPOINT 1")))

(defroutes admin-routes
  (GET "/admin-endpoint" [] ("ADMIN ENDPOINT")))

(def app
  (handler/api
    (routes
      public-routes
      (-> user-routes
          (wrap-basic-authentication user-auth?)))))
      (-> admin-routes
          (wrap-basic-authentication admin-auth?)))))

这不能按预期工作,因为wrap-basic-authentication确实包装了路由,因此无论包装的路由如何,它都会被尝试。具体来说,如果请求需要被路由到admin-routes,user-auth?仍然会被尝试(并且失败)。

我求助于在公共基本路径context根植一些路由,但这是一个相当大的限制(下面的代码可能不起作用,它只是为了说明这个想法):

(defroutes user-routes
  (GET "-endpoint1" [] ("USER ENDPOINT 1"))
  (GET "-endpoint2" [] ("USER ENDPOINT 1")))

(defroutes admin-routes
  (GET "-endpoint" [] ("ADMIN ENDPOINT")))

(def app
  (handler/api
    (routes
      public-routes
      (context "/user" []
        (-> user-routes
            (wrap-basic-authentication user-auth?)))
      (context "/admin" []
        (-> admin-routes
            (wrap-basic-authentication admin-auth?))))))

我想知道我是否遗漏了某些东西,或者是否有任何方法可以在不受限制且不使用通用基本路径的情况下实现我想要defroutes的(理想情况下,不会有)。

4

6 回答 6

22
(defroutes user-routes*
  (GET "-endpoint1" [] ("USER ENDPOINT 1"))
  (GET "-endpoint2" [] ("USER ENDPOINT 1")))

(def user-routes
     (-> #'user-routes*
         (wrap-basic-authentication user-auth?)))

(defroutes admin-routes*
  (GET "-endpoint" [] ("ADMIN ENDPOINT")))


(def admin-routes
     (-> #'admin-routes*
         (wrap-basic-authentication admin-auth?)))

(defroutes main-routes
  (ANY "*" [] admin-routes)
  (ANY "*" [] user-routes)

这将首先通过 admin-routes 然后通过用户路由运行传入请求,在这两种情况下都应用正确的身份验证。nil这里的主要思想是,如果调用者无法访问路由而不是抛出错误,则您的身份验证函数应该返回。这样,如果 a) 路由实际上与定义的 admin-routes 不匹配或 b) 用户没有所需的身份验证,则 admin-routes 将返回 nil。如果 admin-routes 返回 nil,则 compojure 将尝试用户路由。

希望这可以帮助。

编辑:前段时间我写了一篇关于 Compojure 的帖子,您可能会觉得它很有用:https ://vedang.me/techlog/2012-02-23-composability-and-compojure/

于 2012-05-30T18:43:58.860 回答
18

我偶然发现了这个问题,它似乎wrap-routes(compojure 1.3.2)优雅地解决了:

(def app
  (handler/api
    (routes
      public-routes
      (-> user-routes
          (wrap-routes wrap-basic-authentication user-auth?)))))
      (-> admin-routes
          (wrap-routes wrap-basic-authentication admin-auth?)))))
于 2015-04-16T06:54:10.640 回答
4

这是一个合理的问题,当我自己遇到这个问题时,我发现它非常棘手。

我想你想要的是这样的:

(defroutes public-routes
  (GET "/public-endpoint" [] ("PUBLIC ENDPOINT")))

(defroutes user-routes
  (GET "/user-endpoint1" _
       (wrap-basic-authentication
        user-auth?
        (fn [req] (ring.util.response/response "USER ENDPOINT 1"))))

  (GET "/user-endpoint2" _
       (wrap-basic-authentication
        user-auth?
        (fn [req] (ring.util.response/response "USER ENDPOINT 1")))))

(defroutes admin-routes
  (GET "/admin-endpoint" _
       (wrap-basic-authentication
        admin-auth? (fn [req] (ring.util.response/response "ADMIN ENDPOINT")))))

(def app
  (handler/api
   (routes
    public-routes
    user-routes
    admin-routes)))

需要注意两点:身份验证中间件位于路由表单内部,中间件调用一个匿名函数,该函数是一个真正的处理程序。为什么?

  1. 如您所说,您需要在路由应用身份验证中间件,否则请求将永远无法路由到身份验证中间件!换句话说,路由需要在认证环之外的中间件环上。

  2. 如果您使用 Compojure 的路由表单(如 GET),并且您在表单的主体中应用中间件,则中间件函数需要一个真正的环响应处理程序(即接受请求并返回响应的函数)作为其参数,而不是像字符串或响应映射这样更简单的东西。

这是因为,根据定义,像 wrap-basic-authentication 这样的中间件函数只将处理程序作为参数,而不是裸字符串或响应映射或其他任何东西。

那么为什么这么容易错过呢?原因是像 (GET [path args & body] ...) 这样的 Compojure 路由运算符尝试通过对允许在 body 字段中传递的形式非常灵活来使事情变得容易。你可以传入一个真正的处理函数,或者只是一个字符串,或者一个响应映射,或者可能是我没有想到的其他东西。这一切都render在 Compojure 内部的多方法中进行了布局。

这种灵活性掩盖了 GET 表单实际上在做什么,因此当您尝试做一些不同的事情时很容易混淆。

在我看来,在大多数情况下,vedang 的主要答案的问题并不是一个好主意。它本质上使用复合机器来回答“路由是否匹配请求?”这个问题。(如果没有,则返回 nil)以回答“请求是否通过身份验证?”的问题。这是有问题的,因为根据 HTTP 规范,通常您希望身份验证失败的请求返回带有 401 状态代码的正确响应。在该答案中,请考虑如果您在该示例中添加了针对失败的管理员身份验证的错误响应,那么有效的用户身份验证请求会发生什么:所有有效的用户身份验证请求都将失败并在管理路由层出现错误。

于 2013-11-07T03:38:25.190 回答
1

我刚刚发现以下不相关的页面解决了同样的问题:

http://compojureongae.posterous.com/using-the-app-engine-users-api-from-clojure

我没有意识到可以使用这种类型的语法(我还没有测试过):

(defroutes public-routes
  (GET "/public-endpoint" [] ("PUBLIC ENDPOINT")))

(defroutes user-routes
  (GET "/user-endpoint1" [] ("USER ENDPOINT 1"))
  (GET "/user-endpoint2" [] ("USER ENDPOINT 1")))

(defroutes admin-routes
  (GET "/admin-endpoint" [] ("ADMIN ENDPOINT")))

(def app
  (handler/api
    (routes
      public-routes
      (ANY "/user*" []
        (-> user-routes
            (wrap-basic-authentication user-auth?)))
      (ANY "/admin*" []
        (-> admin-routes
            (wrap-basic-authentication admin-auth?))))))
于 2012-05-30T18:37:11.400 回答
1

您是否考虑过使用Sandbar?它使用基于角色的授权,并允许您以声明方式指定访问特定资源所需的角色。查看 Sandbar 的文档以获取更多信息,但它可以像这样工作(注意对虚构的引用my-auth-function,这是您放置身份验证代码的地方):

(def security-policy
     [#"/admin-endpoint.*"          :admin 
      #"/user-endpoint.*"           :user
      #"/public-endpoint.*"         :any])

(defroutes my-routes
  (GET "/public-endpoint" [] ("PUBLIC ENDPOINT"))
  (GET "/user-endpoint1"  [] ("USER ENDPOINT1"))
  (GET "/user-endpoint2"  [] ("USER ENDPOINT2"))
  (GET "/admin-endpoint"  [] ("ADMIN ENDPOINT"))

(def app
  (-> my-routes
      (with-security security-policy my-auth-function)
      wrap-stateful-session
      handler/api))
于 2012-05-30T20:38:50.547 回答
0

我将改变您最终处理身份验证的方式,以拆分身份验证和过滤身份验证路由的过程。

而不仅仅是拥有管理员身份验证?和用户身份验证?返回布尔值或用户名,将其用作更多的“访问级别”键,您可以在更多每个路由级别上进行过滤,而无需为不同的路由“重新验证”。

(defn auth [user pass]
  (cond
    (admin-auth? user pass) :admin
    (user-auth? user pass) :user
    true :unauthenticated))

您还需要为此路径考虑现有基本身份验证中间件的替代方案。按照目前的设计,{:status 401}如果您不提供凭据,它将始终返回 a,因此您需要考虑到这一点并让它继续执行。

其结果被放入:basic-authentication请求映射中的键中,然后您可以在您想要的级别进行过滤。

想到的主要“过滤”案例是:

  • 在上下文级别(例如您在答案中的内容),除了您可以过滤掉没有所需:basic-authentication密钥的请求
  • 在每个路由级别上,您在本地检查其身份验证方式后返回 401 响应。请注意,这是区分 404 和 401 的唯一方法,除非您对各个路由进行上下文级别过滤。
  • 页面的不同视图取决于身份验证级别

要记住的最重要的事情是,除非请求的 url 需要身份验证,否则您必须继续为无效路由反馈 nil。您需要通过返回 401 来确保您没有过滤掉超出您想要的范围,这将导致 ring 停止尝试任何其他路由/句柄。

于 2012-05-31T03:20:59.450 回答