背景

在最近的一个项目上,客户想要为他们的多租户(Multi-tenant)系统添加一个新的报表中心。技术选型自然沿用之前的选择:Apache Superset,一款由爱彼迎贡献给开源社区的框架。

关于Superset

Superset的前端是中规中矩的React,图表功能则是使用NVD3/D3。后端没有使用万年Java,而是Python3。Web方面使用的是Flask框架,其他的框架没有过多的深入了解。

需要解决的问题

由于之前的业务原因,之前的系统在用户登录时,只能选择其中的一个租户绑定到会话中。这个模式在业务早期没有什么困扰,但随着多租户用户的增多,系统的用户更希望看到跨租户的总览数据。

为此,我们新增了一个资源服务,提供了一个接口用于查询到当前用户的租户信息。用户的认证时通过OAuth 2.0,连接到鉴权服务。

这种变化对于原来的解决方案带来了两个问题:

  1. Superset需要接入资源服务所用的鉴权服务,并且在OAuth 2.0鉴权后访问资源服务,通过接口获取到当前用户的租户信息。
  2. 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 TemplatingRow 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鉴权服务并从其下的资源服务获取相关信息,以及如何添加自定义的宏命令。

Superset SSO改造和自定义宏命令的更多相关文章

  1. cas sso原理(转)

    采用CAS原理构建单点登录 企业的信息化过程是一个循序渐进的过程,在企业各个业务网站逐步建设的过程中,根据各种业务信息水平的需要构建了相应的应用系统,由于这些应用系统一般是 在不同的时期开发完成的,各 ...

  2. 单点登录之CAS SSO从入门到精通(第三天)

    开场白 各位新年好,上海的新年好冷,冷到我手发抖. 做好准备全身心投入到新的学习和工作中去了吗?因为今天开始的教程很"变态"啊,我们要完成下面几件事: 自定义CAS SSO登录界面 ...

  3. Shrio第二天——认证、授权与其它特性

    一.认证——Authentication (即登陆),简单分析之前的HelloWorld的认证: 1. 获取当前的 Subject. 调用 SecurityUtils.getSubject(); 2. ...

  4. Dubbo学习系列之九(Shiro+JWT权限管理)

    村长让小王给村里各系统来一套SSO方案做整合,隔壁的陈家村流行使用Session+认证中心方法,但小王想尝试点新鲜的,于是想到了JWT方案,那JWT是啥呢?JavaWebToken简称JWT,就是一个 ...

  5. 采用CAS原理构建单点登录

    企业的信息化过程是一个循序渐进的过程,在企业各个业务网站逐步建设的过程中,根据各种业务信息水平的需要构建了相应的应用系统,由于这些应用系统一般是在不同的时期开发完成的,各应用系统由于功能侧重.设计方法 ...

  6. xcode调试打印QString

    xcode调试打印QString xcode内置GDB,在调试工程过程中可以通过print命令打印基本的数据类型,但像QString这样复杂类型就不行了.虽然我们可以在程序代码通过添加Qt的调试打印语 ...

  7. ubuntu16.04编译安装mysql-boost-5.7.21并编译成php扩展测试与使用

    我之前的文章已经改造了自定义MVC框架中的工具类(验证码,图片上传,图像处理,分类)4个类,接下来,就要改造模型类,模型类肯定要连接数据库,由于我的Ubuntu Linux是裸装的php(目前只编译了 ...

  8. Confluence 6 连接到 Jira 用户管理的限制

    当你在使用 JIRA 目录为用户目录的时候,请考虑下面的一些限制和建议. 不知道跨平台的多应用单点登录 当你使用 JIRA 为你的目录管理器的时候,系统将不能支持跨平台的单点登录.当 JIRA 用作目 ...

  9. GDB && QString

    [1]GDB && QString GDB的print命令仅能打印基本数据类型,而像QString这样的复杂类型就无能为力了! 如果调试时不能看QString的值,很让人抓狂!!!幸好 ...

随机推荐

  1. PowerShell配置文件后门

      PowerShell 配置文件是在 PowerShell 启动时运行的脚本.   在某些情况下,攻击者可以通过滥用PowerShell配置文件来获得持久性和提升特权.修改这些配置文件,以包括任意命 ...

  2. SpringBoot项目 maven打包时候提示程序包xxx不存在

    A模块依赖B模块 A打包的时候会报程序包xxx不存在 这时候我们看下B模块的pom.xml文件是否加了 <build> <plugins> <plugin> < ...

  3. IDEA中springboot项目添加yml格式配置文件

    1.先创建application.properties 文件,在resources文件夹,右键 new -> Resource Bundle  如下图所示,填写名称 2.生成如下图所示文件 3. ...

  4. html5调用摄像头截图

    关于html5调用音视频等多媒体硬件的API已经很成熟,不过一直找不到机会把这些硬件转化为实际的应用场景,不过近年来随着iot和AI的浪潮,我觉得软硬结合的时机已经成熟.那我们就提前熟悉下怎么操作这些 ...

  5. Lucene 基础数据压缩处理

    Lucene 为了使的信息的存储占用的空间更小,访问速度更快,采取了一些特殊的技巧,然 而在看 Lucene 文件格式的时候,这些技巧却容易使我们感到困惑,所以有必要把这些特殊 的技巧规则提取出来介绍 ...

  6. 面试造火箭系列,栽在了cglib和jdk动态代理

    "喂,你好,我是XX巴巴公司的技术面试官,请问你是张小帅吗".声音是从电话那头传来的 "是的,你好".小帅暗喜,大厂终于找上我了. "下面我们来进行一 ...

  7. 阿里云视觉智能开放平台的人脸1:N搜索的开源替代-Java版(文末赋开源地址)

    ​ 一.人脸检测相关概念 人脸检测(Face Detection)是检测出图像中人脸所在位置的一项技术,是人脸智能分析应用的核心组成部分,也是最基础的部分.人脸检测方法现在多种多样,常用的技术或工具大 ...

  8. Missing Data in Kernel PCA

    目录 引 主要内容 关于缺失数据的导数 附录 极大似然估计 代码 Sanguinetti G, Lawrence N D. Missing data in kernel PCA[J]. europea ...

  9. vue安装使用v-chart时报错解决方案

    npm i v-charts echarts -S 1.在main.js中使用报以下错 liquidFill echarts/lib/visual/dataColor 找不到 出现此原因是因为版本问题 ...

  10. [git]常用 Git 命令清单

    新建 创建一个新的 git 版本库.这个版本库的配置.存储等信息会被保存到.git 文件夹中 # 初始化当前项目 $ git init # 新建一个目录,将其初始化为Git代码库 $ git init ...