具有不同中间件的Compojure路由

时间:2012-05-30 18:20:58

标签: clojure routes middleware compojure ring

我目前正在使用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 root 一些路线 路径,但它是一个相当大的约束(下面的代码可能不起作用,它只是为了说明这个想法):

(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并且不使用公共基本路径(理想情况下,没有)

6 个答案:

答案 0 :(得分:20)

(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)

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

希望这有帮助。

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

答案 1 :(得分:16)

我偶然发现了这个问题,似乎wrap-routes(组件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?)))))

答案 2 :(得分:3)

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

我认为你想要的是:

(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),并且您正在表单的正文中应用中间件,那么中间件函数需要一个真正的响铃处理程序(即接受请求的函数)作为其参数并返回响应),而不是像字符串或响应映射那样简单。

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

    那为什么这么容易错过呢?原因是Compojure路由操作符(GET [path args& body] ...)试图通过非常灵活地允许您在体域中传递的形式来使事情变得容易。您可以传入一个真正的处理函数,或者只传递一个字符串,一个响应映射,或者可能传递给我的其他东西。这些都是在Compojure内部的render多方法中进行的。

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

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

答案 3 :(得分: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?))))))

答案 4 :(得分: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))

答案 5 :(得分:0)

我会改变你最终处理身份验证的方式,以便在身份验证过程中拆分身份验证和过滤路由的过程。

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

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

您还需要考虑此路径的现有基本身份验证中间件的替代方案。正如它目前设计的那样,如果您不提供凭据,它将始终返回{:status 401},因此您需要将此考虑在内并让其继续使用。

将结果放在请求图中的:basic-authentication键中,然后您可以在所需的级别进行过滤。

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

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

要记住的最重要的事情是,除非要求网址需要进行身份验证,否则必须继续为无效路由反馈nil。你需要通过返回401来确保你没有过滤掉超过你想要的东西,这将导致戒指停止尝试任何其他路线/手柄。