以下是Superset的配置文件,请检查是否启用了Guest Token 功能:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
优化的Superset混合认证配置
"""
import os
import sys
import logging
import pymysql
import ssl
from urllib.parse import quote_plus
from flask_sqlalchemy import SQLAlchemy
# ============================================================================
# 基础配置
# ============================================================================
BASE_DIR = os.path.dirname(__file__)
sys.path.append(BASE_DIR)
# 环境变量
ENVIRONMENT = os.getenv("SUPERSET_ENV", "development") # production/development
IS_PRODUCTION = ENVIRONMENT == "production"
IS_DEVELOPMENT = ENVIRONMENT == "development"
# 版本和部署信息
SUPERSET_VERSION = os.getenv("SUPERSET_VERSION", "4.1.2")
DEPLOYMENT_ID = os.getenv("DEPLOYMENT_ID", "default")
print(f"🔧 加载Superset配置 - 环境: {ENVIRONMENT}")
# ============================================================================
# 数据库配置(优化版)
# ============================================================================
pymysql.install_as_MySQLdb()
db = SQLAlchemy()
DB_USER = os.getenv("SUPERSET_DB_USER", "superset")
DB_PASSWORD = os.getenv("SUPERSET_DB_PASSWORD", "SupersetPassword2025!")
DB_HOST = os.getenv("SUPERSET_DB_HOST", "10.18.6.120")
DB_PORT = int(os.getenv("SUPERSET_DB_PORT", "3306"))
DB_NAME = os.getenv("SUPERSET_DB_NAME", "superset")
SQLALCHEMY_DATABASE_URI = (
f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
"?charset=utf8mb4"
)
# 优化的数据库引擎配置
SQLALCHEMY_ENGINE_OPTIONS = {
"pool_pre_ping": True,
"pool_recycle": 3600,
"pool_timeout": 30,
"max_overflow": 20,
"pool_size": 10,
"echo": False,
"isolation_level": "READ_COMMITTED",
"connect_args": {
"connect_timeout": 10,
"read_timeout": 60,
"write_timeout": 60,
"charset": "utf8mb4",
# 移除了collation参数
"autocommit": False,
"sql_mode": "STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO",
}
}
# SQLAlchemy配置优化
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_RECORD_QUERIES = False
SQLALCHEMY_ECHO = False
# 数据库健康检查
DATABASE_HEALTH_CHECK_ENABLED = True
DATABASE_HEALTH_CHECK_INTERVAL = 30
# ============================================================================
# SQLLab专用配置(修复前端错误)
# ============================================================================
# SQLLab基础配置
ENABLE_SQLLAB = True
SQLLAB_BACKEND_PERSISTENCE = True
SQLLAB_TIMEOUT = 300
SQL_MAX_ROW = 10000
SQLLAB_DEFAULT_DBID = None
SQLLAB_CTAS_NO_LIMIT = True
SQLLAB_QUERY_COST_ESTIMATES_ENABLED = False # 关闭查询成本估算
SQLLAB_ASYNC_TIME_LIMIT_SEC = 600
# 防止SQLLab错误的配置
PREVENT_UNSAFE_DB_CONNECTIONS = False
SQLLAB_VALIDATION_TIMEOUT = 10
ENABLE_TEMPLATE_PROCESSING = True
# 查询结果配置
SUPERSET_WEBSERVER_TIMEOUT = 300
SUPERSET_WORKERS = 1 if IS_DEVELOPMENT else 4
# ============================================================================
# 安全配置 (多层防护)
# ============================================================================
SECRET_KEY = os.environ.get(
"SUPERSET_SECRET_KEY",
"FPwbFnYKL6wQTD0vtQfGBw7Y530FUfufsnHwXQTLlrrn8koVcctkMwiK",
)
# 安全检查
if len(SECRET_KEY) < 32:
raise ValueError("SECRET_KEY长度必须至少32位")
if IS_DEVELOPMENT:
logging.debug(f"SECRET_KEY已设置: {'*' * len(SECRET_KEY)}")
# 禁用安全警告弹框
SECURITY_WARNING_BANNER = False
# 或者设置为空字符串
SECURITY_WARNING_MESSAGE = ""
# ============================================================================
# Redis和异步查询检查
# ============================================================================
# 检查Redis可用性
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "minth@888") # Redis密码
REDIS_DB = int(os.getenv("REDIS_DB", "0"))
# URL编码密码以处理特殊字符
REDIS_PASSWORD_ENCODED = quote_plus(REDIS_PASSWORD) if REDIS_PASSWORD else ""
# 构建Redis连接URL
if REDIS_PASSWORD:
REDIS_URL = f"redis://:{REDIS_PASSWORD_ENCODED}@{REDIS_HOST}:{REDIS_PORT}"
else:
REDIS_URL = f"redis://{REDIS_HOST}:{REDIS_PORT}"
# 检查Redis可用性
REDIS_AVAILABLE = False
try:
import redis
# 创建Redis连接(带密码)
r = redis.Redis(
host=REDIS_HOST,
port=REDIS_PORT,
password=REDIS_PASSWORD if REDIS_PASSWORD else None,
socket_timeout=5,
socket_connect_timeout=5
)
# 测试连接
r.ping()
REDIS_AVAILABLE = True
print(f"✅ Redis连接成功: {REDIS_HOST}:{REDIS_PORT} (已认证)")
# 测试基本操作
test_key = "superset_test"
r.set(test_key, "test_value", ex=10) # 10秒过期
if r.get(test_key):
print("✅ Redis读写测试成功")
r.delete(test_key)
except Exception as e:
print(f"⚠️ Redis连接失败: {e}")
REDIS_AVAILABLE = False
# ============================================================================
# 特性标志
# ============================================================================
FEATURE_FLAGS = {
# SQLLab核心功能
"ENABLE_SQLLAB": True,
"SQLLAB_BACKEND_PERSISTENCE": True,
"ESTIMATE_QUERY_COST": False, # 关闭可能导致ROLLBACK的功能
"QUERY_COST_FORMATTERS_BY_ENGINE": {},
"RESULTS_BACKEND_USE_MSGPACK": True, # 🔧 强制启用msgpack
# 权限和安全
"DASHBOARD_RBAC": True,
"ENABLE_EXPLORE_JSON_CSRF_PROTECTION": True,
"ENABLE_TEMPLATE_PROCESSING": True,
"ROW_LEVEL_SECURITY": True, # 行级安全
# 嵌入功能
"EMBEDDED_SUPERSET": True,
"ALLOW_DASHBOARD_EMBEDDING": True,
"EMBEDDED_IN_FRAME": True,
# 过滤器和交互
"DASHBOARD_NATIVE_FILTERS": True,
"DASHBOARD_CROSS_FILTERS": True,
"ENABLE_FILTER_BOX_MIGRATION": True,
"DASHBOARD_FILTERS": True,
# 异步查询 禁用
"GLOBAL_ASYNC_QUERIES": False ,
"ASYNC_QUERIES": False,
"SCHEDULED_QUERIES": False,
# 高级功能 暂时不设置
"ENABLE_JAVASCRIPT_CONTROLS": True,
"DYNAMIC_PLUGINS": False,
"THUMBNAILS": REDIS_AVAILABLE,
"SCREENSHOTS": REDIS_AVAILABLE,
# 开发和调试(仅开发环境)
"ENABLE_REACT_CRUD_VIEWS": IS_DEVELOPMENT,
"ENABLE_BROAD_ACTIVITY_ACCESS": IS_DEVELOPMENT,
}
# ============================================================================
# 禁用异步查询配置·RESULTS_BACKEND
# ============================================================================
RESULTS_BACKEND = None
CELERY_CONFIG = None
# ============================================================================
# 结果处理修复配置
# ============================================================================
RESULTS_BACKEND_USE_MSGPACK = True
import json
class SupersetJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, set):
return list(obj)
if hasattr(obj, 'isoformat'):
return obj.isoformat()
if hasattr(obj, '__dict__'):
return obj.__dict__
return super().default(obj)
JSON_DEFAULT = SupersetJSONEncoder().encode
SQLLAB_RESULTS_BACKEND_PERSISTENCE = True
SQLLAB_TIMEOUT = 300
SUPERSET_SQLLAB_TIMEOUT = 300
print("🔧 结果处理配置已修复")
# ============================================================================
# LDAP认证配置(增强版)
# ============================================================================
from flask_appbuilder.security.manager import AUTH_LDAP
AUTH_TYPE = AUTH_LDAP
# LDAP服务器配置
AUTH_LDAP_SERVER = "ldap://10.18.2.50:389"
AUTH_LDAP_USE_TLS = False # 可根据服务器能力调整
# 用户名和搜索配置
AUTH_LDAP_USERNAME_FORMAT = "%(username)[email protected]"
AUTH_LDAP_SEARCH = "dc=minth,dc=intra"
AUTH_LDAP_SEARCH_FILTER = "(sAMAccountName={username})"
# 用户属性映射
AUTH_LDAP_UID_FIELD = "sAMAccountName"
AUTH_LDAP_FIRSTNAME_FIELD = "givenName"
AUTH_LDAP_LASTNAME_FIELD = "sn"
AUTH_LDAP_EMAIL_FIELD = "mail"
# LDAP行为配置
AUTH_LDAP_ALLOW_SELF_SIGNED = True
AUTH_LDAP_ALWAYS_SEARCH = True
# 用户管理
AUTH_USER_REGISTRATION = False
AUTH_USER_REGISTRATION_ROLE = "Public"
AUTH_ROLES_SYNC_AT_LOGIN = True
# 角色映射
AUTH_ROLES_MAPPING = {
"cn=SupersetAdmins,ou=Groups,dc=minth,dc=intra": ["Admin"],
"cn=DataAnalysts,ou=Groups,dc=minth,dc=intra": ["Alpha"],
"cn=DataViewers,ou=Groups,dc=minth,dc=intra": ["Gamma"],
"cn=ReportViewers,ou=Groups,dc=minth,dc=intra": ["Public"],
}
AUTH_LDAP_GROUP_FIELD = "memberOf"
# ============================================================================
# 混合安全管理器
# ============================================================================
try:
from security.hybrid_security_manager import HybridSecurityManager
CUSTOM_SECURITY_MANAGER = HybridSecurityManager
# 指定数据库认证用户(与HybridSecurityManager中的DB_AUTH_USERS对应)
DB_AUTH_USERS = ['admin','superset']
# LDAP连接池和缓存配置
# AUTH_LDAP_POOL_SIZE = 10
# AUTH_LDAP_POOL_RETRY_MAX = 3
# AUTH_LDAP_POOL_RETRY_DELAY = 30
# AUTH_LDAP_CACHE_ENABLED = True
# AUTH_LDAP_CACHE_TIMEOUT = 300
# 认证策略配置
AUTH_STRATEGY = {
'db_first_users': DB_AUTH_USERS,
'ldap_fallback_enabled': True,
'cache_enabled': REDIS_AVAILABLE, # 只有Redis可用时才启用缓存
'cache_timeout': 300,
'max_login_attempts': 5,
'lockout_duration': 900, # 15分钟
}
print("✅ 混合安全管理器已启用")
except ImportError as e:
print(f"⚠️ 混合安全管理器导入失败,使用标准LDAP认证: {e}")
CUSTOM_SECURITY_MANAGER = None
# ============================================================================
# Guest Token配置(安全优化版)
# ============================================================================
GUEST_TOKEN_JWT_SECRET = SECRET_KEY
GUEST_TOKEN_JWT_ALGO = "HS256"
GUEST_TOKEN_JWT_EXP_DELTA_SECONDS = 3600
GUEST_TOKEN_HEADER_NAME = "X-GuestToken"
# 强制设置AUD验证
SUPERSET_BASE_URL = os.getenv("SUPERSET_BASE_URL", None)
GUEST_TOKEN_JWT_AUD = SUPERSET_BASE_URL
# Guest Token增强配置
GUEST_TOKEN_AUTO_REFRESH = True
GUEST_TOKEN_REFRESH_THRESHOLD = 300 # 5分钟内过期自动刷新
GUEST_TOKEN_MAX_CONCURRENT = 100 # 最大并发Token数
# ============================================================================
# 缓存配置(修复版)
# ============================================================================
if REDIS_AVAILABLE:
# 生产环境Redis缓存
CACHE_CONFIG = {
'CACHE_TYPE': 'redis',
'CACHE_REDIS_HOST': REDIS_HOST,
'CACHE_REDIS_PORT': REDIS_PORT,
'CACHE_REDIS_DB': REDIS_DB, # 使用DB 0作为缓存
'CACHE_DEFAULT_TIMEOUT': 300,
'CACHE_KEY_PREFIX': f'superset_{DEPLOYMENT_ID}_',
}
# 如果有密码,添加密码配置
if REDIS_PASSWORD:
CACHE_CONFIG['CACHE_REDIS_PASSWORD'] = REDIS_PASSWORD
CACHE_CONFIG['CACHE_REDIS_URL'] = f"{REDIS_URL}/{REDIS_DB}"
print(f"✅ Redis缓存配置: {REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}")
else:
# 开发环境或Redis不可用时使用简单缓存
CACHE_CONFIG = {
'CACHE_TYPE': 'simple',
'CACHE_DEFAULT_TIMEOUT': 60,
}
print("ℹ️ 使用简单缓存")
# ============================================================================
# 安全头配置(生产环境优化)
# ============================================================================
# 消除CSP警告
CONTENT_SECURITY_POLICY_WARNING = False
if IS_PRODUCTION:
# 生产环境:最严格安全配置
TRUSTED_DOMAINS = [d.strip() for d in os.getenv("TRUSTED_DOMAINS", "").split(",") if d.strip()]
HTTP_HEADERS = {
"X-Frame-Options": "SAMEORIGIN", # 更安全的默认值
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "geolocation=(), microphone=(), camera=()",
}
# 如果有信任域名,则允许特定域名
if TRUSTED_DOMAINS:
HTTP_HEADERS["X-Frame-Options"] = f"ALLOW-FROM {TRUSTED_DOMAINS[0]}"
TALISMAN_ENABLED = True
TALISMAN_CONFIG = {
'content_security_policy': {
'default-src': "'self'",
'script-src': "'self' 'unsafe-inline' 'unsafe-eval'",
'style-src': "'self' 'unsafe-inline'",
'img-src': "'self' data: blob: https:",
'font-src': "'self' data:",
'frame-ancestors': TRUSTED_DOMAINS or ["'self'"],
'connect-src': "'self'",
},
'force_https': os.getenv("FORCE_HTTPS", "false").lower() == "true",
'strict_transport_security': True,
'strict_transport_security_max_age': 31536000,
}
# CORS严格配置
CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = TRUSTED_DOMAINS
ENABLE_CORS = len(TRUSTED_DOMAINS) > 0
else:
# 开发环境:宽松配置
HTTP_HEADERS = {
"X-Frame-Options": "",
"X-XSS-Protection": "1; mode=block",
}
TALISMAN_ENABLED = False
CORS_ORIGIN_ALLOW_ALL = True
ENABLE_CORS = True
# ============================================================================
# 日志配置(生产级)
# ============================================================================
LOG_DIR = os.getenv("SUPERSET_LOG_DIR", "/app/superset/logs")
os.makedirs(LOG_DIR, exist_ok=True)
LOG_LEVEL = "INFO" if IS_PRODUCTION else "DEBUG"
LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(funcName)s() - %(message)s"
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"detailed": {
"format": LOG_FORMAT,
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"simple": {
"format": "%(levelname)s - %(message)s",
},
"json": {
"format": "%(asctime)s %(name)s %(levelname)s %(message)s",
},
},
"handlers": {
"console": {
"level": LOG_LEVEL,
"class": "logging.StreamHandler",
"formatter": "simple" if IS_DEVELOPMENT else "json",
},
"app_file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"filename": f"{LOG_DIR}/superset.log",
"maxBytes": 10485760, # 20MB
"backupCount": 5,
"formatter": "detailed",
},
"auth_file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"filename": f"{LOG_DIR}/auth.log",
"maxBytes": 10485760, # 10MB
"backupCount": 5,
"formatter": "detailed",
},
"error_file": {
"level": "ERROR",
"class": "logging.handlers.RotatingFileHandler",
"filename": f"{LOG_DIR}/error.log",
"maxBytes": 10485760,
"backupCount": 10,
"formatter": "detailed",
},
# "security_file": {
# "level": "WARNING",
# "class": "logging.handlers.RotatingFileHandler",
# "filename": f"{LOG_DIR}/security.log",
# "maxBytes": 10485760,
# "backupCount": 10,
# "formatter": "detailed",
# },
},
"loggers": {
"superset": {
"level": LOG_LEVEL,
"handlers": ["app_file", "console"],
"propagate": False,
},
"flask_appbuilder.security": {
"level": "INFO",
"handlers": ["auth_file", "console"],
"propagate": False,
},
"security_manager": {
"level": LOG_LEVEL,
"handlers": ["auth_file", "console"],
"propagate": False,
},
"werkzeug": {
"level": "WARNING",
"handlers": ["console"],
"propagate": False,
},
},
"root": {
"level": LOG_LEVEL,
"handlers": ["console"],
},
}
# ============================================================================
# 业务配置
# ============================================================================
# 角色设置
GUEST_ROLE_NAME = "Public"
PUBLIC_ROLE_LIKE = "Gamma"
PUBLIC_ROLE_LIKE_GAMMA = True
# 国际化
BABEL_DEFAULT_LOCALE = "zh"
BABEL_DEFAULT_FOLDER = "babel/translations"
LANGUAGES = {
"en": {"flag": "us", "name": "English"},
"zh": {"flag": "cn", "name": "Chinese"},
}
# 系统设置
SUPERSET_DEFAULT_TIMEZONE = "Asia/Shanghai"
SUPERSET_WEBSERVER_TIMEOUT = 120 if IS_PRODUCTION else 60
# 基础功能启用
ENABLE_CSRF = True
ENABLE_SWAGGER = not IS_PRODUCTION # 生产环境关闭Swagger
ENABLE_GUEST_TOKEN = True
EMBEDDED_SUPERSET = True
ALLOW_DASHBOARD_EMBEDDING = True
# ============================================================================
# 环境特定配置
# ============================================================================
if IS_PRODUCTION:
# 生产环境优化
WTF_CSRF_TIME_LIMIT = 7200
SEND_FILE_MAX_AGE_DEFAULT = 31536000
# 数据源连接超时
SQL_MAX_ROW = 100000
SQLLAB_TIMEOUT = 300
SUPERSET_WORKERS = int(os.getenv("SUPERSET_WORKERS", "4"))
else:
# 开发环境配置
WTF_CSRF_TIME_LIMIT = None
SQL_MAX_ROW = 10000
SQLLAB_TIMEOUT = 60
# ============================================================================
# 启动验证
# ============================================================================
def validate_config():
"""配置验证"""
errors = []
# 必需的环境变量检查
required_env_vars = ["SUPERSET_SECRET_KEY"]
if IS_PRODUCTION:
required_env_vars.extend(["SUPERSET_BASE_URL", "TRUSTED_DOMAINS"])
for var in required_env_vars:
if not os.getenv(var):
errors.append(f"缺少必需的环境变量: {var}")
# 安全配置检查
if IS_PRODUCTION and CORS_ORIGIN_ALLOW_ALL:
errors.append("生产环境不应允许所有来源的CORS请求")
if errors:
raise ValueError(f"配置验证失败:\n" + "\n".join(f" - {error}" for error in errors))
# 执行配置验证
try:
validate_config()
print(f"🚀 Superset配置加载完成 - 环境: {ENVIRONMENT}")
print(f" 数据库: {DB_HOST}:{DB_PORT}/{DB_NAME}")
print(f" 认证: LDAP混合模式")
print(f" 缓存: {'Redis' if IS_PRODUCTION else 'Simple'}")
print(f" 日志级别: {LOG_LEVEL}")
except ValueError as e:
print(f"❌ 配置验证失败: {e}")
if IS_PRODUCTION:
raise # 生产环境严格要求
else:
print("⚠️ 开发环境忽略配置错误")