Routes Compojure avec différents middleware

J’écris actuellement une API dans Clojure en utilisant Compojure (et Ring et le middleware associé).

J’essaie d’appliquer un code d’authentification différent selon l’itinéraire. Considérez le code suivant:

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

Cela ne fonctionne pas comme prévu, car wrap-basic-authentication encapsule en effet les routes pour qu’elles soient essayées quelles que soient les routes encapsulées. Plus précisément, si les requêtes doivent être routées vers admin-routes , user-auth? sera toujours essayé (et échouer).

J’ai eu recours au context pour rooter certaines routes sous un chemin de base commun, mais c’est une contrainte (le code ci-dessous peut ne pas fonctionner, c’est simplement pour illustrer l’idée):

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

Je me demande si il me manque quelque chose ou s’il y a un moyen de réaliser ce que je veux sans contrainte sur mes defroutes et sans utiliser un chemin de base commun (comme, idéalement, il n’y en aurait pas).

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

Cela exécutera la demande entrante d’abord via les routes d’administration, puis via les itinéraires utilisateur, en appliquant l’authentification correcte dans les deux cas. L’idée principale ici est que votre fonction d’authentification devrait renvoyer nil si la route n’est pas accessible à l’appelant au lieu de générer une erreur. De cette façon, admin-routes retourne nil si a) la route ne correspond pas aux routes d’administration définies ou b) l’utilisateur ne dispose pas de l’authentification requirejse. Si admin-routes retourne nil, les routes utilisateurs seront essayées par compojure.

J’espère que cela t’aides.

EDIT: J’ai écrit un article sur Compojure il y a quelque temps, que vous pourriez trouver utile: http://vedang.me/techlog/2012/02/23/composability-and-compojure

Je suis tombé sur ce problème, et il semble que wrap-routes (compojure 1.3.2) résout avec élégance:

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

C’est une question raisonnable, que j’ai trouvée étonnamment délicate quand je l’ai rencontré moi-même.

Je pense que ce que vous voulez c’est ceci:

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

Deux choses à noter: le middleware d’authentification se trouve à l’intérieur du formulaire de routage et le middleware appelle une fonction anonyme qui est un véritable gestionnaire. Pourquoi?

  1. Comme vous l’avez dit, vous devez appliquer un middleware d’authentification après le routage ou la demande ne sera jamais acheminée vers le middleware d’authentification! En d’autres termes, le routage doit se faire sur un anneau de middleware en dehors de l’anneau d’authentification.

  2. Si vous utilisez les formulaires de routage de Compojure tels que GET et que vous appliquez un middleware dans le corps du formulaire, la fonction de middleware a pour argument un véritable gestionnaire de réponses en anneau (une fonction qui prend une requête et renvoie une réponse). plutôt que quelque chose de plus simple comme une chaîne ou une carte de réponse.

Cela est dû au fait que, par définition, les fonctions middleware telles que wrap-basic-authentication ne prennent que les gestionnaires comme des arguments, et non comme des chaînes nues, des cartes de réponses ou autre.

Alors, pourquoi est-ce si facile de manquer ça? La raison en est que les opérateurs de routage Compojure comme (GET [chemin args & body] …) essayent de vous faciliter la tâche en étant très flexible avec la forme que vous êtes autorisé à passer dans le champ corps. Vous pouvez transmettre une vraie fonction de gestionnaire, ou simplement une chaîne, ou une carte de réponse, ou probablement quelque chose qui ne m’est pas arrivé. Tout est prévu dans la multi-méthode de render dans les internes Compojure.

Cette flexibilité cache ce que fait le formulaire GET, il est donc facile de se mêler lorsque vous essayez de faire quelque chose de différent.

À mon avis, le problème de la principale réponse de vedang n’est pas une bonne idée dans la plupart des cas. Il utilise essentiellement des machines de compojure destinées à répondre à la question “Est-ce que l’itinéraire correspond à la demande?” (sinon, retourne nil) pour répondre également à la question “La demande passe-t-elle l’authentification?” Cela pose problème car vous voulez généralement que les requêtes qui échouent à l’authentification renvoient des réponses correctes avec des codes d’état 401, conformément à la spécification HTTP. Dans cette réponse, considérez ce qui arriverait aux requêtes authentifiées par l’utilisateur si vous ajoutiez une telle réponse d’erreur pour l’authentification d’administrateur ayant échoué à cet exemple: toute la requête authentifiée par l’utilisateur échouerait et donnerait des erreurs à la couche de routage admin.

Avez-vous envisagé d’utiliser Sandbar ? Il utilise une autorisation basée sur les rôles et vous permet de spécifier de manière déclarative quels rôles sont nécessaires pour accéder à une ressource particulière. Consultez la documentation de Sandbar pour plus d’informations, mais cela pourrait fonctionner comme ceci (notez la référence à une fonction fictive my-auth-function , c’est là que vous placeriez votre code d’authentification):

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

Je viens de trouver la page non reliée suivante qui traite du même problème:

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

Je n’ai pas réalisé qu’il est possible d’utiliser ce type de syntaxe (que je n’ai pas encore testé):

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

Je changerais la façon dont vous finissez par gérer l’authentification en général pour séparer le processus d’authentification et de filtrage des routes lors de l’authentification.

Plutôt que de simplement avoir l’authentification de l’administrateur? et utilisateur-auth? renvoyer des booléens ou un nom d’utilisateur, utilisez-le plus d’une clé de “niveau d’access” sur laquelle vous pouvez filtrer beaucoup plus au niveau d’une route sans avoir à “s’authentifier” pour des routes différentes.

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

Vous voudrez également envisager une alternative au middleware d’authentification de base existant pour ce chemin. Comme il est actuellement conçu, il renverra toujours un {:status 401} si vous ne fournissez pas d’informations d’identification. Vous devrez donc en tenir compte et le poursuivre à la place.

Le résultat de ceci est placé dans la clé :basic-authentication de la mappe de requête, que vous pouvez ensuite filtrer au niveau souhaité.

Les principaux cas de “filtrage” qui viennent à l’esprit sont:

  • Au niveau du contexte (comme ce que vous avez dans votre réponse), sauf que vous pouvez simplement filtrer les requêtes qui ne sont pas requirejses :basic-authentication clé d’ :basic-authentication
  • Au niveau de chaque route, où vous retournez une réponse 401 après une vérification locale de son authentification. Notez que c’est la seule façon d’obtenir une distinction entre 404 et 401, sauf si vous effectuez un filtrage au niveau du contexte sur des itinéraires individuels.
  • Différentes vues pour une page en fonction du niveau d’authentification

La chose la plus importante à retenir est que vous devez continuer à récupérer zéro pour les routes non valides à moins que l’URL demandée ne nécessite une authentification. Vous devez vous assurer que vous ne filtrez pas plus que vous ne le souhaitez en renvoyant un 401, ce qui fera que ring cessera d’essayer d’autres routes / poignées.