You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Superset与Keycloak集成后用户验证通过却触发500错误(未知主机问题)

Superset与Keycloak集成后用户验证通过却触发500错误(未知主机问题)

看起来你已经把Superset和Keycloak的OIDC集成基础配置都捋顺了:Keycloak登录界面能正常弹出、用户身份验证有效、角色映射也匹配,而且Keycloak客户端里的Superset URL配置也没问题。但用户登录后直接跳转到500错误页,从日志和网络请求来看,问题出在Superset回调环节无法访问Keycloak的Token端点,核心报错是(504, b'Unknown Host')。

错误根源分析

从你提供的Docker日志Traceback可以看到,错误发生在flask_oidc处理回调时的flow.step2_exchange(code)步骤——这一步是Superset拿着Keycloak返回的授权码,去请求Keycloak的Token端点换取访问令牌,但Superset容器无法解析Keycloak的主机地址,导致请求失败触发500错误。

排查与解决方案

针对这个“未知主机”问题,你可以按以下步骤排查:

1. 验证Superset容器的DNS解析能力

因为是Docker环境,Superset容器可能无法解析Keycloak的域名/主机名。你可以进入Superset容器,尝试ping Keycloak的地址:

# 进入Superset容器(替换容器名或ID)
docker exec -it superset_app bash
# ping Keycloak的主机地址(比如你的Keycloak域名/IP)
ping your-keycloak-hostname

如果ping不通,说明容器的DNS配置有问题,或者Keycloak的主机地址在容器网络中不可达。你可以:

  • 如果Keycloak和Superset在同一Docker网络,改用Keycloak容器的服务名(比如keycloak)作为地址;
  • 如果Keycloak在外部网络,确保Superset容器所在的网络能访问Keycloak的IP,并且DNS服务器能解析Keycloak的域名。

2. 检查client_secret.json中的端点地址

你的OIDC_CLIENT_SECRETS指向的client_secret.json文件里,issuer、token_endpoint、authorization_endpoint等地址必须是Superset容器能访问的地址。比如:

  • 不要用http://localhost:8080/auth/...(容器内的localhost是容器自身,不是宿主机);
  • 改用宿主机的局域网IP,或者Docker内部服务名(如果Keycloak也是容器)。

3. 检查网络与代理配置

  • 如果Superset需要通过代理访问外部网络,要确保httplib2(flask_oidc依赖的HTTP库)的代理配置正确。你可以在superset_config.py里添加代理相关配置:
import os
os.environ['HTTP_PROXY'] = 'http://your-proxy:port'
os.environ['HTTPS_PROXY'] = 'http://your-proxy:port'
  • 检查防火墙或安全组设置,确保Superset容器能访问Keycloak的8080(或自定义)端口。

4. 验证角色解析逻辑(可选)

虽然当前错误是主机解析问题,但如果后续解决后还有角色相关问题,你可以检查security.py中的findAccessRoles函数:确保AFIP_SERVICE_ID和Keycloak客户端的客户端ID完全匹配,否则会找不到resource_access里的角色。

你提供的配置与日志参考

Superset配置(superset_config.py)

from security import OIDCSecurityManager
from flask_appbuilder.security.manager import AUTH_OID

logger.info('@keycloak config ..')

AUTH_TYPE = AUTH_OID

curr = os.path.abspath(os.getcwd())
OIDC_CLIENT_SECRETS = curr + '/docker/pythonpath_dev/client_secret.json'
OIDC_ID_TOKEN_COOKIE_SECURE = False
OIDC_REQUIRE_VERIFIED_EMAIL = False
OIDC_ID_TOKEN_COOKIE_NAME = 'pergamino'
AUTH_USER_REGISTRATION = True
#AUTH_USER_REGISTRATION_ROLE = 'Gamma'
AUTH_USER_REGISTRATION_ROLE = 'Public'
CUSTOM_SECURITY_MANAGER = OIDCSecurityManager

自定义Security Manager(security.py)

from flask import redirect, request, abort
from flask_appbuilder.security.manager import AUTH_OID
from superset.security import SupersetSecurityManager
from flask_oidc import OpenIDConnect
from flask_appbuilder.security.views import AuthOIDView
from flask_login import login_user
from urllib.parse import quote
from flask_appbuilder.views import ModelView, SimpleFormView, expose
import logging
import json
from base64 import b64decode
import os

logger = logging.getLogger()

AFIP_SERVICE_ID = os.environ.get("AFIP_SERVICE_ID", "pergamino")

class AuthOIDCView(AuthOIDView):
    @expose('/login/', methods=['GET', 'POST'])
    def login(self, flag=True):
        sm = self.appbuilder.sm
        oidc = sm.oid

        @self.appbuilder.sm.oid.require_login
        def handle_login():
            if not oidc.get_access_token():
                logger.warn('Not found access_token')
                return self.logout()

            roles = findAccessRoles(sm)
            logger.debug('findAccessRoles %s' % roles)
            if not roles or len(roles) == 0:
                logger.warn('Not found roles')
                return self.logout()

            info = oidc.user_getinfo(
                ['preferred_username', 'given_name', 'family_name', 'email', 'sub'])
            email = oidc.user_getfield('email')
            if email is None:
                logger.debug('email no informado, se intenta crear un valor')
                username = oidc.user_getfield('username') or info.get('preferred_username')
                email = username + "@afip.gob.ar"

            user = sm.auth_user_oid(email)
            if user is None:
                logger.debug('crear usuario %s' % email)
                user = sm.add_user(info.get('preferred_username'), info.get(
                    'given_name'), info.get('family_name'), email, roles[0])
                if len(roles) > 1:
                    user.roles = roles
                    sm.update_user(user)
            elif len(roles) != len(user.roles) or not all(e in roles for e in user.roles):
                logger.debug('actualizar usuario %s' % email)
                user.roles = roles
                sm.update_user(user)

            logger.debug('user.roles %s' % user.roles)
            login_user(user, remember=False)
            return redirect(self.appbuilder.get_url_for_index)

        return handle_login()

    @expose('/logout/', methods=['GET', 'POST'])
    def logout(self):
        oidc = self.appbuilder.sm.oid
        oidc.logout()
        super(AuthOIDCView, self).logout()
        redirect_url = request.url_root.strip(
            '/') + self.appbuilder.get_url_for_login
        return redirect(oidc.client_secrets.get('issuer') + '/protocol/openid-connect/logout?redirect_uri=' + quote(redirect_url))

