摘要
在企业业务系统中接入 Apache Superset,真正需要解决的通常不只是"如何展示一个 Dashboard",还包括认证、授权、数据隔离、单点登录、跨域、安全策略以及后续运维等问题。
实际项目中,Superset 集成通常存在两类场景:
- 普通业务用户查看报表:用户不离开业务系统,通过 Superset Embedded SDK 将 Dashboard 直接嵌入业务页面,并由业务后端签发短期 Guest Token。
- 管理员进入 Superset 控制台:管理员从业务系统跳转到 Superset,通过 OAuth2/SSO 建立 Superset 登录会话,再进行 Dashboard、Chart、Dataset 等配置。
这两个场景虽然都和 Superset 登录有关,但实现链路完全不同。本文将分别介绍两套方案,并重点说明 Guest Token 权限校验、RLS 行级数据隔离、Allowed Domains、CSP、Cookie、服务账号以及常见问题排查。
标签
Apache Superset Java Vue3 OAuth2 SSO Guest Token Embedded SDK RLS 数据可视化 多租户
1. 先说结论:两条链路不要混在一起
Superset 集成最容易出现的问题之一,就是把"普通用户看报表"和"管理员登录 Superset"当成同一个问题处理。
实际上,两者应该拆开。
1.1 普通用户查看报表
推荐链路:
text
业务用户
↓
业务前端
↓
业务后端
↓
Superset Guest Token API
↓
Embedded SDK
↓
Dashboard
特点:
- 用户只需要登录业务系统。
- 不需要创建 Superset 普通用户账号。
- 浏览器拿不到 Superset 管理员账号、密码或 Access Token。
- 可以在业务后端统一做菜单权限、租户权限、组织权限和数据权限控制。
1.2 管理员进入 Superset 控制台
推荐链路:
text
管理员
↓
业务系统
↓
Superset /login/{provider}
↓
业务平台 OAuth2
↓
Superset Session
↓
Superset 控制台
特点:
- 用于真正进入 Superset 后台。
- 适合 Dashboard 设计、Chart 编辑、Dataset 管理等场景。
- 通过 OAuth2/SSO 避免再次输入 Superset 用户名和密码。
因此可以简单理解为:
看报表用 Guest Token,进后台用 OAuth2/SSO。
2. 整体架构
#mermaid-svg-dL7xzeQoqwkx1yrT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dL7xzeQoqwkx1yrT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dL7xzeQoqwkx1yrT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dL7xzeQoqwkx1yrT .error-icon{fill:#552222;}#mermaid-svg-dL7xzeQoqwkx1yrT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dL7xzeQoqwkx1yrT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dL7xzeQoqwkx1yrT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dL7xzeQoqwkx1yrT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dL7xzeQoqwkx1yrT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dL7xzeQoqwkx1yrT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dL7xzeQoqwkx1yrT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dL7xzeQoqwkx1yrT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dL7xzeQoqwkx1yrT .marker.cross{stroke:#333333;}#mermaid-svg-dL7xzeQoqwkx1yrT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dL7xzeQoqwkx1yrT p{margin:0;}#mermaid-svg-dL7xzeQoqwkx1yrT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dL7xzeQoqwkx1yrT .cluster-label text{fill:#333;}#mermaid-svg-dL7xzeQoqwkx1yrT .cluster-label span{color:#333;}#mermaid-svg-dL7xzeQoqwkx1yrT .cluster-label span p{background-color:transparent;}#mermaid-svg-dL7xzeQoqwkx1yrT .label text,#mermaid-svg-dL7xzeQoqwkx1yrT span{fill:#333;color:#333;}#mermaid-svg-dL7xzeQoqwkx1yrT .node rect,#mermaid-svg-dL7xzeQoqwkx1yrT .node circle,#mermaid-svg-dL7xzeQoqwkx1yrT .node ellipse,#mermaid-svg-dL7xzeQoqwkx1yrT .node polygon,#mermaid-svg-dL7xzeQoqwkx1yrT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dL7xzeQoqwkx1yrT .rough-node .label text,#mermaid-svg-dL7xzeQoqwkx1yrT .node .label text,#mermaid-svg-dL7xzeQoqwkx1yrT .image-shape .label,#mermaid-svg-dL7xzeQoqwkx1yrT .icon-shape .label{text-anchor:middle;}#mermaid-svg-dL7xzeQoqwkx1yrT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dL7xzeQoqwkx1yrT .rough-node .label,#mermaid-svg-dL7xzeQoqwkx1yrT .node .label,#mermaid-svg-dL7xzeQoqwkx1yrT .image-shape .label,#mermaid-svg-dL7xzeQoqwkx1yrT .icon-shape .label{text-align:center;}#mermaid-svg-dL7xzeQoqwkx1yrT .node.clickable{cursor:pointer;}#mermaid-svg-dL7xzeQoqwkx1yrT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dL7xzeQoqwkx1yrT .arrowheadPath{fill:#333333;}#mermaid-svg-dL7xzeQoqwkx1yrT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dL7xzeQoqwkx1yrT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dL7xzeQoqwkx1yrT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dL7xzeQoqwkx1yrT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dL7xzeQoqwkx1yrT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dL7xzeQoqwkx1yrT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dL7xzeQoqwkx1yrT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dL7xzeQoqwkx1yrT .cluster text{fill:#333;}#mermaid-svg-dL7xzeQoqwkx1yrT .cluster span{color:#333;}#mermaid-svg-dL7xzeQoqwkx1yrT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dL7xzeQoqwkx1yrT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dL7xzeQoqwkx1yrT rect.text{fill:none;stroke-width:0;}#mermaid-svg-dL7xzeQoqwkx1yrT .icon-shape,#mermaid-svg-dL7xzeQoqwkx1yrT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dL7xzeQoqwkx1yrT .icon-shape p,#mermaid-svg-dL7xzeQoqwkx1yrT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dL7xzeQoqwkx1yrT .icon-shape .label rect,#mermaid-svg-dL7xzeQoqwkx1yrT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dL7xzeQoqwkx1yrT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dL7xzeQoqwkx1yrT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dL7xzeQoqwkx1yrT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 查看报表
登录/申请 Guest Token
Guest Token
Guest Token
Embedded SDK
进入控制台
业务用户
业务前端
管理员
业务后端
Superset
Superset Dashboard
Superset OAuth 登录入口
业务平台 OAuth2
整体职责划分建议如下:
| 模块 | 职责 |
|---|---|
| 业务前端 | 展示报表列表、初始化 Embedded SDK、跳转 Superset 控制台 |
| 业务后端 | 用户鉴权、报表权限校验、查询 Dashboard UUID、申请 Guest Token |
| Superset | Dashboard、Chart、Dataset、Guest Token、OAuth2 Provider |
| OAuth2 服务 | 给 Superset 提供统一身份认证 |
| 业务数据库 | 保存报表编码、Dashboard UUID、状态、分类、权限范围等配置 |
3. 普通用户嵌入 Dashboard
3.1 为什么不建议直接 iframe
最直接的方案可能是:
html
<iframe
src="https://superset.example.com/superset/dashboard/xxx/"
></iframe>
但单纯 iframe 很快会遇到登录问题。
因为 Superset 并不知道当前业务系统用户是谁。
如果依赖 Superset 自己的登录态,就意味着:
- 业务系统用户还要再登录一次 Superset;
- 需要维护第二套账号体系;
- 用户、角色和权限同步成本增加;
- iframe 中可能出现登录页;
- 跨域 Cookie 还会带来额外问题。
因此,普通业务用户查看报表时,更推荐:
text
Embedded SDK + Guest Token
核心思想是:
业务系统负责"这个用户能不能看",Superset 负责"把报表展示出来"。
4. Guest Token 完整流程
Superset 业务后端 业务前端 用户 Superset 业务后端 业务前端 用户 #mermaid-svg-1Q7tfm2e6sNHC1YR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1Q7tfm2e6sNHC1YR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1Q7tfm2e6sNHC1YR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1Q7tfm2e6sNHC1YR .error-icon{fill:#552222;}#mermaid-svg-1Q7tfm2e6sNHC1YR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1Q7tfm2e6sNHC1YR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1Q7tfm2e6sNHC1YR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1Q7tfm2e6sNHC1YR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1Q7tfm2e6sNHC1YR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1Q7tfm2e6sNHC1YR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1Q7tfm2e6sNHC1YR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1Q7tfm2e6sNHC1YR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1Q7tfm2e6sNHC1YR .marker.cross{stroke:#333333;}#mermaid-svg-1Q7tfm2e6sNHC1YR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1Q7tfm2e6sNHC1YR p{margin:0;}#mermaid-svg-1Q7tfm2e6sNHC1YR .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1Q7tfm2e6sNHC1YR text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-1Q7tfm2e6sNHC1YR .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1Q7tfm2e6sNHC1YR .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-1Q7tfm2e6sNHC1YR .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-1Q7tfm2e6sNHC1YR .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-1Q7tfm2e6sNHC1YR #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-1Q7tfm2e6sNHC1YR .sequenceNumber{fill:white;}#mermaid-svg-1Q7tfm2e6sNHC1YR #sequencenumber{fill:#333;}#mermaid-svg-1Q7tfm2e6sNHC1YR #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-1Q7tfm2e6sNHC1YR .messageText{fill:#333;stroke:none;}#mermaid-svg-1Q7tfm2e6sNHC1YR .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1Q7tfm2e6sNHC1YR .labelText,#mermaid-svg-1Q7tfm2e6sNHC1YR .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-1Q7tfm2e6sNHC1YR .loopText,#mermaid-svg-1Q7tfm2e6sNHC1YR .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-1Q7tfm2e6sNHC1YR .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1Q7tfm2e6sNHC1YR .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-1Q7tfm2e6sNHC1YR .noteText,#mermaid-svg-1Q7tfm2e6sNHC1YR .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-1Q7tfm2e6sNHC1YR .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1Q7tfm2e6sNHC1YR .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1Q7tfm2e6sNHC1YR .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1Q7tfm2e6sNHC1YR .actorPopupMenu{position:absolute;}#mermaid-svg-1Q7tfm2e6sNHC1YR .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-1Q7tfm2e6sNHC1YR .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1Q7tfm2e6sNHC1YR .actor-man circle,#mermaid-svg-1Q7tfm2e6sNHC1YR line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-1Q7tfm2e6sNHC1YR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 打开报表页面查询当前用户可访问报表返回 reportCode、名称等根据 reportCode 申请 Guest Token校验当前用户和报表权限根据 reportCode 查询 Dashboard UUIDPOST /api/v1/security/loginAccess TokenPOST /api/v1/security/guest_token/Guest TokenGuest TokenEmbedded SDK 加载 Dashboardiframe 展示 Dashboard
这里有一个非常重要的安全边界:
text
浏览器
↓
只能拿 Guest Token
业务后端
↓
持有 Superset 服务账号
↓
持有 Superset Access Token
不要把 Superset 服务账号、密码或 Access Token 返回给浏览器。
5. 前端集成 Embedded SDK
5.1 安装依赖
bash
npm install @superset-ui/embedded-sdk
实际版本需要与目标 Superset 版本联调确认,不建议直接照搬其他项目的版本号。
5.2 前端环境配置
env
VITE_SUPERSET_URL=https://superset.example.com
这个地址必须是:
用户浏览器能够直接访问的 Superset 外部地址。
例如:
text
https://bi.example.com
而不是只有服务器内部能够访问的:
text
http://superset:8088
5.3 Vue / TypeScript 示例
ts
import { embedDashboard } from '@superset-ui/embedded-sdk';
async function loadDashboard(
reportCode: string,
mountPoint: HTMLElement
) {
const report = await request.get(
'/api/reports/superset/report-info',
{
params: { reportCode }
}
);
await embedDashboard({
id: report.data.dashboardUuid,
supersetDomain: import.meta.env.VITE_SUPERSET_URL,
mountPoint,
fetchGuestToken: async () => {
const response = await request.get(
'/api/reports/superset/guest-token',
{
params: { reportCode }
}
);
return response.data.token;
},
dashboardUiConfig: {
hideTitle: true,
filters: {
visible: true,
expanded: false
}
}
});
}
这里建议前端始终围绕:
text
reportCode
与业务后端交互,而不是直接把任意 dashboardUuid 交给后端。
6. Dashboard UUID 不要搞错
Embedded SDK 中:
ts
id: dashboardUuid
这里需要使用的是:
Superset Dashboard 的 Embedded UUID。
不要和下面这些值混淆:
- 业务数据库中的报表主键;
- Dashboard 数字 ID;
- URL 中的其他参数;
- 自己生成的业务编码。
推荐业务系统内部维护一个映射关系:
text
reportCode
↓
Dashboard Embedded UUID
例如:
text
water_usage
↓
0f2cxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
这样业务代码不需要到处保存 Superset UUID。
7. 报表配置建议放到业务数据库
实际项目中不建议把所有 Dashboard UUID 写死在 Vue 页面中。
可以建立类似:
text
sys_superset_report
的报表配置表。
例如:
| 字段 | 说明 |
|---|---|
| report_code | 业务系统内部唯一报表编码 |
| superset_uuid | Dashboard Embedded UUID |
| category_code | 报表分类编码 |
| name_cn | 中文名称 |
| name_en | 英文名称 |
| sort_order | 排序 |
| status | 是否启用 |
| permission_code | 可选,业务权限标识 |
例如:
text
report_code = water_usage
superset_uuid = 0f2cxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
category_code = statistics
name_cn = 用水分析
sort_order = 10
status = 1
这样以后 Superset Dashboard 发生调整时,通常只需要修改配置,不需要修改前端源码。
8. 后端申请 Superset Access Token
业务后端首先需要使用专门的 Superset 服务账号调用登录接口。
http
POST /api/v1/security/login
Content-Type: application/json
请求示例:
json
{
"username": "${SUPERSET_SERVICE_USERNAME}",
"password": "${SUPERSET_SERVICE_PASSWORD}",
"provider": "db"
}
Superset 返回 Access Token 后,业务后端再调用 Guest Token API。
这里建议:
- 不使用日常管理员账号;
- 创建专门的 Superset 服务账号;
- 只给它 Guest Token 相关最小权限;
- 密码通过环境变量、配置中心或 Secret 管理。
例如:
yaml
superset:
base-url: ${SUPERSET_INTERNAL_URL:https://superset.example.com}
service-username: ${SUPERSET_SERVICE_USERNAME}
service-password: ${SUPERSET_SERVICE_PASSWORD}
connect-timeout: 5s
response-timeout: 15s
不要在 Git 中提交真实账号密码。
9. 申请 Guest Token
拿到 Access Token 后调用:
http
POST /api/v1/security/guest_token/
Authorization: Bearer {access_token}
Content-Type: application/json
请求示例:
json
{
"user": {
"username": "guest_user_001",
"first_name": "Guest",
"last_name": "User"
},
"resources": [
{
"type": "dashboard",
"id": "dashboard-embedded-uuid"
}
],
"rls": []
}
Superset 返回:
json
{
"token": "..."
}
业务后端最终只需要返回给浏览器:
json
{
"token": "superset-guest-token",
"expiresIn": 300
}
10. Guest Token 接口最重要的不是"能签发",而是"先鉴权"
很多 Superset 集成能正常运行,但安全边界并不完整。
例如后端暴露:
http
GET /api/reports/superset/guest-token?dashboardUuid=xxx
如果后端只是拿到 UUID 后直接申请 Guest Token,那么用户可以尝试手动修改:
text
dashboardUuid=A
为:
text
dashboardUuid=B
只要 B 是一个有效 Dashboard,就存在越权访问风险。
因此,更推荐设计成:
text
前端传 reportCode
↓
后端根据 reportCode 查询报表配置
↓
判断报表是否启用
↓
校验当前用户是否有权限
↓
校验租户 / 组织 / 区域范围
↓
取得 Dashboard UUID
↓
申请 Guest Token
后端至少应检查:
- 当前用户是否已登录;
- 当前用户是否拥有该报表菜单或权限;
- 报表配置是否存在;
- 报表是否启用;
- 当前租户是否允许访问;
- 当前组织、区域或项目是否允许访问;
- UUID 必须来自服务端报表配置,而不是完全信任前端。
一句话总结:
Dashboard UUID 是 Superset 资源标识,不应该直接当成业务权限凭证。
11. 多租户项目一定要考虑 RLS
如果 Guest Token 中:
json
"rls": []
表示当前方案只限制了:
这个用户可以访问哪个 Dashboard。
但并没有限制:
这个用户可以看到 Dashboard 中的哪些数据。
例如同一个 Dataset 中包含:
text
tenant-001
tenant-002
tenant-003
如果多个租户共用一个 Dashboard,而没有行级权限,那么不同租户之间可能看到不属于自己的数据。
因此,多租户场景建议使用 RLS。
示例:
json
{
"resources": [
{
"type": "dashboard",
"id": "dashboard-uuid"
}
],
"rls": [
{
"dataset": 42,
"clause": "tenant_id = 'tenant-001'"
}
],
"user": {
"username": "guest_user_001"
}
}
最终效果类似:
sql
SELECT *
FROM meter_data
WHERE tenant_id = 'tenant-001';
12. RLS 的值必须来自可信后端上下文
RLS 本身并不等于绝对安全。
如果后端这样写:
java
String tenantId = request.getParameter("tenantId");
String clause =
"tenant_id = '" + tenantId + "'";
那么用户仍然可以修改请求参数。
更加合理的流程应该是:
text
当前登录用户
↓
SecurityContext
↓
TenantContext / OrganizationContext
↓
获取可信 tenantId
↓
生成 RLS
也就是说:
租户、组织、区域、项目等数据权限值,应该来自后端认证上下文,而不是直接相信浏览器输入。
对于 RLS 条件,也建议:
- 做白名单校验;
- 避免直接拼接任意用户输入;
- 明确 Dataset ID;
- 检查 Dashboard 内所有 Chart 是否都使用了受控 Dataset。
13. 管理员进入 Superset 控制台:OAuth2/SSO
Guest Token 很适合看 Dashboard,但不适合真正进入 Superset 控制台。
如果管理员需要:
- 创建 Dashboard;
- 修改 Chart;
- 配置 Dataset;
- 进入 SQL Lab;
- 管理 Superset 内部对象;
就需要建立 Superset 正常登录 Session。
此时可以使用:
text
OAuth2 / SSO
14. OAuth2/SSO 登录流程
业务 OAuth2 服务 Superset 业务前端 管理员 业务 OAuth2 服务 Superset 业务前端 管理员 #mermaid-svg-ITLbTx64V7BfGbzq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ITLbTx64V7BfGbzq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ITLbTx64V7BfGbzq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ITLbTx64V7BfGbzq .error-icon{fill:#552222;}#mermaid-svg-ITLbTx64V7BfGbzq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ITLbTx64V7BfGbzq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ITLbTx64V7BfGbzq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ITLbTx64V7BfGbzq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ITLbTx64V7BfGbzq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ITLbTx64V7BfGbzq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ITLbTx64V7BfGbzq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ITLbTx64V7BfGbzq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ITLbTx64V7BfGbzq .marker.cross{stroke:#333333;}#mermaid-svg-ITLbTx64V7BfGbzq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ITLbTx64V7BfGbzq p{margin:0;}#mermaid-svg-ITLbTx64V7BfGbzq .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ITLbTx64V7BfGbzq text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ITLbTx64V7BfGbzq .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ITLbTx64V7BfGbzq .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ITLbTx64V7BfGbzq .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ITLbTx64V7BfGbzq .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ITLbTx64V7BfGbzq #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ITLbTx64V7BfGbzq .sequenceNumber{fill:white;}#mermaid-svg-ITLbTx64V7BfGbzq #sequencenumber{fill:#333;}#mermaid-svg-ITLbTx64V7BfGbzq #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ITLbTx64V7BfGbzq .messageText{fill:#333;stroke:none;}#mermaid-svg-ITLbTx64V7BfGbzq .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ITLbTx64V7BfGbzq .labelText,#mermaid-svg-ITLbTx64V7BfGbzq .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ITLbTx64V7BfGbzq .loopText,#mermaid-svg-ITLbTx64V7BfGbzq .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ITLbTx64V7BfGbzq .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ITLbTx64V7BfGbzq .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ITLbTx64V7BfGbzq .noteText,#mermaid-svg-ITLbTx64V7BfGbzq .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ITLbTx64V7BfGbzq .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ITLbTx64V7BfGbzq .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ITLbTx64V7BfGbzq .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ITLbTx64V7BfGbzq .actorPopupMenu{position:absolute;}#mermaid-svg-ITLbTx64V7BfGbzq .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ITLbTx64V7BfGbzq .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ITLbTx64V7BfGbzq .actor-man circle,#mermaid-svg-ITLbTx64V7BfGbzq line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ITLbTx64V7BfGbzq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 点击 Superset 控制台打开 /login/businessAuthorization Code 授权请求Authorization CodeCode 换 TokenAccess Token / UserInfo创建 Superset Session进入 Superset 控制台
最终用户体验:
text
登录业务系统
↓
点击"Superset 控制台"
↓
自动完成 OAuth2
↓
进入 Superset
15. Superset OAuth Provider
例如业务前端打开:
text
https://superset.example.com/login/business
这里的:
text
business
需要和 Superset 服务端配置的 OAuth Provider 名称对应。
也就是说:
text
/login/business
中的 business 不是随便写的。
Superset 侧通常需要配置:
- client_id;
- client_secret;
- authorization endpoint;
- token endpoint;
- user info endpoint;
- scope;
- 用户信息映射;
- Superset Role 映射。
OAuth2 服务端则需要:
- 创建 Superset OAuth2 Client;
- 配置允许的 redirect_uri;
- 使用 Authorization Code 流程;
- 校验 state;
- 保证授权码一次性使用。
具体参数需要以实际 Superset 版本和业务 OAuth2 实现为准。
16. 直接打开指定 Dashboard 编辑页面
当管理员已经拥有 Superset Session 后,可以直接打开:
ts
window.open(
`${SUPERSET_BASE_URL}/superset/dashboard/${uuid}/`,
'_blank'
);
如果 Superset Session 还存在,会直接进入 Dashboard 页面。
如果 Session 不存在,则应该先进入 Superset 登录链路,再通过 OAuth2/SSO 建立会话。
17. Superset Embedded 必要配置
17.1 开启 Embedded 功能
常见配置形式:
python
FEATURE_FLAGS = {
"EMBEDDED_SUPERSET": True
}
部分容器环境也可能通过对应环境变量开启。
具体配置项可能随 Superset 版本变化,应以实际部署版本为准。
17.2 Allowed Domains
Dashboard 开启 Embedded 后,需要设置允许嵌入它的业务域名。
例如:
text
https://business.example.com
生产环境不建议配置任意来源。
如果浏览器出现:
text
Refused to frame
或:
text
403
Allowed Domains 是第一批需要检查的配置之一。
18. Guest Role 和最小权限原则
嵌入场景建议使用专门的 Guest Role。
例如:
python
GUEST_ROLE_NAME = "EmbeddedGuest"
权限应该尽可能收敛,只允许:
- 查看需要的 Dashboard;
- 读取对应 Chart;
- 读取对应 Dataset;
- 访问必要 Database;
- 使用 Embedded 相关接口。
不要为了"先跑起来"直接给 Guest 或服务账号过大的权限。
19. 前端地址和后端地址可以不同
实际部署中,经常会存在:
text
浏览器访问地址:
https://bi.example.com
而业务后端访问:
text
http://superset:8088
这是完全合理的。
例如:
text
Browser
↓
https://bi.example.com
↓
Nginx
↓
Superset
业务后端则可能:
text
Business Backend
↓
http://superset:8088
因此配置时可以拆成:
前端:
env
VITE_SUPERSET_URL=https://bi.example.com
后端:
yaml
superset:
base-url: http://superset:8088
唯一需要保证的是:
两个地址最终访问的是同一个 Superset 实例,或者同一套 Superset 集群。
20. iframe、CORS、CSP 和 Cookie
Superset 嵌入出现白屏、403、Refused to frame 时,通常需要同时排查多个层面。
20.1 Allowed Domains
确认当前业务前端域名已经加入 Dashboard Embedded Allowed Domains。
20.2 Content-Security-Policy
重点检查:
text
Content-Security-Policy
尤其是:
text
frame-ancestors
20.3 X-Frame-Options
检查是否存在:
text
DENY
或:
text
SAMEORIGIN
20.4 Nginx / 网关
有时 Superset 配置本身没有问题,但 Nginx 或 API Gateway 又覆盖了一层响应头。
因此需要同时查看浏览器最终收到的 Header。
20.5 Cookie
如果某些跨站场景依赖 Superset Session Cookie,需要考虑:
python
SESSION_COOKIE_SAMESITE = "None"
SESSION_COOKIE_SECURE = True
SameSite=None 通常需要和 HTTPS 一起使用。
不建议为了临时解决 iframe 问题直接:
- 关闭全部 CSP;
- 关闭 CSRF;
- 允许所有 Origin;
- 放宽所有 Cookie 安全属性。
这样虽然可能暂时能打开页面,但会带来新的安全风险。
21. 推荐的项目目录划分
21.1 前端
text
frontend/
├── api/
│ └── superset.ts
├── views/
│ └── reports/
│ └── index.vue
├── components/
│ └── SupersetDashboard.vue
└── views/
└── sso/
└── SupersetSsoCallback.vue
21.2 后端
text
backend/
├── controller/
│ └── SupersetReportController
├── service/
│ └── SupersetGuestTokenService
├── entity/
│ └── ReportConfig
├── security/
│ └── ReportPermissionService
└── config/
└── SupersetProperties
职责划分:
| 模块 | 职责 |
|---|---|
| ReportConfig | 保存业务报表编码与 Superset UUID |
| ReportPermissionService | 校验用户、租户、组织、区域权限 |
| SupersetGuestTokenService | 登录 Superset 并申请 Guest Token |
| SupersetDashboard | 前端 Embedded SDK 封装 |
| SupersetSsoCallback | 管理员 OAuth2/SSO 登录处理 |
22. 推荐联调顺序
Superset 集成涉及的环节比较多。
如果所有问题一起排查,很容易把:
text
网络
Guest Token
权限
iframe
CORS
CSP
Cookie
OAuth2
混在一起。
推荐按下面顺序逐层验证。
第一步:浏览器能否访问 Superset
text
Browser
↓
Superset External URL
先确认基础网络没问题。
第二步:业务后端能否访问 Superset
text
Business Backend
↓
Superset Internal URL
第三步:服务账号登录
测试:
http
POST /api/v1/security/login
确认可以拿到 Access Token。
第四步:申请 Guest Token
测试:
http
POST /api/v1/security/guest_token/
确认接口正常。
第五步:检查 Dashboard
确认:
- Dashboard 已发布;
- 已开启 Embedded;
- Embedded UUID 正确;
- Allowed Domains 正确。
第六步:调前端 Embedded SDK
确认:
text
Embedded SDK
↓
Guest Token
↓
Dashboard
能正常加载。
第七步:验证权限
重点测试:
- 无权限用户是否能访问;
- 不同租户是否能看到对方数据;
- 不同组织/区域账号是否满足数据隔离;
- RLS 是否绑定正确 Dataset。
第八步:最后再调 OAuth2/SSO
管理员控制台登录放到最后。
这样排查起来会清晰很多。
23. 常见问题排查
23.1 Guest Token 返回 401 / 403
重点检查:
- Superset 服务账号用户名和密码;
/api/v1/security/login是否成功;- Access Token 是否正确;
Authorization: Bearer ...是否正确;- 服务账号是否有 Guest Token 签发权限;
- Embedded 功能是否开启。
23.2 Guest Token 已返回,但 iframe 白屏或 403
重点检查:
- 是否使用 Embedded Dashboard UUID;
- Dashboard 是否已经发布;
- 是否开启 Embedded;
- Allowed Domains 是否包含业务系统 Origin;
- 前端和后端是否指向同一个 Superset;
- 浏览器 Console 是否存在 CSP 错误;
- Network 中是否存在 401、403 或 Cookie 错误。
排查时建议直接打开:
text
F12
↓
Console
↓
Network
浏览器最终收到的错误通常比后端日志更直观。
23.3 Refused to frame
重点排查:
text
Content-Security-Policy
X-Frame-Options
frame-ancestors
Allowed Domains
另外不要忘记检查:
text
Superset
↓
Nginx
↓
Gateway
是否有某一层覆盖了响应头。
23.4 Superset 控制台不停跳登录页
重点检查:
- OAuth Provider 是否存在;
- Provider 名称是否和
/login/{provider}一致; - Client ID 是否一致;
- Client Secret 是否一致;
- redirect_uri 是否完全一致;
- redirect_uri 是否加入白名单;
- state 是否被正确保留和校验;
- Superset Session Cookie 是否被浏览器阻止。
23.5 用户可以看到其他租户的数据
这个问题通常不是 iframe 问题,而是:
数据权限问题。
检查:
- Guest Token 是否传了 RLS;
- RLS 是否绑定正确 Dataset ID;
- tenantId 是否来自可信认证上下文;
- Dashboard 中是否引用了其他未受限制的 Dataset;
- 某些 Chart 是否绕过了预期数据集。
24. 几个最容易踩的坑
24.1 Guest Token 和 OAuth2/SSO 不要混用
普通用户:
text
Embedded SDK + Guest Token
管理员:
text
OAuth2 / SSO
两者解决的是不同问题。
24.2 不要把 Superset 服务账号暴露到前端
错误:
text
Browser
↓
Superset username/password
↓
Superset
推荐:
text
Browser
↓
Business Backend
↓
Superset Service Account
↓
Superset
24.3 不要完全相信 dashboardUuid
推荐:
text
reportCode
↓
后端查配置
↓
校验权限
↓
取得 UUID
↓
Guest Token
24.4 Dashboard 权限不等于数据权限
text
允许访问 Dashboard
只解决:
text
能不能打开这个报表
并不等于:
text
能看到哪些数据
多租户系统还需要考虑 RLS。
24.5 不要在生产 YAML 中写真实密码
以下内容都不应该直接提交 Git:
- Superset 服务账号密码;
- OAuth2 Client Secret;
SUPERSET_SECRET_KEY;- Guest Token 相关密钥。
应统一放到:
- 环境变量;
- 配置中心;
- Kubernetes Secret;
- Vault;
- 企业密钥管理服务。
25. 最终推荐架构
text
┌──────────────┐
│ 业务用户 │
└──────┬───────┘
│
↓
┌───────────────┐
│ 业务前端 │
└───────┬───────┘
│
┌──────────────┴──────────────┐
│ │
↓ ↓
查看报表 管理报表
│ │
↓ ↓
reportCode /login/provider
│ │
↓ ↓
业务后端 Superset
│ │
用户/报表权限校验 OAuth2
│ │
租户/组织权限校验 ↓
│ 业务认证中心
查询 Dashboard UUID
│
生成 RLS
│
申请 Guest Token
│
↓
Embedded SDK
│
↓
Dashboard
这套方案的职责边界比较清晰。
对普通用户
只需要登录业务系统:
text
业务系统
↓
Dashboard
无需感知 Superset。
对管理员
可以:
text
业务系统
↓
OAuth2/SSO
↓
Superset 控制台
对安全体系
Superset 服务账号、密码和 Access Token 始终保留在服务端。
业务系统还可以继续叠加:
text
菜单权限
+
报表权限
+
租户权限
+
组织权限
+
区域权限
+
RLS 行级数据权限
26. 总结
企业业务系统集成 Superset,本质上不是简单的 iframe 集成,而是围绕以下几个问题设计:
text
身份认证
+
业务授权
+
Dashboard 授权
+
数据权限
+
单点登录
+
浏览器安全策略
对于普通业务用户,推荐:
text
Embedded SDK
+
Guest Token
对于管理员和报表设计人员,推荐:
text
OAuth2
+
SSO
同时,业务后端最好建立:
text
reportCode
↓
Dashboard UUID
映射,并在 Guest Token 签发前完成业务权限校验。
如果系统存在多租户、多组织、多区域的数据隔离要求,则进一步结合:
text
Guest Token RLS
限制 Dataset 行级数据范围。
最终形成:
业务系统负责用户、权限和业务上下文,Superset 专注于数据分析与可视化。
这类职责划分更清晰,也更适合中大型项目长期维护。
参考资料
- Apache Superset Embedded Dashboard 官方文档
- Apache Superset Guest Token API
- Apache Superset Networking / CORS 配置文档
- Apache Superset Security 配置文档
Superset 不同版本的配置项和接口细节可能存在变化,实际落地时建议以当前部署版本的官方文档为准。