🧠 背景介绍
本文介绍如何通过模拟登录方式接入 Kuboard,并在无法直接使用 API 接口的场景下,实现自动化集群管理能力集成,解决运维自动化中的权限认证问题。
Kuboard 是一款非常流行的 Kubernetes 管理面板,提供了良好的可视化集群运维能力。然而,在某些自动化集成场景下,官方提供的 API 能力有限,尤其是在集群管理、权限接入等方面仍依赖于用户界面交互:
- SSO 登录流程需人工输入用户名密码
- 缺乏用于登录态获取的标准开放 API
- Token 有效期管理及跨系统调用困难
因此,我们探索了一种 模拟浏览器行为登录 Kuboard 并获取登录态 token 的方案,进而实现自动化添加集群等操作。
🔐 登录流程分析
Kuboard 的登录流程为:
- 用户访问
/sso/auth/default?req=xxx
页面 - 提交
login
与password
字段进行认证 - 成功后服务端设置 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 不支持的前提下,是一种务实且有效的替代方案。
🔗 参考资料
```