使用不同的中间件组合路由
Posted
技术标签:
【中文标题】使用不同的中间件组合路由【英文标题】:Compojure routes with different middleware 【发布时间】:2012-06-05 00:39:36 【问题描述】:我目前正在使用 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
并且不使用公共基本路径(理想情况下,不会有)。
【问题讨论】:
【参考方案1】:(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 将尝试 user-routes。
希望这会有所帮助。
编辑:前段时间我写了一篇关于 Compojure 的帖子,您可能会觉得它很有用:https://vedang.me/techlog/2012-02-23-composability-and-compojure/
【讨论】:
有没有办法将中间件仅应用于在 defroutes 下实际映射的特定 url,而不是将其应用于任何 "url/*" 的通用模式?我在中间件内部进行了数据库检查,我不希望任何随机 url 模式发生数据库调用。 感谢您的回答,这对我帮助很大! ^_^【参考方案2】:我偶然发现了这个问题,似乎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?)))))
【讨论】:
从哪里包含环绕路线? 来自 wrap-routes (weavejester.github.io/compojure/…) 的文档:“在路由匹配后将中间件功能应用于路由。”【参考方案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)))
需要注意两点:身份验证中间件位于路由表单内部,中间件调用一个匿名函数,该函数是一个真正的处理程序。为什么?
正如您所说,您需要在路由之后应用身份验证中间件,否则请求将永远无法路由到身份验证中间件!换句话说,路由需要位于身份验证环外部的中间件环上。
1234563并返回响应),而不是像字符串或响应映射这样更简单的东西。这是因为,根据定义,像 wrap-basic-authentication 这样的中间件函数仅将处理程序作为参数,而不是裸字符串或响应映射或其他任何东西。
那么为什么这么容易错过呢?原因是像 (GET [path args & body] ...) 这样的 Compojure 路由操作符通过对允许在 body 字段中传递的形式非常灵活,试图使事情变得容易。你可以传入一个真正的处理函数,或者只是一个字符串,或者一个响应映射,或者可能是我没有想到的其他东西。这一切都在 Compojure 内部的 render
多方法中列出。
这种灵活性掩盖了 GET 表单的实际作用,因此当您尝试做一些不同的事情时很容易混淆。
在我看来,在大多数情况下,vedang 的主要答案的问题并不是一个好主意。它本质上使用复合机器来回答“路由是否匹配请求?”这个问题。 (如果没有,则返回 nil)以回答“请求是否通过身份验证?”的问题。这是有问题的,因为根据 HTTP 规范,通常您希望身份验证失败的请求返回带有 401 状态代码的正确响应。在该答案中,请考虑如果您在该示例中添加了针对失败的管理员身份验证的错误响应,那么有效的用户身份验证请求会发生什么:所有有效的用户身份验证请求都将失败并在管理路由层出现错误。
【讨论】:
【参考方案4】:我刚刚发现以下不相关的页面解决了相同的问题:
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?))))))
【讨论】:
哦,哇,没想到我的一篇旧博文会出现在这个问题上! :-) 我很想知道它是否适合你。【参考方案5】:您是否考虑过使用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))
【讨论】:
非常有趣的方法。我真的很喜欢在安全策略中描述这些东西的想法;再一次,这都是关于命名事物的。我不认为我会在项目中拉出沙洲怪物,但一旦我实施了网站,我肯定会对其进行彻底的了解。我想我会实现自己的安全性(我认为这在我的水平上是一个很好的练习)。【参考方案6】:我会改变您最终处理身份验证的方式,将身份验证和过滤路由的过程分开。
而不仅仅是拥有管理员身份验证?和用户身份验证?返回布尔值或用户名,将其用作更多的“访问级别”键,您可以在更多的每个路由级别上进行过滤,而无需为不同的路由“重新验证”。
(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 的唯一方法,除非您对各个路由进行上下文级别过滤。
页面的不同视图取决于身份验证级别
要记住的最重要的事情是,除非请求的 url 需要身份验证,否则您必须继续为无效路由反馈 nil。您需要通过返回 401 来确保您没有过滤掉超出您想要的内容,这将导致 ring 停止尝试任何其他路由/句柄。
【讨论】:
以上是关于使用不同的中间件组合路由的主要内容,如果未能解决你的问题,请参考以下文章