本文档属于 Cloud WAF — 企业 Web 防护管理平台
CLI 工具:zcloud· 5 大模块:guard / sys / analytics / cli_release / auth
完整 API 索引:/api/openapi.json · 文档地图:/sitemap.xml · AI 速读:/llms.txt
认证说明
概述
Cloud WAF 当前支持两种认证方式。所有受保护接口都要求请求头携带 Authorization,同一请求只能选择其中一种通道:
| 场景 | 请求头 | 说明 |
|---|---|---|
| 人工登录 / Web 控制台 / CLI 交互登录 | Authorization: Bearer <token> |
token 由 POST /api/auth/login 颁发,通常有效期为数小时(受系统安全策略约束) |
| 脚本 / CI / 第三方系统集成 | Authorization: ApiKey zck_<prefix>.<secret> |
API Key 是机器调用凭证,明文仅签发时返回一次,适合长期自动化对接 |
推荐:人工操作使用
Bearer会话;机器对接优先使用 API Key。历史的“账号密码换短期 token + 401 自动重登”仍可用,但只建议作为兼容方案。
API Key 机器调用凭证
API Key 是发给脚本、CI、第三方系统使用的机器调用凭证。它代表某个用户在某个 OEM 下发起请求,并受到两层限制:
- 签发用户当前拥有的 RBAC 权限;
- API Key 自身的
scopes列表。
最终有效权限为:effective_perms = user.RBAC ∩ key.scope。scope 只能收窄,不能放大;如果 scopes 为空数组,则完整继承签发用户当前 RBAC。
签发 API Key
curl -sS -X POST https://waf.example.com/api/sys/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "生产对接",
"scopes": ["guard.domain.list", "guard.domain.view"],
"expires_in_days": 90
}'
响应中的 data.api_key 是完整明文:
{
"code": 0,
"message": "ok",
"data": {
"key_id": "8f21c0c5-55ae-4cbd-a60a-8e64a6e2b1d0",
"api_key": "zck_abc12345.A1b2C3d4E5f6G7h8I9j0K1L2M3n4O5p6Q7r8S9t0",
"prefix": "zck_abc12345",
"last4": "5t0",
"expires_at": 1732982400000
}
}
⚠
api_key明文仅此一次返回。后续列表接口只返回prefix和last4,无法找回完整 secret;丢失后请吊销并重新签发。
使用 API Key 调接口
curl -sS https://waf.example.com/api/guard/domains \
-H "Authorization: ApiKey zck_abc12345.A1b2C3d4..." \
-H 'Accept-Language: zh-CN'
API Key 与 Bearer token 的区别
| 维度 | Bearer token | API Key |
|---|---|---|
| 主要用途 | 人登录后的短期会话 | 机器、脚本、CI、第三方系统长期调用 |
| 获取方式 | POST /api/auth/login |
POST /api/sys/api-keys |
| 请求头 | Authorization: Bearer <token> |
Authorization: ApiKey zck_<prefix>.<secret> |
| 生命周期 | 通常数小时,受自动登出策略影响 | 默认 90 天,最大 365 天,可主动吊销 |
| 权限边界 | 当前用户 RBAC | 当前用户 RBAC ∩ Key scopes |
| 明文保存 | 客户端保存 token | 只在签发响应返回一次,服务端不保存明文 |
Scope 匹配规则(重要)
API Key 的 scopes 校验语义与用户角色 RBAC 刻意分离,请务必先理解后再签发:
- 精确字符串相等(大小写敏感):
scope列表中的元素必须与接口要求的权限 key 字面量完全一致才算命中。 - 不展开通配符:写
guard.*或guard.domain.*进scopes不会命中任何接口——服务端不做前缀 / glob / 正则展开。例如接口要求guard.domain.list,scope 里只有guard.domain.*时仍然返回code=1053。 - 与用户 RBAC 独立判定:用户角色权限可以是
guard.domain.*简化分配,但 API Key scope 必须列举到叶子 perm key。两者按effective_perms = user.RBAC ∩ key.scope串行判断,user 通配符匹配的接口若不在 scope 列表里仍然被拦截。 - 签发时如需 guard 全模块权限:请逐条列举具体 perm key(例如
guard.domain.list、guard.domain.view、guard.domain.create…),可通过GET /api/sys/permissions/tree获取叶子权限清单。 - 失败错码:scope 不命中本次接口返回
code=1053(HTTP 403);与 1056 (apikey forbidden,越 OEM 边界) 是不同语义,排查时可按错码 1:1 区分。
安全建议
- 给 CI / 第三方系统创建专用低权限用户,再由该用户签发 API Key。
scopes只授予必需权限,例如只读域名列表时只给guard.domain.list/guard.domain.view。- 不要把完整 API Key 写进 git、日志或工单;只展示
prefix/last4。 - 定期查询
GET /api/sys/api-keys,审计last_used_at/last_used_ip。 - 发现泄露或不再使用时,立即调用
DELETE /api/sys/api-keys/:id吊销。
登录链路图(文字版)
┌─────────────┐ ┌────────────────┐
│ 客户端 / CLI │ ──── POST /api/auth/login ───▶ │ Cloud WAF 后端 │
└─────────────┘ {username, password} └────────────────┘
│ │
│ ◀────── 200 OK {data: {token, user}} ───────────│
│ │
│ ──── GET /api/sys/users (Bearer token) ────────▶│
│ ◀────── 200 OK {code:0, data:[...]} ────────────│
│ │
│ ──── (token 过期) GET /api/... ────────────────▶│
│ ◀────── 401 Unauthorized ───────────────────────│
│ │
│ ──── POST /api/auth/login (重新登录) ──────────▶│
│ ◀────── 200 OK {data: {token: <新>}} ──────────│
完整 curl 示例
1) 登录
curl -sS -X POST https://waf.example.com/api/auth/login \
-H 'Content-Type: application/json' \
-d '{
"username": "admin",
"password": "YOUR_PASSWORD"
}'
返回:
{
"code": 0,
"message": "ok",
"data": {
"token": "eyJhbGciOi...",
"user": { "uuid": "abc-123", "username": "admin", "role_id": 1 }
}
}
2) 携带 token 调用接口
TOKEN='eyJhbGciOi...'
curl -sS https://waf.example.com/api/sys/users \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept-Language: zh-CN'
3) 注销
curl -sS -X POST https://waf.example.com/api/auth/logout \
-H "Authorization: Bearer $TOKEN"
注销后该 token 立刻失效,后续使用返回 401。
CLI 多 profile 机制
CLI 把凭据写到 ~/.zcloud/credentials.toml,结构如下:
active = "default"
[profiles.default]
api_url = "https://waf.example.com"
username = "admin"
token = "eyJhbGciOi..."
oem_id = "default"
[profiles.prod]
api_url = "https://prod.example.com"
username = "ops"
token = "eyJhbGciOi..."
切换 profile:
zcloud config profiles list
zcloud config profiles activate prod
# 注意:profiles create 只接受位置参数 <name>,不支持任何 flag
zcloud config profiles create staging
zcloud --profile staging config set api_url https://staging.example.com
每次 zcloud auth login 会更新当前 profile 的 token;不同 profile 互不干扰。
401 自动重试模式
token 过期是常态,正确做法是把"401 → 重登 → 重试"封装成 wrapper。
Python 伪代码
import requests
class CloudWAF:
def __init__(self, base_url, username, password):
self.base = base_url.rstrip('/')
self.username = username
self.password = password
self.token = None
self._login()
def _login(self):
r = requests.post(f"{self.base}/api/auth/login", json={
"username": self.username,
"password": self.password,
})
r.raise_for_status()
self.token = r.json()["data"]["token"]
def call(self, method, path, **kwargs):
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {self.token}"
url = f"{self.base}{path}"
r = requests.request(method, url, headers=headers, **kwargs)
if r.status_code == 401:
self._login() # 重登
headers["Authorization"] = f"Bearer {self.token}"
r = requests.request(method, url, headers=headers, **kwargs)
r.raise_for_status()
return r.json()
Go 伪代码
type Client struct {
BaseURL string
User string
Password string
Token string
HTTP *http.Client
}
func (c *Client) login() error {
body, _ := json.Marshal(map[string]string{"username": c.User, "password": c.Password})
resp, err := c.HTTP.Post(c.BaseURL+"/api/auth/login", "application/json", bytes.NewReader(body))
if err != nil { return err }
defer resp.Body.Close()
var out struct{ Data struct{ Token string } }
json.NewDecoder(resp.Body).Decode(&out)
c.Token = out.Data.Token
return nil
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
req.Header.Set("Authorization", "Bearer "+c.Token)
resp, err := c.HTTP.Do(req)
if err != nil { return nil, err }
if resp.StatusCode == 401 {
resp.Body.Close()
if err := c.login(); err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+c.Token)
return c.HTTP.Do(req)
}
return resp, nil
}
机器对接模式(兼容方案)
机器对接推荐使用上文 API Key。只有在暂时无法签发 API Key、或需要兼容旧流水线时,才使用“账号密码换短期 token + 401 自动重登”模式。
zcloud auth login 是交互式命令(用 util.ReadPassword 读不回显的密码),不支持 --username/--password 之类的 flag。CI Pipeline / 第三方系统应直接调用登录 REST API 拿到 token,然后通过 ZCLOUD_TOKEN 环境变量注入 CLI。
推荐做法:
- 优先评估 API Key:如果只是调用 REST API,直接签发 API Key;如果必须复用旧 CLI 流程,再继续下面步骤
- 创建专用账号:在 Web 控制台为 CI 创建
ci-bot用户,分配最小必要权限 - 凭据保存到 secrets:
CLOUDWAF_USERNAME/CLOUDWAF_PASSWORD存到 GitHub Actions / GitLab CI secrets - 任务开始时调 REST 拿 token:
POST /api/auth/login,密码用 base64 编码后传 - 后续 CLI 命令读取
ZCLOUD_TOKEN环境变量,无需写入本地 credentials 文件 - 永远启用 401 重试 wrapper:长任务中 token 可能过期,wrapper 拿到 401 时重新登录刷新 token
# Gitea Actions 示例
jobs:
publish-cert:
steps:
- run: |
curl -fsSL ${{ vars.WAF_URL }}/api/cli/install.sh | sh
zcloud config set api_url ${{ vars.WAF_URL }}
# 1) 通过 REST 接口换 token(密码 base64 编码)
PWD_B64=$(printf '%s' "${{ secrets.WAF_PASS }}" | base64 -w0)
TOKEN=$(curl -fsS "${{ vars.WAF_URL }}/api/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"username\":\"${{ secrets.WAF_USER }}\",\"password\":\"$PWD_B64\"}" \
| jq -r '.data.token')
# 2) 注入环境变量后跑业务命令;CLI 自动读取 ZCLOUD_TOKEN
export ZCLOUD_TOKEN="$TOKEN"
zcloud guard certs upload --name star --cert ./fullchain.pem --key ./private.key
# 3) 任务结束撤销 token
zcloud auth logout
安全建议
- ✅ 始终走 HTTPS;HTTP 仅限本地开发
- ✅ token 不要写进 git,不要打日志
- ✅ API Key 明文仅保存到密钥管理系统,不要写入配置仓库或普通日志
- ✅ CI 用专用账号 + 最小权限,不要复用管理员
- ✅ 定期审计
GET /api/sys/sessions在线会话,发现异常立即kill - ✅ 定期审计
GET /api/sys/api-keys,发现异常立即吊销 - ❌ 禁止把 token 嵌入前端代码(前端拿 token 应通过登录接口)
- ❌ 禁止把 API Key 嵌入前端代码或公开客户端
- ❌ 禁止跨账号共用 profile(多人共用 = 审计无法定责)
常见问题
| 现象 | 处理 |
|---|---|
登录返回 code 非 0 |
检查 message,常见原因:账号锁定、密码错、密码到期 |
| 调接口反复 401 | 检查 token 是否粘贴完整、Authorization 头格式是否正确 |
| API Key 调接口返回 401 | 检查请求头是否为 Authorization: ApiKey zck_<prefix>.<secret>,确认 key 未过期或吊销 |
API Key 调接口返回 code=1053 (HTTP 403) |
scopes 未覆盖接口所需权限。注意 scope 是精确字符串相等,写 guard.* 不会命中任何接口,需逐条列举叶子 perm key |
API Key 调接口返回 code=1056 (HTTP 403) |
跨 OEM 越权:请求 hostname 与签发 API Key 时绑定的 OEM 不一致,请用同 OEM hostname 重试 |
登录返回 200 但 data.token 为空 |
服务端配置异常,联系运维 |
| 多个 CLI 进程互相挤掉对方 token | 用不同 profile 隔离 |
相关文档
Cloud WAF · 支持 Bearer 会话与 API Key 机器调用凭证双通道认证