Reitit: 访问控制

Reitit是Clojure/ClojureScript的Web开发框架。它的主要功能有:

  1. 后台路由及针对ClojureScript的前端路由
  2. 支持Ring
  3. 支持中间件(middleware),中间件使用Wrapper的方式修改处理请求的行为。参考middleware-registry
  4. 支持拦截器(interceptor)。拦截器与中间件实现的是相同的功能,不同的是拦截器使用:enter, :leave函数修改处理请求的行为。参考interceptors
  5. 请求/应答参数类型转换及验证,支持clojure.spec, spec-tools/data-specs, Plumatic Schemamalli。(其中data-specs是基于clojure.spec的实现。)
  6. 支持Swagger 及Swagger-UI(即Swagger的API Web调试页面)

本文主要介绍如何为Reitit添加登录验证。

假如我们有以下访问路径,

  • /web/* 即前端静态页面,不需要访问控制
  • /admin/* 除了其中的/admin/login外,其它均需要访问控制

Reitit的路由定义为:

(defn- handle-post-admin-login [{:keys [parameters session]}]
  (let [{:keys [username password]} (:body parameters)]
    (response
      (if (check-user {:username username :password password})
        (let [token (uuid)
              data  {:expires-at (+ (System/currentTimeMillis) (* 1000 1800))
                     :token      token
                     :username   username}]
          (swap! token-store assoc token data)
          {:code 0 :data data})
        {:code 2 :msg "Invalid username or password"}))))



(def admin-routes
  [""
   {:middleware [[wrap-access-rules {:rules access-rules :on-error on-error}]]
    :no-doc     true}
   ["/web/*"
    {:get {:handler (ring/create-file-handler {:root "./web/"})}}]
   ["/admin"
    [""
     {:get {:handler handle-get-admin}}]
    ["/users"
     {:get {:parameters {:query {:page       int?
                                 :today-only boolean?}}
            :handler    handle-get-admin-users}}]
    ["/login"
     {:post {:parameters {:body {:username string?
                                 :password string?}}
             :handler    handle-post-admin-login}}]
    ["/logout"
     {:post {:handler handle-post-admin-logout}}]]])

其中的middleware里使用了wrap-access-rules使用access-rules对请求进行验证。access-rules的相关定义如下:

;; 在(ns :require 里添加:)
[buddy.auth.accessrules                      :refer [wrap-access-rules]]
;;


;; 这里我们将token保存于内存中,用户量较大或需要持久化时可存储在Redis或数据库中:
(defonce token-store (atom {}))

;; token验证函数: 上传的token必须有效且没有过期
(defn check-identity [{:keys [headers]}]
  (let [{:keys [token expires-at]} (some-> headers (get "mytoken") (@token-store))]
    (->> (when expires-at
           (or (> expires-at (System/currentTimeMillis))
               (do
                 (swap! token-store dissoc token)
                 nil)))
         nil?
         not)))

(def any-access (constantly true))


;; 定义验证失败时的响应函数
(def on-error (constantly {:status 403 :body "Denied"}))

(def access-rules
  [{:uri     "/admin/login"
    :handler any-access}
   {:pattern #"^/admin.*"
    :handler check-identity}])

需要注意,

  • 对于没有在access-rules里匹配的URI,所有人均可以访问。上面的/web/*就符合这种情形。
  • 若需要对全局进行访问控制,可以在全局Middleware里使用access-rules,例如
(ring/ring-handler
    (ring/router
      [api-routes admin-routes]
      {:data      {
                   ;;
                   :middleware [
                                 ;;
                                 [wrap-access-rules {:rules access-rules :on-error on-error}]
                                 ;;
                               ]}})
    (ring/routes
      (swagger-ui/create-swagger-ui-handler
        {:path   "/swagger"
         :config {:validatorUrl nil}})
      (ring/create-default-handler)))