metabase session/login 分析

metabase 登录及session 管理最重要的两个CLJ文件是

  • session.clj
  • handler.clj

代码分析

先看handler.clj ,这个handler.clj是Ring 框架的web请求handler定义,关键代码如下,注释也很清晰

(def app
  "The primary entry point to the Ring HTTP server."
  (->
   ;; in production, dereference routes now because they will not change at runtime, so we don't need to waste time
   ;; dereferencing the var on every request. For dev & test, use the var instead so it can be tweaked without having
   ;; to restart the web server
   (if config/is-prod?
     routes/routes
     #'routes/routes)
   ;; ▼▼▼ POST-PROCESSING ▼▼▼ happens from TOP-TO-BOTTOM
   mw.exceptions/catch-uncaught-exceptions ; catch any Exceptions that weren't passed to `raise`
   mw.exceptions/catch-api-exceptions      ; catch exceptions and return them in our expected format
   mw.log/log-api-call
   mw.security/add-security-headers        ; Add HTTP headers to API responses to prevent them from being cached
   mw.json/wrap-json-body                  ; extracts json POST body and makes it avaliable on request
   mw.json/wrap-streamed-json-response     ; middleware to automatically serialize suitable objects as JSON in responses
   wrap-keyword-params                     ; converts string keys in :params to keyword keys
   wrap-params                             ; parses GET and POST params as :query-params/:form-params and both as :params
   mw.misc/maybe-set-site-url              ; set the value of `site-url` if it hasn't been set yet
   mw.session/bind-current-user            ; Binds *current-user* and *current-user-id* if :metabase-user-id is non-nil
   mw.session/wrap-current-user-info       ; looks for :metabase-session-id and sets :metabase-user-id and other info if Session ID is valid
   mw.session/wrap-session-id              ; looks for a Metabase Session ID and assoc as :metabase-session-id
   mw.auth/wrap-api-key                    ; looks for a Metabase API Key on the request and assocs as :metabase-api-key
   wrap-cookies                            ; Parses cookies in the request map and assocs as :cookies
   mw.misc/add-content-type                ; Adds a Content-Type header for any response that doesn't already have one
   mw.misc/disable-streaming-buffering     ; Add header to streaming (async) responses so ngnix doesn't buffer keepalive bytes
   wrap-gzip                               ; GZIP response if client can handle it
   mw.misc/bind-request                    ; bind `metabase.middleware.misc/*request*` for the duration of the request
   mw.ssl/redirect-to-https-middleware))   ; Redirect to HTTPS if configured to do so
;; ▲▲▲ PRE-PROCESSING ▲▲▲ happens from BOTTOM-TO-TOP

分析的时候,只要特别注意clojure处理是从下往上,session相关的是如下

   mw.session/bind-current-user            ; Binds *current-user* and *current-user-id* if :metabase-user-id is non-nil
   mw.session/wrap-current-user-info       ; looks for :metabase-session-id and sets :metabase-user-id and other info if Session ID is valid
   mw.session/wrap-session-id              ; looks for a Metabase Session ID and assoc as :metabase-session-id
   mw.auth/wrap-api-key                    ; looks for a Metabase API Key on the request and assocs as :metabase-api-key
   wrap-cookies   

依次是分析cookie(RING 提供的)、找到session id,绑定当前用户信息。
上述代码mw.session对应就是session.clj,即核心代码主要都在session.clj中。
session id获取代码如下:

(defn wrap-session-id
  "Middleware that sets the `:metabase-session-id` keyword on the request if a session id can be found.
   We first check the request :cookies for `metabase.SESSION`, then if no cookie is found we look in the http headers
  for `X-METABASE-SESSION`. If neither is found then then no keyword is bound to the request."
  [handler]
  (fn [request respond raise]
    (let [request (or (wrap-session-id-with-strategy :best request)
                      request)]
      (handler request respond raise))))

根据session获取用户id的代码如下

(defn- current-user-info-for-session
  "Return User ID and superuser status for Session with `session-id` if it is valid and not expired."
  [session-id anti-csrf-token]
  (when (and session-id (init-status/complete?))
    (let [sql    (session-with-id-query (mdb/db-type)
                                        (config/config-int :max-session-age)
                                        (if (seq anti-csrf-token) :full-app-embed :normal))
          params (concat [session-id]
                         (when (seq anti-csrf-token)
                           [anti-csrf-token]))]
      (first (jdbc/query (db/connection) (cons sql params))))))

session-with-id-query访问的模型为Session ,对应的表为core_session

(models/defmodel Session :core_session)

设置用户信息使用binding及^:dynamic 模式, 关于^:dynamic的用途,参考https://stackoverflow.com/questions/11730828/clojure-and-dynamic,说明如下:
^:dynamic is an instruction to the Clojure compiler that a symbol (as defined with def) is intended to be dynamically rebound (with binding).

关键binding代码如下

(defn do-with-current-user
  "Impl for `with-current-user`."
  [{
    
    :keys [metabase-user-id is-superuser? user-locale]} thunk]
  (binding [*current-user-id*              metabase-user-id
            i18n/*user-locale*             user-locale
            *is-superuser?*                (boolean is-superuser?)
            *current-user*                 (delay (find-user metabase-user-id))
            *current-user-permissions-set* (delay (some-> metabase-user-id user/permissions-set))]
    (thunk)))

当用户没有登录时候,这些相关id被设置为空,代码在comm.clj 中,如下:

;;; ----------------------------------------------- DYNAMIC VARIABLES ------------------------------------------------
;; These get bound by middleware for each HTTP request.

(def ^:dynamic ^Integer *current-user-id*
  "Int ID or `nil` of user associated with current API call."
  nil)

(def ^:dynamic *current-user*
  "Delay that returns the `User` (or nil) associated with the current API call.
   ex. `@*current-user*`"
  (atom nil)) ; default binding is just something that will return nil when dereferenced

(def ^:dynamic ^Boolean *is-superuser?*
  "Is the current user a superuser?"
  false)

(def ^:dynamic *current-user-permissions-set*
  "Delay to the set of permissions granted to the current user."
  (atom #{
    
    }))
登录后token的有效时长为48小时,这个是被hard code写死的,代码如下:

(def ^:private ^:const reset-token-ttl-ms
  "Number of milliseconds a password reset is considered valid."
  (* 48 60 60 1000)) ; token considered valid for 48 hours

总结:

  • login时候写cookies
  • 通过http header支持sso等外部认证
  • session信息保存在数据库中,每次访问都从数据库获取用户信息(metabase 并没有太过考虑高并发,因为这种场景存在可能性很低)
  • token 时间固定为48小时

猜你喜欢

转载自blog.csdn.net/weixin_40455124/article/details/113956212