hmacauth
基于 go-zero httpc 封装的 Kong HMAC Auth Go 客户端,自动完成 HMAC 签名,内置熔断器。遵循 Kong HMAC Auth 插件 官方规范(基于 draft-cavage-http-signatures 草案,使用 @request-target 伪头)。
特性
- 🔐 自动签名 — 每个请求自动注入
Authorization签名头(hmac username="...", algorithm="...", headers="...", signature="..."),调用方无感知 - 🔀 双方法 API —
Do处理结构化数据(JSON / form / path / header 自动映射),DoRaw处理原始字节(文件上传、XML、纯文本等) - 🛡️ 熔断保护 — 底层使用 go-zero httpc,同一个 Host 自动共享熔断器,错误率过高自动熔断
- 🔧 可定制 HTTP Client — 通过
WithClient注入自定义*http.Client(超时、TLS、连接池等),不传则使用http.DefaultClient - 🧩 多算法支持 —
hmac-sha1、hmac-sha224、hmac-sha256、hmac-sha384、hmac-sha512五种算法全覆盖 - 📋 灵活签名头 — 支持自定义参与签名的 header 列表,默认
["date", "@request-target"] - 📦 Body 完整性 — 当
Headers包含digest时,自动计算Digest: SHA-256=...头(含空 body 场景)
安装
go get github.com/lerity-yao/czt-contrib/kong/hmacauth@v0.0.1配置参数
Conf
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
Host | string | 是 | — | Kong 网关地址,必须以 http:// 或 https:// 开头 |
Username | string | 是 | — | Kong consumer 凭证用户名(即 key id) |
Secret | string | 是 | — | HMAC 签名密钥 |
Algorithm | string | 否 | hmac-sha256 | 签名算法,支持 hmac-sha1 / hmac-sha224 / hmac-sha256 / hmac-sha384 / hmac-sha512 |
Headers | []string | 否 | ["date", "@request-target"] | 参与签名的 header 列表(小写),支持标准 HTTP header 和 @request-target 伪头 |
调用 NewClient 时会自动执行 Validate() 校验上述字段,并为 Algorithm 和 Headers 填充默认值。
Algorithm和Headers支持 go-zeroconf.MustLoad的optional/defaulttag,YAML 配置文件中可省略。
API 参考
构造函数
| 函数 | 签名 | 说明 |
|---|---|---|
MustNewClient | func MustNewClient(c Conf, opts ...ClientOption) Client | 创建 Client,校验失败 panic |
NewClient | func NewClient(c Conf, opts ...ClientOption) (Client, error) | 创建 Client,校验失败返回 error |
ClientOption
| Option | 参数 | 说明 |
|---|---|---|
WithClient | *http.Client | 注入自定义 HTTP Client(超时、TLS、连接池等),不传则使用 http.DefaultClient |
算法常量
| 常量 | 值 | 说明 |
|---|---|---|
AlgorithmHmacSHA1 | "hmac-sha1" | SHA-1,Kong 默认禁用,不推荐使用 |
AlgorithmHmacSHA224 | "hmac-sha224" | 需要 Kong 3.14+ |
AlgorithmHmacSHA256 | "hmac-sha256" | 推荐默认 |
AlgorithmHmacSHA384 | "hmac-sha384" | |
AlgorithmHmacSHA512 | "hmac-sha512" |
Client 接口方法
| 方法 | 签名 | 适用场景 |
|---|---|---|
Do | Do(ctx, method, path string, data any) (*http.Response, error) | 结构化请求:JSON body / form query / path 参数 / header 自动映射 |
DoRaw | DoRaw(ctx, method, path, contentType string, body []byte) (*http.Response, error) | 原始请求:文件上传、XML、纯文本、加密 payload 等自定义 body |
注意:
DoRaw在body非空时强制要求contentType非空,否则返回 error。无 body 的请求(如 GET)contentType可为空。
响应解析
| 函数 | 签名 | 适用场景 |
|---|---|---|
Parse | Parse(resp *http.Response, err error, val any) error | JSON 响应:解析 header + body,自动关闭 Body |
Parse 内部委托 go-zero httpc.Parse,支持响应头(header tag)和 JSON body(json tag)解析。对于 XML、二进制等非 JSON 响应,请直接读取 resp.Body。
HTTP 方法请直接使用标准库
http.MethodGet、http.MethodPost等。
进阶指南
签名机制
SDK 基于 Kong HMAC Auth 插件 官方规范,每个请求自动完成以下工作:
- 设置默认头(未设置时):
Date(GMT 格式)、User-Agent(默认Go-Kong-HmacAuth-Client)、Host(当 host 在签名列表中时) - 计算 Digest(当
Headers包含digest时):对非 form / multipart 的 body 计算SHA-256,格式为Digest: SHA-256=<base64>;空 body 也计算零长度摘要 - 构建待签名字符串:按
Headers列表顺序拼接,每行格式为header-name: value,行间用\n分隔,最后一行无尾随换行
date: Thu, 22 Jun 2023 17:15:21 GMT
@request-target: get /v1/users?page=1
host: api.example.com
digest: SHA-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=- 计算签名:用
Secret对待签名字符串做 HMAC(算法由Algorithm指定),结果 Base64 编码 - 设置 Authorization 头:
Authorization: hmac username="alice", algorithm="hmac-sha256", headers="date @request-target", signature="abc123..."@request-target 伪头
Kong HMAC Auth 使用 @request-target 作为伪头表示请求目标行,值为 method /path?query(method 全小写):
@request-target: post /v1/users?name=tomKong 官方推荐使用
@request-target而非旧版的request-line。
Digest 计算
当 Headers 包含 digest 时,SDK 自动计算并注入 Digest 头:
- 算法:固定使用 SHA-256,格式
Digest: SHA-256=<base64> - 空 body:同样计算摘要(
sha256.Sum256(nil)),确保请求完整性可验证 - form / multipart 跳过:
application/x-www-form-urlencoded和multipart/form-data类型的 body 不计算 Digest,因为 form 数据在签名前可能被 HTTP transport 重编码
当 Kong 端同时启用
validate_request_body时,SDK 的 Digest 头可保证 body 端到端完整性校验。
算法选择
| 算法 | 安全性 | 性能 | 适用场景 |
|---|---|---|---|
hmac-sha1 | 弱(已不推荐) | 快 | 仅兼容旧系统,Kong 默认禁用 |
hmac-sha224 | 中 | 较快 | 需要 Kong 3.14+ |
hmac-sha256 | 强 | 快 | 推荐默认,安全性与性能的最佳平衡 |
hmac-sha384 | 很强 | 较慢 | 高安全需求场景 |
hmac-sha512 | 很强 | 最慢 | 最高安全需求场景 |
算法通过
Conf.Algorithm或常量(如hmacauth.AlgorithmHmacSHA256)指定,不传默认hmac-sha256。传入不区分大小写,内部自动转小写。
签名头列表配置
Headers 决定哪些 header 参与签名。默认只签 date 和 @request-target,Kong 官方推荐的强签名配置为 @request-target + host + date + digest:
hmacauth.Conf{
Host: "https://api.example.com",
Username: "alice",
Secret: "my-secret",
Headers: []string{"date", "host", "@request-target", "digest"},
}常见 header 选项:
| Header | 说明 |
|---|---|
date | 请求时间(GMT 格式),Kong 用此做 clock skew 校验 |
host | 请求目标主机 |
@request-target | 请求方法 + 路径 + 查询参数 |
digest | 请求体 SHA-256 摘要,用于 body 完整性校验 |
| 自定义 header | 如 x-request-id、x-custom-header 等 |
Headers中的 header 名会自动转为小写。自定义 header 需要确保请求中已设置对应值,否则签名字符串中该 header 值为空。
Do 与 DoRaw 如何选择
| 场景 | 推荐方法 | 说明 |
|---|---|---|
| POST JSON body | Do | struct 带 json tag,自动序列化 |
| GET 带 query 参数 | Do | struct 带 form tag,自动拼 query string |
| GET 带 path 参数 | Do | struct 带 path tag,自动填充 :id |
| 设置自定义 header | Do | struct 带 header tag |
| GET 无参数 | Do | data 传 nil |
| 文件上传(multipart) | DoRaw | 手动构建 multipart body |
| XML body | DoRaw | 传 XML 字符串的字节 |
| 纯文本 / 加密 payload | DoRaw | 传原始字节 |
| 表单(手动编码) | DoRaw | 传已编码的字节 |
Do 的 data 参数 Tag 说明
Do 内部委托 go-zero httpc buildRequest,通过 struct tag 自动映射:
| Tag | 作用 | 示例 |
|---|---|---|
path:"id" | 填充 URL 路径参数 :id | /v1/users/:id → /v1/users/123 |
form:"page" | 拼接到 query string | ?page=1 |
json:"name" | JSON body 字段 | {"name":"tom"} |
header:"X-Token" | 请求头 | X-Token: abc |
当 struct 含
jsontag 时,httpc 自动设置Content-Type: application/json。GET / HEAD 方法不允许带 body。
超时控制
SDK 遵循 go-zero httpc 惯例,不设 http.Client.Timeout,超时完全由 context.Context 控制。
在 go-zero 框架中,调用方传入的 ctx 已自带 deadline(由 rest.RestConf.Timeout 注入),会自动传播到每个请求,无需额外处理:
// go-zero handler 中,l.ctx 已有 RestConf.Timeout 的 deadline
resp, err := l.svcCtx.Kong.Do(l.ctx, http.MethodGet, "/v1/users", nil)如果在非 go-zero 环境中使用,请通过 context.WithTimeout 传入 deadline:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
resp, err := client.Do(ctx, http.MethodGet, "/v1/users", nil)注意:不要在自定义
*http.Client上设置Timeout。如果http.Client.Timeout早于 context deadline 到期,会覆盖调用方意图中的超时时间,导致行为与预期不一致。
完整示例
创建客户端
import "github.com/lerity-yao/czt-contrib/kong/hmacauth"
client := hmacauth.MustNewClient(hmacauth.Conf{
Host: "https://api.example.com",
Username: "alice",
Secret: "my-secret",
})自定义算法和签名头
client := hmacauth.MustNewClient(hmacauth.Conf{
Host: "https://api.example.com",
Username: "alice",
Secret: "my-secret",
Algorithm: hmacauth.AlgorithmHmacSHA512,
Headers: []string{"date", "host", "@request-target", "digest"},
})Do:结构化请求
// 1. POST JSON body + 解析响应
type CreateUserReq struct {
Name string `json:"name"`
Email string `json:"email"`
}
resp, err := client.Do(ctx, http.MethodPost, "/v1/users", &CreateUserReq{
Name: "tom",
Email: "tom@example.com",
})
var result struct {
ID int `json:"id"`
}
if err := hmacauth.Parse(resp, err, &result); err != nil {
return err
}
// result.ID == 123,resp.Body 已自动关闭// 2. GET 带 path + query 参数
type GetUserReq struct {
ID string `path:"id"`
Page int `form:"page"`
}
// 自动填充:GET /v1/users/123?page=1
resp, err := client.Do(ctx, http.MethodGet, "/v1/users/:id", GetUserReq{
ID: "123",
Page: 1,
})
defer resp.Body.Close()// 3. GET 无参数,data 传 nil
resp, err := client.Do(ctx, http.MethodGet, "/v1/users", nil)
defer resp.Body.Close()DoRaw:原始请求
// 1. JSON 原始字节
jsonBody, _ := json.Marshal(map[string]any{"name": "tom"})
resp, err := client.DoRaw(ctx, http.MethodPost, "/v1/users",
"application/json", jsonBody)
defer resp.Body.Close()// 2. XML body
xmlStr := `<?xml version="1.0"?><user><name>tom</name></user>`
resp, err := client.DoRaw(ctx, http.MethodPost, "/api/pay",
"application/xml", []byte(xmlStr))
defer resp.Body.Close()// 3. 文件上传(multipart)
multipartBody := &bytes.Buffer{}
writer := multipart.NewWriter(multipartBody)
part, _ := writer.CreateFormFile("file", "test.png")
part.Write(fileBytes)
writer.Close()
resp, err := client.DoRaw(ctx, http.MethodPost, "/upload",
writer.FormDataContentType(), multipartBody.Bytes())
defer resp.Body.Close()// 4. GET 无 body,contentType 传空
resp, err := client.DoRaw(ctx, http.MethodGet, "/v1/users", "", nil)
defer resp.Body.Close()自定义 HTTP Client
import "net/http"
import "crypto/tls"
cli := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
MaxIdleConns: 100,
},
}
client := hmacauth.MustNewClient(conf, hmacauth.WithClient(cli))在 go-zero 中使用
// internal/config/config.go
type Config struct {
rest.RestConf
Kong hmacauth.Conf
}# etc/config.yaml
Name: order-api
Host: 0.0.0.0
Port: 8888
Kong:
Host: https://api.example.com
Username: alice
Secret: my-secret
# Algorithm 和 Headers 可省略,使用默认值
# Algorithm: hmac-sha256
# Headers:
# - date
# - host
# - "@request-target"
# - digest// internal/svc/servicecontext.go
type ServiceContext struct {
Config config.Config
Kong hmacauth.Client
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
Kong: hmacauth.MustNewClient(c.Kong),
}
}// internal/logic/createuserlogic.go
func (l *CreateUserLogic) CreateUser(req *types.CreateUserReq) error {
resp, err := l.svcCtx.Kong.Do(l.ctx, http.MethodPost, "/v1/users", &struct {
Name string `json:"name"`
Email string `json:"email"`
}{
Name: req.Name,
Email: req.Email,
})
if err != nil {
return err
}
defer resp.Body.Close()
// 处理响应...
return nil
}独立脚本中使用
package main
import (
"context"
"fmt"
"net/http"
"time"
"github.com/lerity-yao/czt-contrib/kong/hmacauth"
)
func main() {
client := hmacauth.MustNewClient(hmacauth.Conf{
Host: "https://api.example.com",
Username: "alice",
Secret: "my-secret",
Algorithm: hmacauth.AlgorithmHmacSHA256,
})
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// GET 请求
resp, err := client.Do(ctx, http.MethodGet, "/v1/users", nil)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println("Status:", resp.StatusCode)
// POST JSON 请求
resp, err = client.Do(ctx, http.MethodPost, "/v1/users", &struct {
Name string `json:"name"`
}{Name: "tom"})
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result struct {
ID int `json:"id"`
}
if err := hmacauth.Parse(resp, err, &result); err != nil {
panic(err)
}
fmt.Println("Created user ID:", result.ID)
}更新日志
查看 CHANGELOG.md