🧠 背景介绍

本文介绍如何通过模拟登录方式接入 Kuboard,并在无法直接使用 API 接口的场景下,实现自动化集群管理能力集成,解决运维自动化中的权限认证问题。

Kuboard 是一款非常流行的 Kubernetes 管理面板,提供了良好的可视化集群运维能力。然而,在某些自动化集成场景下,官方提供的 API 能力有限,尤其是在集群管理、权限接入等方面仍依赖于用户界面交互:

  • SSO 登录流程需人工输入用户名密码
  • 缺乏用于登录态获取的标准开放 API
  • Token 有效期管理及跨系统调用困难

因此,我们探索了一种 模拟浏览器行为登录 Kuboard 并获取登录态 token 的方案,进而实现自动化添加集群等操作。


🔐 登录流程分析

Kuboard 的登录流程为:

  1. 用户访问 /sso/auth/default?req=xxx 页面
  2. 提交 loginpassword 字段进行认证
  3. 成功后服务端设置 Cookie,返回页面跳转或 token

登录请求实质上是一个 form 表单提交,核心参数包括:

  • login:用户名,如 admin
  • password:实际为一个 JSON 字符串,包含密码和 OTP 字段,如:
{
  "password": "Kuboard123",
  "passcode": ""
}

经过 URL 编码后提交,即可获取登录态。


🧪 模拟登录实现

import urllib.parse
import http.client
import logging

# 配置设置
config = {
    "host": "10.10.1.111",
    "port": 8089,
    "login": "admin",
    "password": '{"password":"Kuboard123","passcode":""}',
    "client_id": "kuboard-sso",
    "redirect_uri": "/callback",
    "scope": "openid profile email groups",
    "connector_id": "default",
    "state": "/"
}

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# 存储全局code值
global_code = None

# 发送请求并返回响应对象
def send_request(method, url, headers=None, body=None):
    conn = http.client.HTTPConnection(config["host"], config["port"])
    logging.info(f"发送请求: {method} {url}")
    
    conn.request(method, url, body, headers)
    response = conn.getresponse()
    
    logging.info(f"响应状态码: {response.status}")
    
    # 如果是callback接口,直接返回响应对象,不处理重定向
    if url.startswith("/callback"):
        return response
    
    # 处理重定向 (303 或 302 状态码)
    if response.status in [303, 302]:
        location = response.getheader("Location")
        if location:
            logging.info(f"重定向到: {location}")
            # 解析URL获取code参数
            parsed_url = urllib.parse.urlparse(location)
            query_params = urllib.parse.parse_qs(parsed_url.query)
            if 'code' in query_params:
                global global_code
                global_code = query_params['code'][0]
                logging.info(f"获取到code参数: {global_code}")
            conn.close()
            return location
    return response

# 1. 发送第一个请求,获取 req 参数
def get_req():
    url = f"/sso/auth?access_type=offline&client_id={config['client_id']}&redirect_uri={urllib.parse.quote(config['redirect_uri'])}&response_type=code&scope={urllib.parse.quote(config['scope'])}&state={urllib.parse.quote(config['state'])}&connector_id={config['connector_id']}"
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    
    response = send_request("GET", url, headers)
    
    # 获取 Location 中的 req 参数
    if isinstance(response, str):  # 如果是重定向
        req = response.split("req=")[-1]
        logging.info(f"[Step 1] 获取 req 参数... req = {req}")
        return req
    else:
        logging.error("无法获取 req 参数")
        return None

# 2. 使用 req 发送登录请求
def login(req):
    url = f"/sso/auth/default?req={req}"
    headers = {
        "Content-Type": "application/x-www-form-urlencoded",
        "Referer": f"http://{config['host']}:{config['port']}/sso/auth/default?req={req}"
    }
    
    data = {
        "login": config['login'],
        "password": config['password']
    }
    
    logging.info(f"[Step 2] 正在进行登录...")
    response = send_request("POST", url, headers, body=urllib.parse.urlencode(data))
    if isinstance(response, str):  # 如果是重定向
        return response
    logging.info(f"[Step 2] 登录成功... 状态码: {response.status}")
    return None

# 3. 提交认证信息
def submit_auth(req):
    url = f"/sso/approval?req={req}"
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    
    logging.info(f"[Step 3] 提交认证信息...")
    response = send_request("GET", url, headers)
    if isinstance(response, str):  # 如果是重定向
        return response
    logging.info(f"[Step 3] 提交认证信息成功... 状态码: {response.status}")
    return None

# 4. 获取最终 callback 返回的 cookie 中的 KuboardToken
def get_kuboard_token():
    if not global_code:
        logging.error("未获取到code参数")
        return None
        
    url = f"/callback?code={global_code}&state=%2F"
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    
    logging.info(f"[Step 4] 正在获取 KuboardToken...")
    response = send_request("GET", url, headers)
    
    # 直接获取 Set-Cookie 头部中的 KuboardToken
    if isinstance(response, http.client.HTTPResponse):
        set_cookie = response.getheader("Set-Cookie")
        if set_cookie:
            token = None
            for cookie in set_cookie.split(";"):
                if "KuboardToken" in cookie:
                    token = cookie.split("=")[-1]
                    logging.info(f"[Step 5] 获取 KuboardToken... KuboardToken = {token}")
                    return token
            if not token:
                logging.error("未获取到 KuboardToken")
        else:
            logging.error("没有 Set-Cookie 头部,无法获取 KuboardToken")
    else:
        logging.error("响应类型错误,无法获取 KuboardToken")
    return None

