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小时