clojure - Rutas de Compojure con diferentes middleware
routes ring (6)
¿Has considerado usar Sandbar ? Utiliza autorización basada en roles y le permite especificar declarativamente qué roles son necesarios para acceder a un recurso en particular. Consulte la documentación de Sandbar para obtener más información, pero podría funcionar de la siguiente manera (observe la referencia a una función my-auth-function
ficticia, ahí es donde pondría su código de autenticación):
(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))
Actualmente estoy escribiendo una API en Clojure usando Compojure (y Ring y middleware asociado).
Estoy tratando de aplicar un código de autenticación diferente según la ruta. Considera el siguiente código:
(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?)))))
Esto no funciona como se esperaba porque wrap-basic-authentication
hecho envuelve las rutas, por lo que se prueba independientemente de las rutas envueltas. Específicamente, si las solicitudes se deben enrutar a admin-routes
, user-auth?
todavía se probará (y fallará).
Recurrí a usar el context
para rootear algunas rutas bajo una ruta base común pero es una restricción (el siguiente código puede no funcionar simplemente para ilustrar la idea):
(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?))))))
Me pregunto si me falta algo o si hay alguna manera de lograr lo que quiero sin restricciones en mis defroutes
y sin utilizar una ruta base común (como idealmente, no habría ninguna).
Acabo de encontrar la siguiente página no relacionada que aborda el mismo problema:
http://compojureongae.posterous.com/using-the-app-engine-users-api-from-clojure
No me di cuenta de que es posible usar ese tipo de sintaxis (que aún no he probado):
(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?))))))
Cambiaría la forma en que terminas manejando la autenticación en general para dividir el proceso de autentificación y filtrado de rutas en la autenticación.
¿En lugar de solo tener la autorización de administrador? y user-auth? devuelve booleanos o un nombre de usuario, utilízalo como una clave de "nivel de acceso" que puedes filtrar en un nivel mucho más por ruta sin la necesidad de "volver a autenticarse" para diferentes rutas.
(defn auth [user pass]
(cond
(admin-auth? user pass) :admin
(user-auth? user pass) :user
true :unauthenticated))
También querrá considerar una alternativa al middleware de autenticación básica existente para esta ruta. Como está diseñado actualmente, siempre devolverá un {:status 401}
si no proporciona credenciales, por lo que tendrá que tener esto en cuenta y que continúe en su lugar.
El resultado de esto se pone en :basic-authentication
clave de :basic-authentication
en el mapa de solicitud, que luego puede filtrar al nivel que desee.
Los principales casos de "filtrado" que se me ocurren son:
- En un nivel de contexto (como lo que tienes en tu respuesta), excepto que puedes filtrar las solicitudes que no tienen la requerida
:basic-authentication
clave de:basic-authentication
- En un nivel por ruta, donde devuelve una respuesta 401 después de un control local sobre cómo se autentica. Tenga en cuenta que esta es la única forma de obtener una distinción entre 404 y 401 a menos que realice el filtrado de nivel de contexto en rutas individuales.
- Diferentes vistas para una página según el nivel de autenticación
Lo más importante que debe recordar es que debe continuar realizando una alimentación nula para las rutas no válidas, a menos que se solicite la autenticación de la URL. Debes asegurarte de no filtrar más de lo que deseas devolviendo un 401, lo que hará que el anillo deje de intentar con otras rutas / identificadores.
Esta es una pregunta razonable, que me resultó sorprendentemente difícil cuando me topé con ella.
Creo que lo que quieres es esto:
(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)))
Dos cosas a tener en cuenta: el middleware de autenticación está dentro del formulario de enrutamiento y el middleware llama a una función anónima que es un controlador genuino. ¿Por qué?
Como dijiste, necesitas aplicar el middleware de autenticación después del enrutamiento, ¡o la solicitud nunca se enrutará al middleware de autenticación! En otras palabras, el enrutamiento debe estar en un anillo de middleware fuera del anillo de autenticación.
Si utiliza formularios de enrutamiento de Compojure como GET y está aplicando middleware en el cuerpo del formulario, entonces la función de middleware necesita como argumento un controlador de respuesta de anillo genuino (es decir, una función que toma una solicitud y devuelve una respuesta), en lugar de algo más simple como una cadena o un mapa de respuesta.
Esto se debe a que, por definición, las funciones de middleware como wrap-basic-authentication solo toman los manejadores como argumentos, no cadenas desnudas o mapas de respuesta o cualquier otra cosa.
Entonces, ¿por qué es tan fácil perder esto? La razón es que los operadores de enrutamiento de Compojure como (GET [path args & body] ...) intentan facilitarte las cosas al ser muy flexible con la forma que tienes permitido pasar en el campo del cuerpo. Puede pasar una verdadera función de controlador, o simplemente una cadena, o un mapa de respuesta, o probablemente algo más que no se me haya ocurrido. Está todo dispuesto en el multi-método de render
en el interior de Compojure.
Esta flexibilidad disimula lo que realmente está haciendo el formulario GET, por lo que es fácil confundirse cuando intenta hacer algo un poco diferente.
En mi opinión, el problema con la respuesta principal de vedang no es una gran idea en la mayoría de los casos. En esencia, utiliza una maquinaria compuesta que responde a la pregunta "¿Coincide la ruta con la solicitud?" (de lo contrario, devuelva nil) para responder también a la pregunta "¿La solicitud pasa la autenticación?" Esto es problemático porque generalmente desea que las solicitudes que fallen la autenticación devuelvan las respuestas adecuadas con los códigos de estado 401, según las especificaciones HTTP. En esa respuesta, considere qué pasaría con las solicitudes autenticadas por el usuario válidas si agregara tal respuesta de error para la autenticación de administrador fallida a ese ejemplo: todas las solicitudes válidas autenticadas por el usuario fallarían y darían errores en la capa de enrutamiento del administrador.
Me encontré con este problema, y parece que wrap-routes
(compojure 1.3.2) resuelve elegantemente:
(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?)))))
(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)
Esto ejecutará la solicitud entrante primero a través de rutas de administrador y luego a través de rutas de usuario, aplicando la autenticación correcta en ambos casos. La idea principal aquí es que su función de autenticación debería ser nil
si la ruta no es accesible para la persona que llama en lugar de arrojar un error. De esta forma, las rutas de administrador devolverán nulo si a) la ruta en realidad no coincide con las rutas de administrador definidas o b) el usuario no tiene la autenticación requerida. Si admin-routes devuelve nil, los usuarios-routes serán juzgados por compojure.
Espero que esto ayude.
EDITAR: Hace algún tiempo escribí una publicación sobre Compojure, que podría serle útil: http://vedang.me/techlog/2012/02/23/composability-and-compojure