我目前正在使用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
并且不使用公共基本路径(理想情况下,没有)
答案 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)))
需要注意两点:身份验证中间件位于路由表单中,中间件调用匿名函数,这是一个真正的处理程序。为什么呢?
正如您所说,您需要在路由之后应用身份验证中间件,否则请求将永远不会路由到身份验证中间件!换句话说,路由需要位于 认证环的中间件环上。
如果您使用Compojure的路由表格(如GET),并且您正在表单的正文中应用中间件,那么中间件函数需要一个真正的响铃处理程序(即接受请求的函数)作为其参数并返回响应),而不是像字符串或响应映射那样简单。
这是因为,根据定义,像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
密钥的请求要记住的最重要的事情是,除非要求网址需要进行身份验证,否则必须继续为无效路由反馈nil。你需要通过返回401来确保你没有过滤掉超过你想要的东西,这将导致戒指停止尝试任何其他路线/手柄。