前言
ckman
是一款由擎创科技数据库团队主导开发的,用来管理和监控ClickHouse
的可视化开源工具。它将繁琐的ClickHouse
集群的配置和监控工作变的简单化、直观化。github
地址为ckman, 感兴趣的同学可以从此处下载源代码。
虽然ckman
提供了web
界面,但是对于部分企业来说,可能某些API
的调用,直接使用网页端并不是那么的通用,而是更多的期望将其整合到自己的产品中去。比如通过自己的产品页面,就可以直接调用ckman
的API
,将ckman
作为一个server
端,从而很方便地进行集群管理,数据库表的管理以及数据监控。
为了安全考虑,ckman
加入了https
支持以及token
进行鉴权。token
的加入,虽然大大提高了数据链路的安全性,但是对于开发来说,却提供了一定难度(因为我们虽然能够通过登录的接口获取到token
是什么,但是一旦ckman
重启或者token
过期,就会导致token
失效)。
正是因为这种情况,ckman
在token
之外,提供了另一种访问方案。只要请求在header
中携带userToken
的字段,且满足ckman
规定的默认格式,就可以跳过token
的鉴权机制,从而保证不会有token
无效或过期自动跳转到登录页面的问题。
那么userToken
是如何保证链路安全的呢?
在这里,ckman
使用了RSA
加密的技术。用户可以自己提供公钥和私钥,将userToken
的内容使用私钥加密后,放入请求的header
中,在ckman
接收到请求的时候,一旦识别到header
中有userToken
的字段,就会使用对应的公钥去解密,从而建立有效的连接。
而我们仅仅要做的,就是将公钥配置到ckman
的配置文件中即可。
server:
id: 1
port: 8808
https: false
pprof: true
session_timeout: 3600
public_key: #该字段配置公钥
那么,userToken
的默认格式是什么样的呢?
ckman
提供的userToken
是一个包含了时间戳、超时时间等字段的一个类(结构体),其结构如下所示:
type UserTokenModel struct {
Duration int64
RandomPaddingValue string
UserId int64
Timestamp int64
}
其中,我们只需要关心Duration
和Timestamp
字段即可。Duration
指的是超时时间,ckman
会将接收到请求的当前时间与userToken
中携带的Timestamp
时间进行比对,一旦时间间隔超过Duration
,则认为超时。因此,这个超时时间是由client
自己去控制的,自由度比较大。
代码演示
由于ckman
是使用go
语言开发的,所以我在这里也是用go
进行演示。(事实上,只要符合以上规范,使用任何编程语言都能达到同样的效果)。
首先,我们在ckman
的配置文件中配置公钥,关于如何生成RSA
公钥,网上教程有很多,这里就不多赘述了。
(ps
:示例代码中的公钥和私钥来自github.com/housepower/ckman/common/rsa_test.go)
server:
id: 1
port: 8808
https: false
pprof: true
session_timeout: 3600
public_key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNw9cbOh1JRVNf/pQiRRMoa4TSmgZeq9zyK+Z5qE0Ak1XcmFzRg1m667ZAgfl/gEiwMGtbKyiPBGeHP5Gw3z5ENIHg7WGKTE0yRM/U/FMnktjly2xzjf7HUl/IA7PFYq5KBVBNPhjwzuFxpmsJL+fhhuYB75uDL0axYwcm7WHdewIDAQAB
然后启动ckman
,保持服务运行。
接下来就可以编写client
端的代码了。
为了方便,我在这里直接引用ckman
的包,只需要在控制台运行go get github.com/housepower/ckman
即可。当然如果觉得引入ckman
的包太大,你也可以自己实现一套rsa
通过私钥加密的算法,可参考github.com/housepower/ckman/common/rsa.go。
代码示例如下:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/housepower/ckman/common"
)
const PRI_KEY string = "MIICXgIBAAKBgQDNw9cbOh1JRVNf/pQiRRMoa4TSmgZeq9zyK+Z5qE0Ak1XcmFzRg1m667ZAgfl/gEiwMGtbKyiPBGeHP5Gw3z5ENIHg7WGKTE0yRM/U/FMnktjly2xzjf7HUl/IA7PFYq5KBVBNPhjwzuFxpmsJL+fhhuYB75uDL0axYwcm7WHdewIDAQABAoGBAIKxMz1t6hAR4mUEc95YdVSlBhYmEomrK4j97UO0bERDULPuanYAscuRz46lf21Gc+TEvEuJ3BcKux8id00aXpcbbNhqIDyUMvET4MjdisgXhxay/dzc6jRBYQdhMrLT0NYfQSbULdXA3CGQhti4nChazn708ag6slvjGtsC4O9BAkEA9c/ZmbKisBb3GweWP/IhYB+GO5Qsby0KkF582NgnGIjnpGirniO2jyNSXO72QerTfG4JXqofGkH7AmlO0bkX0QJBANZLFBzoRIJr8x32dsKnd/V/7k2OgNbrUGwFJrJOGCSClPF7yM3xjN0lg3EjKW4AZP75pr//vOLOYTQDHyeNv4sCQQCoUlzyJ2XJ6N/q7WYQgbAjD1MuxwcqVhBuzZT2NAWJgm4EofwqvM/M8mX651NPzgploT/fR+UmaNoGS7BCYlmRAkAFAY3/uuFW1qTAT3CozXa88ncjsq+J1cd0Lo6f3bksqSxHk+e1/+2VgPnYG8Us/69cUYK2u4ezGLUmnOgOaX5PAkEA6wwIjYGDQRYIEVD4oJyNtdL7FFso63lon3LMySxLgi/KZGS4N8+FYJQVIzWWCrdk3Z1mXw4wuOQkE4pDy8xx+w=="
func main() {
//构建userToken结构体
userTokenModel := common.UserTokenModel{
Duration: 3600, //超时时间,这里设的是1个小时
RandomPaddingValue: "ckmandemo", //随机值,eoi产品中使用的,ckman中用不到
UserId: 123456, //用户id,eoi产品中可以设置不同的用户权限,ckman中用不到
Timestamp: time.Now().UnixNano() / 1e6, //当前时间,单位为毫秒
}
//将userToken结构体使用私钥加密
uenc, _ := json.Marshal(userTokenModel)
var rsaEncrypt common.RSAEncryption
userToken, err := rsaEncrypt.Encode(uenc, PRI_KEY)
if err != nil {
fmt.Println("rsa encode error:", err)
return
}
//创建http客户端
client := http.Client{}
//示例中我们调用/api/v1/ck/get 接口去获取ipv4集群的节点状态
url := "http://localhost:8808/api/v1/ck/get/ipv4"
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println("http request failed:", err)
return
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Content-type", "application/json")
req.Header.Add("userToken", string(userToken)) //将userToken添加到header中,可以看到,这里并没有登录,也没有在header中添加token
response, err := client.Do(req)
if err != nil {
fmt.Println("http error:", err)
return
}
if response.StatusCode == 200 {
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
fmt.Println("response error:", err)
return
}
//打印返回结果
fmt.Println(string(body))
} else {
fmt.Println("response:", response.Status)
return
}
}
运行结果如下所示:
chenyc@YenchangChan:ckmanDemo$go run main.go
{"retCode":0,"retMsg":"ok","entity":{"status":"green","version":"21.3.9.83","nodes":[{"ip":"192.168.21.51","hostname":"node1","status":"green","shardNumber":1,"replicaNumber":1,"disk":"36.95GB/49.98GB"},{"ip":"192.168.21.52","hostname":"node2","status":"green","shardNumber":1,"replicaNumber":2,"disk":"28.15GB/49.98GB"},{"ip":"192.168.21.53","hostname":"node3","status":"green","shardNumber":2,"replicaNumber":1,"disk":"28.71GB/49.98GB"},{"ip":"192.168.21.54","hostname":"node4","status":"green","shardNumber":2,"replicaNumber":2,"disk":"23.27GB/49.98GB"}],"mode":"deploy","needPassword":false}}
这与我们使用网页端访问的结果一致:
如果我们使用F12
打开调试模式,会发现该接口返回的JSON
与代码输出的JSON
结果是一致的。
上面简单演示了GET
请求,如果是POST
或者其他请求,也是一样的操作,这里就不做演示了。关于ckman
对外提供的接口,可以参考swagger
文档。
开启swagger
文档需要在配置文件增加swagger_enable
配置项:
server:
id: 1
port: 8808
https: false
pprof: true
swagger_enable: true #配置此项即可使用swagger文档
session_timeout: 3600
public_key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNw9cbOh1JRVNf/pQiRRMoa4TSmgZeq9zyK+Z5qE0Ak1XcmFzRg1m667ZAgfl/gEiwMGtbKyiPBGeHP5Gw3z5ENIHg7WGKTE0yRM/U/FMnktjly2xzjf7HUl/IA7PFYq5KBVBNPhjwzuFxpmsJL+fhhuYB75uDL0axYwcm7WHdewIDAQAB
重启后可登录http://localhost:8808/swagger... 进行查看。界面如下所示:
总结
本文简单介绍了如何在实际开发中使用代码操作ckman
,可以通过配置RSA
公钥和私钥的方式跳过token
鉴权,这样有助于将ckman
的调用与企业自己的产品相结合,使得ckman
不仅仅是一款独立的管理工具,更是扮演了一个server
服务端的角色,从而大大扩展了ckman
的使用场景。