def main():
    logging.info("[Start] 开始流程...")
    
    # 步骤 1 获取 req 参数
    req = get_req()
    
    if req:
        # 步骤 2 登录
        redirect_url = login(req)
        if redirect_url:
            # 步骤 3 提交认证信息
            req = submit_auth(req)
            if req:
                # 步骤 4 获取 KuboardToken
                token = get_kuboard_token()
                if token:
                    print(token)  # 打印 token 以便 shell 调用
                else:
                    logging.error("未成功获取到 KuboardToken")

if __name__ == "__main__":
    main()

🔧 添加集群操作

完成模拟登录并获取 token 后,即可调用相关接口添加集群。示例请求如下:

# token=$(python3 login.py)
    # echo "KuboardToken is: $token"

# curl -i 'http://10.10.1.111:8089/kuboard-api/cluster/node011/kind/KubernetesCluster' \
#   -H 'Content-Type: application/json;charset=UTF-8' \
#   -b 'KuboardToken=eyJhbGciOiJSUzI1NiIsImtpZCI6ImU1Nzc5ZTMyMTkyMWRmMDVlODU3MjcyMzQyNDU5NWQzZjZlZGVlYzIifQ.eyJpc3MiOiJodHRwOi8vMTAuMTAuMS4yNjo4MDg5L3NzbyIsInN1YiI6IkNnVmhaRzFwYmhJSFpHVm1ZWFZzZEEiLCJhdWQiOiJrdWJvYXJkLXNzbyIsImV4cCI6MTc0NDc1Mzk4OSwiaWF0IjoxNzQ0MTQ5MTg5LCJhdF9oYXNoIjoiMHBnRnp3ZmhyU2stSnlHVHIzWmQxZyIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJncm91cHMiOlsiYWRtaW5pc3RyYXRvcnMiXSwibmFtZSI6IlN5c3RlbSBBZG1pbmlzdHJhdG9yIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWRtaW4ifQ.W6Fs8z16j67wy8_ZAFH1jIeBSmoJBgfEX8USuBrW6IzrbKHVajdOEpoBomug8m_pjr7oq1LUBTA-vg54w-lga6KD_utQ-OfJ9e00k1vOWshocH2ZJqUgWND1GSw8zeYd00CTR2QmNjPa4M_5cTV0yLQIVs3OWcs4P7u_knxfHQ3epX38i4VDjXPup_QXOniz64wEHWcS_PTDNkSr2EZ4Sp3yoT6LXwfGTv-Y0rwDce1849k-GntUVf9W5kmvlAqHBp1IF7wHkevA6jliytuCAV560czNKWT6Oh6ovHQYAZkIVuDETuo4Rzh9sIzUHjsMdq9add_M4VN3bvK4K8PsVw; KuboardLogin=true;' \
#   --data-raw '{"kind":"KubernetesCluster","metadata":{"name":"node011","annotations":{"description":"node011 名称"},"cluster":"node011"},"spec":{"connectionType":"token","type":"EXISTING","kubeserver":"https://10.10.1.111:6443","kubetoken":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjVDMjNJcjFwMmFtLWcteW9LOTkxTW9GRnFoVmxCRTBTbzNINkJla1BGY0UifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJvYXJkIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6Imt1Ym9hcmQtYWRtaW4tdG9rZW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoia3Vib2FyZC1hZG1pbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjkzMmM4MDUzLTM0MGMtNDMzMi1hNWQzLWQwMjVmZjY4MDVjMyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJvYXJkOmt1Ym9hcmQtYWRtaW4ifQ.EAmLkUjgUW6Fnb5EfO93jh1vyJyBTIAoochS0jGgMEZ1Ng_wXoWUj_IGTRBnfHi1rmeP6MOYxh3q_DNzuV07iT6jIT9CfhJMH50EOV2b7tnGp2IKZ8UZf65Abq2pJltVqPC98MuXsjU6ruWXM-yXsH997oYpDXzZI8revQoVGpOaCZOgTe1dKnMFTmZGCRUyL6W9cjuaNgcWp6B8A0db7t1sCavLUmLlidXGWG2AJGRYLxYnVcE1WCNpFBfQ4allohVAyV1MySIz5XOcZB75w1zBvAxLFHHza7dhxkjmpBQjYVdjob9i06OZ8GtzuQTQAZK0dADRqUCJdtHqyQ2DWQ"},"status":{"phase":"IMPORTED"}}' \
#   --insecure

其中 cluster_config.json 包含集群信息、API server、kubeconfig 等字段,可通过界面抓包获取标准格式。


🪛 技术细节与优化建议

  • 登录请求推荐使用 Python + requests 模拟更为稳妥(可携带完整 Cookie + 重定向处理)
  • REQ 参数为跳转标识,来自初始页面请求,可自动提取或缓存重用
  • 支持将整个流程封装为脚本,自动处理登录、token 获取与接口调用
  • 可集成至运维平台实现“一键导入集群”功能

⚠️ 风险提示

虽然该方式解决了集成能力不足的问题,但仍存在一些潜在风险:

  • Kuboard 登录流程如有升级,可能导致模拟失败
  • Token 有效期管理需自行处理(如定时刷新)
  • 模拟行为可能违反运维合规要求,需与平台管理员确认

📌 总结

Kuboard 提供了丰富的可视化管理能力,但在接口集成上仍不够开放。通过模拟浏览器登录流程,我们可以:

✅ 获取登录态并提取 Token
✅ 实现自动化添加集群等能力集成
✅ 规避人工操作瓶颈,助力 DevOps 自动化

此方案适用于需要高频或批量管理 K8s 集群的场景,在官方 API 不支持的前提下,是一种务实且有效的替代方案。


🔗 参考资料

```