Metabase session/login analysis

The two most important CLJ files for metabase login and session management are

  • session.clj
  • handler.clj

Code analysis

First look at handler.clj. This handler.clj is the web request handler definition of the Ring framework. The key codes are as follows, and the comments are very clear.

(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

When analyzing, just pay special attention to clojure processing from bottom to top, session related is as follows

   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   

The sequence is to analyze the cookie (provided by RING), find the session id, and bind the current user information.
The above code mw.session corresponds to session.clj, that is, the core code is mainly in session.clj.
The code for obtaining session id is as follows:

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

The code to get the user id according to the session is as follows

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

The model accessed by session-with-id-query is Session, and the corresponding table is core_session

(models/defmodel Session :core_session)

Set user information to use binding and ^:dynamic mode. For the purpose of ^:dynamic, refer to https://stackoverflow.com/questions/11730828/clojure-and-dynamic, the description is as follows:
^:dynamic is an instruction to the Clojure compiler that a symbol (as defined with def) is intended to be dynamically rebound (with binding).

The key binding code is as follows

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

When the user is not logged in, these related ids are set to empty, and the code is in comm.clj, as follows:

;;; ----------------------------------------------- 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

to sum up:

  • Write cookies during login
  • Support external authentication such as sso through http header
  • The session information is stored in the database, and user information is obtained from the database for each visit (metabase does not consider high concurrency too much, because the possibility of such a scenario is very low)
  • The token time is fixed at 48 hours

Guess you like

Origin blog.csdn.net/weixin_40455124/article/details/113956212