class OIDCSecurityManager(SupersetSecurityManager):
    authoidview = AuthOIDCView

    def __init__(self, appbuilder):
        super(OIDCSecurityManager, self).__init__(appbuilder)
        if self.auth_type == AUTH_OID:
            self.oid = OpenIDConnect(self.appbuilder.get_app)

def findAccessRoles(sm):
    try:
        pre, tkn, post = sm.oid.get_access_token().split('.')
        # logger.debug("get_access_token split . {},{},{}".format(pre, tkn, post))
        if not tkn:
            return None
        idp_token_decode = decode_base64(tkn)
        logger.debug('idp_token_decode %s' % idp_token_decode)
        access_token = json.loads(idp_token_decode)
        logger.debug('access_token in key resource_access.%s.roles' % AFIP_SERVICE_ID)
        rolesAccess = access_token['resource_access'][AFIP_SERVICE_ID]['roles']
    except Exception as e:
        logger.error('Failed to parser access_token: '+ str(e))
        return None

    logger.debug('rolesAccess %s' % rolesAccess)
    roles = None
    if rolesAccess or len(rolesAccess) > 0:
        roles = []
        for r in rolesAccess:
            find = sm.find_role(r.capitalize())
            if find:
                roles.append(find)
    return roles

def decode_base64(data):
    '''
    FIX -> OIDC + Python3 + Incorrect padding during base64decode
    '''
    missing_padding = 4-len(data) % 4
    if missing_padding:
        data = f"{data}{'=' * missing_padding:}"
    return b64decode(data)

Docker错误日志

superset_app          | 10.30.61.186 - - [10/Aug/2023:14:09:06 +0000] "GET /superset/welcome/ HTTP/1.0" 302 201 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
superset_app          | 10.30.61.186 - - [10/Aug/2023:14:09:06 +0000] "GET /login/ HTTP/1.0" 302 1449 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
superset_app          | 127.0.0.1 - - [10/Aug/2023:14:09:10 +0000] "GET /health HTTP/1.1" 200 2 "-" "curl/7.74.0"
superset_app          | (504, b'Unknown Host')
superset_app          | Traceback (most recent call last):
superset_app          |   File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1517, in full_dispatch_request
superset_app          |     rv = self.dispatch_request()
superset_app          |   File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1503, in dispatch_request
superset_app          |     return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
superset_app          |   File "/usr/local/lib/python3.8/site-packages/flask_oidc/__init__.py", line 657, in _oidc_callback
superset_app          |     plainreturn, data = self._process_callback('destination')
superset_app          |   File "/usr/local/lib/python3.8/site-packages/flask_oidc/__init__.py", line 689, in _process_callback
superset_app          |     credentials = flow.step2_exchange(code)
superset_app          |   File "/usr/local/lib/python3.8/site-packages/oauth2client/_helpers.py", line 133, in positional_wrapper
superset_app          |     return wrapped(*args, **kwargs)
superset_app          |   File "/usr/local/lib/python3.8/site-packages/oauth2client/client.py", line 2053, in step2_exchange
superset_app          |     resp, content = transport.request(
superset_app          |   File "/usr/local/lib/python3.8/site-packages/oauth2client/transport.py", line 280, in request
superset_app          |     return http_callable(uri, method=method, body=body, headers=headers,
superset_app          |   File "/usr/local/lib/python3.8/site-packages/httplib2/__init__.py", line 1724, in request
superset_app          |     (response, content) = self._request(
superset_app          |   File "/usr/local/lib/python3.8/site-packages/httplib2/__init__.py", line 1444, in _request
superset_app          |     (response, content) = self._conn_request(conn, request_uri, method, body, headers)
superset_app          |   File "/usr/local/lib/python3.8/site-packages/httplib2/__init__.py", line 1366, in _conn_request
superset_app          |     conn.connect()
superset_app          |   File "/usr/local/lib/python3.8/site-packages/httplib2/__init__.py", line 1156, in connect
superset_app          |     sock.connect((self.host, self.port))
superset_app          |   File "/usr/local/lib/python3.8/site-packages/httplib2/socks.py", line 504, in connect
superset_app          |     self.__negotiatehttp(destpair[0], destpair[1])
superset_app          |   File "/usr/local/lib/python3.8/site-packages/httplib2/socks.py", line 465, in __negotiatehttp
superset_app          |     raise HTTPError((statuscode, statusline[2]))
superset_app          | httplib2.socks.HTTPError: (504, b'Unknown Host')
superset_app          | 2023-08-10 14:09:32,111:ERROR:superset.views.base:(504, b'Unknown Host')

备注:内容来源于stack exchange,提问作者basarix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.21 15:49:39