目錄
- 背景
- 關于Superset
- 需要解決的問題
- 定制化改造
- 準備環境
- 改造OAuth SSO
- 安裝依賴
- 配置SSO
- 添加自定義的SecurityManager
- 運行一下吧
- 自定義宏命令
- 開啟配置
- 添加自定義宏命令
- 補充說明
- 小結
背景
在最近的一個專案上,客戶想要為他們的多租戶(Multi-tenant)系統添加一個新的報表中心,技術選型自然沿用之前的選擇:Apache Superset,一款由愛彼迎貢獻給開源社區的框架,
關于Superset
Superset的前端是中規中矩的React,圖表功能則是使用NVD3/D3,后端沒有使用萬年Java,而是Python3,Web方面使用的是Flask框架,其他的框架沒有過多的深入了解,
需要解決的問題
由于之前的業務原因,之前的系統在用戶登錄時,只能選擇其中的一個租戶系結到會話中,這個模式在業務早期沒有什么困擾,但隨著多租戶用戶的增多,系統的用戶更希望看到跨租戶的總覽資料,
為此,我們新增了一個資源服務,提供了一個介面用于查詢到當前用戶的租戶資訊,用戶的認證時通過OAuth 2.0,連接到鑒權服務,
這種變化對于原來的解決方案帶來了兩個問題:
- Superset需要接入資源服務所用的鑒權服務,并且在OAuth 2.0鑒權后訪問資源服務,通過介面獲取到當前用戶的租戶資訊,
- Superset需要在執行查詢時,動態插入行級過濾條件,這個過濾條件的值是依賴當前用戶的租戶資訊,這可以使用SQL Templating,SQL templating,內置了一些運算式(官方稱之為macro,宏,下文稱之為宏命令),但功能有限,
之前的做法是當通過OAuth登錄Superset,登錄的用戶名被改為租戶的ID,也就是一個租戶下的多個用戶在使用Superset時使用的是一個Superset用戶,這是一個安全的隱患,無法準確地追蹤用戶的行為,另外,因為Superset的Row level security只能系結到角色上,所以每個租戶用戶又有一個獨有的角色,這樣的影響是顯而易見的:但隨著業務的增長,租戶相關的資料會越來越多,一定程度上造成管理上的混亂,
定制化改造
針對問題1,在現在的Superset(1.3.2)中,早已提供了對OAuth登錄的支持,官方提供的教程也很詳細,但是在開發程序中,還是遇到了一些小問題,
針對問題2,想辦法改造這個SQL templating的文本處理邏輯,增加更多的宏命令,來獲取當前用戶的租戶資訊,對于這個功能,官方檔案只提供了一個針對Presto資料庫的文本處理改造方案,對于這部分功能改造的博客,網上的資訊很少,但是經過摸索,還是走出了一條路,
準備環境
官方提供兩種方案,一種容器化的,另一種是本地化加虛擬環境,為了除錯方便,我采用了后者,
Superset默認使用sqlite,本地啟動的話,sqlite檔案在~/.superset/superset.db,可以使用IDEA的database面板打開,資料庫schema請選擇main,
教程中提到的環境變數PYTHONPATH,可以理解為Java中的CLASS_PATH(是目錄,而不是具體的某個檔案),用于加載外部的模塊(module),因為Python是解釋型語言,所以可以在這個目錄直接放入Pythone檔案,Superset在啟動時會加載這個目錄下的superset_config.py,并根據其中的代碼,加載其他模塊,
改造OAuth SSO
請先閱讀官方教程:傳送門(英文),
安裝依賴
Superset接入OAuth SSO需要依賴庫Authlib,可以通過pip安裝,
pip install Authlib
對于采用容器化部署的小伙伴,要注意容器被重置時要安裝下載這個依賴,
對于喜歡多個命令列視窗的小伙伴,要注意安裝這個依賴時,要激活superset虛擬環境(virtualenv),
配置SSO
根據教程,我們會在superset_config.py中選擇認證方式為OAuth,并添加鑒權服務的配置,其中配置的詳細說明如下:
from flask_appbuilder.security.manager import AUTH_OAUTH
AUTH_TYPE = AUTH_OAUTH # 選擇認證方式,注意,這個值是參考自flask_appbuilder.security.manager
OAUTH_PROVIDERS = [
{
'name': 'spring-sso', # SSO的名字,用于展示在登錄頁面,格式為SIGN WITH {SSO的名字,大寫},可以配置多個SSO,
'token_key': 'access_token', # AccessToken在ResponseBody中的名字,必須指定,用于框架保存AccessToken,
'remote_app': {
'client_id': 'superset-client', # Superset在鑒權注冊的id
'client_secret': 'superset', # 配套的密鑰
'client_kwargs': {
'scope': 'openid' # OAuth2的scope,多個值用空格分開
},
'access_token_method': 'POST', # 請求access token介面時的HTTP方法
'access_token_params': {
# 請求access token介面附在URL上的引數,視鑒權服務的介面規范添加,可選配置,
'client_id': 'superset-client',
},
'access_token_headers': {
# 請求access token介面附在HEADER上的引數,視鑒權服務的介面規范添加,可選配置,
'Authorization': 'Basic Base64EncodedClientIdAndSecret'
},
'api_base_url': 'http://resource-server', # 資源服務API根路徑,用于獲取AccessToken后請求用戶資訊,
'authorize_url': 'http://auth-server/oauth2/authorize', # OAuth 2.0中的authorize介面
'access_token_url': 'http://auth-server/oauth2/token', # OAuth 2.0中的token介面
}
}
]
# 是否允許創建不存在的用戶,通過SSO登錄的用戶有可能沒有保存在Superset的用戶表中,如果這個配置項為False,那么用戶將被拒絕登錄,
AUTH_USER_REGISTRATION = True
# 創建時的默認權限,只允許一個值,
AUTH_USER_REGISTRATION_ROLE = "Admin"
DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
# 當配置項AUTH_ROLES_SYNC_AT_LOGIN為True時,每次SSO登錄后會將用戶資訊中的角色同步至Superset資料庫,
# 具體做法見下一節內容,
"AUTH_ROLES_SYNC_AT_LOGIN": False,
}
添加自定義的SecurityManager
Superset默認支持OAuth 2.0的登錄方式有GitHub、Twitter、LinkedIn、Google等,但如果鑒權服務是自建的話,就需要撰寫配套的SecurityManager,以便回傳給框架正確的用戶資訊,
在PYTHONPATH下添加一個新的檔案:custom_sso_security_manager.py,添加一個SecurityManager繼承類,覆寫oauth_user_info方法:
import jwt
from flask import session
from superset.security import SupersetSecurityManager
class CustomSsoSecurityManager(SupersetSecurityManager):
def oauth_user_info(self, provider, response=None):
if provider == 'spring-sso': # 判斷SSO的名字
access_token = response.get('access_token') # 從Response中獲取AccessToken
decoded = jwt.decode(access_token, verify=False) # 決議JWT
sub = decoded.get('sub') # 得到OpenId
# 向資源服務請求,通過oauth_remotes呼叫時,框架會自動在Authorization Header添加AccessToken,
# 這個AccessToken就是通過之前配置里的token_key決議得到的,
# 這里的路徑就是之前配置里的api_base_url,
# 理論上資源服務和鑒權服務是分開的,但大部分的SSO vendor提供的獲取用戶資訊介面與token介面的根路徑是一致的,
# 這里是根據業務的需要,向資源服務獲取當前用戶的租戶資訊,
user_details_resp = self.appbuilder.sm.oauth_remotes[provider].get('tenants')
# 將租戶資訊保存在session中,
session["tenants"] = user_details_resp.json()
# 拼接成用戶資訊,
# 用戶資訊中必須要有username或email,否則日志會拋出例外:OAUTH userinfo does not have username or email
# 用戶資訊可以添加role_keys串列,作為用戶的角色串列,
# 當配置項AUTH_ROLES_SYNC_AT_LOGIN為True時,每次SSO登錄后都將串列中的角色同步至Superset資料庫,
user_info = { 'username': sub, 'first_name': sub }
return user_info
然后再superset_config.py追加以下幾行:
from custom_sso_security_manager import CustomSsoSecurityManager
CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
運行一下吧
大功告成,可以試著運行一下,看看是否可以正常介面SSO,
自定義宏命令
開啟配置
為了根據用戶的租戶資訊對查詢的資料進行過濾,需要Superset的SQL Templating和Row level security兩個特性的配合,在superset_config.py中打開這兩個配置:
DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
# ...
"ENABLE_TEMPLATE_PROCESSING": True,
"ROW_LEVEL_SECURITY": True,
# ...
}
清先閱讀下官方檔案:SQL Templating和Row level security,
目前Superset的Row level security功能是比較完備的,可以在頁面上配置過濾的從句(Clause),而且過濾從句可以被SQL Templating處理,所以這里可以寫入宏命令,只是注意這里不需要寫上where關鍵字,因此Row level security無需進行任何改造,
但是對于官方提供的宏命令,還不足以支撐業務的需要(比如一個宏命令tenants(),從session中獲取當前用戶的租戶資訊),所以需要對其進行擴展,
添加自定義宏命令
Superset在jinja_context.py下實作了SQL Templating,對于SQL陳述句中的宏命令的替換處理,主要是通過JinjaTemplateProcessor來實作的,對于HQL的支持是通過HiveTemplateProcessor來實作的,后者在前者的基礎上添加了一些針對磁區(partition)的宏命令,
對于宏命令的擴展,可以參考Superset的教程,在superset_config.py中添加CUSTOM_TEMPLATE_PROCESSORS:
from custom_template_processor import CustomTemplateProcessor
from superset.jinja_context import BaseTemplateProcessor
from typing import Type, Dict
CUSTOM_TEMPLATE_PROCESSORS: Dict[str, Type[BaseTemplateProcessor]] = {
"sqlite": CustomTemplateProcessor
}
CUSTOM_TEMPLATE_PROCESSORS是一個Dict物件,可以理解為Java中的Map,鍵型別為str,代表著所負責的資料庫引擎型別,在我的本地環境中,資料庫使用的是sqlite,所以這里的寫的是sqlite,值型別是BaseTemplateProcessor的子類,這里我自定義了一個CustomTemplateProcessor,保存在同目錄的custom_template_processor.py中:
from functools import partial
from flask import session
from superset.jinja_context import JinjaTemplateProcessor, safe_proxy
from typing import Any
def tenants() -> (): return session["tenants"]
# 只需繼承JinjaTemplateProcessor即可,
class CustomTemplateProcessor(JinjaTemplateProcessor):
# 官方的檔案中給出的列子是將宏命令的識別由{{}}改為$,所以覆寫的是process_template,
# 現在的需要是添加新的宏命令,所以只需覆寫set_context方法即可,記得執行父類的方法!
def set_context(self, **kwargs: Any) -> None:
# 執行父類的方法,
super().set_context(**kwargs)
# 更新context
self._context.update(
{
# 鍵值是宏命令運算式
# 值一定要寫為partial(safe_proxy, func, args),否則父類在更新context會拋出安全例外
"tenants": partial(safe_proxy, tenants),
}
)
添加后,重啟服務,就可以去Row level security添加新增的宏命令了:
tenant IN ({{ "'" + "','".join(tenants()) + "'" }})
補充說明
任何TemplateProcessor都是單例模式,所以不要在這個類中保存與請求或執行緒相關的狀態,
目前租戶資訊是保存在服務session(記憶體)中,后期也可以優化為redis,或是持久化到Superset的資料庫,在每次登錄時更新下,
小結
本篇博客主要是指導如何使用Superset介入OAuth 2.0鑒權服務并從其下的資源服務獲取相關資訊,以及如何添加自定義的宏命令,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/418078.html
標籤:其他
