akm

package module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 20, 2026 License: MIT Imports: 20 Imported by: 0

README

AK/SK 鉴权包

基于 HMAC-SHA256 的请求签名鉴权系统,提供身份认证、请求防篡改、防重放攻击。

Go Version License EN

架构

┌──────────────────────────────┐
│ middleware.go (Gin)          │  ← 集成层:提取 Header → 调用 Verifier
├──────────────────────────────┤
│ verify.go / sign.go          │  ← 引擎层:纯逻辑,零外部依赖
├──────────────────────────────┤
│ types.go (接口) / nonce.go (实现) │  ← 存储层:接口抽象 + Redis/Memory 实现
└──────────────────────────────┘

快速开始

安装
go get github.com/Iristack/accesskey-manager
5 分钟接入

服务端 — 初始化 Verifier 并注册 Gin 中间件:

// 1. 实现 SKStore(从数据库查 SK)
type mySKStore struct{ db *gorm.DB }
func (m *mySKStore) GetSK(ctx context.Context, ak string) (string, error) {
    // SELECT app_secret FROM config_signature WHERE app_id = ?
}

// 2. 初始化
skStore := akm.NewCachedSKStore(&mySKStore{db}, 60*time.Second, 0)
nonceStore := akm.NewRedisNonceStore(redisClient)
verifier := akm.NewVerifier(skStore, nonceStore, akm.Config{TimeWindow: 5 * time.Minute})

// 3. 注册中间件
api := router.Group("/api/open/v1")
api.Use(akm.GinAuth(verifier))

客户端 — 生成签名并发送请求:

ak, sk := "your-ak", "your-sk"
body := []byte(`{"job_sn":"JOB-001"}`)
timestamp := time.Now().Unix()
nonce := uuid.New().String()

strToSign := akm.BuildStringToSign("POST", "/api/trigger", "", body, timestamp, nonce, nil)
sig := akm.Sign(sk, strToSign)

req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
req.Header.Set("X-AK", ak)
req.Header.Set("X-Timestamp", strconv.FormatInt(timestamp, 10))
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Signature", sig)

签名算法

StringToSign = Method        + "\n"
             + Path          + "\n"
             + SortedQuery        + "\n"
             + Hex(SHA256(Body)) + "\n"
             + Timestamp          + "\n"
             + Nonce
             + ExtendFields...  // 可选,按 key 字典序追加

Signature = Hex(HMAC-SHA256(SK, StringToSign))
签名示例
输入:
  AK        = "a1b2c3d4e5f6a7b8c9d0"
  SK        = "e3b0c44298fc1c149afbf4c8996fb924..."
  Method    = "POST"
  Path      = "/api/v1/jobs/trigger"
  RawQuery  = "page=1&size=10"
  Body      = {"job_sn":"JOB-2024-001"}
  Timestamp = 1716123456
  Nonce     = "x7k9m2p4-v8n1-r5q3-t6w0-y2a4b6c8d0e1"

Step 1: Query 字典序排序 → "page=1&size=10"
Step 2: Body SHA256      → "a7f5c8e3..."
Step 3: 拼接待签名字符串
Step 4: HMAC-SHA256(SK)  → "8f3a2e1b..."

请求 Header:
  X-AK: a1b2c3d4e5f6a7b8c9d0
  X-Timestamp: 1716123456
  X-Nonce: x7k9m2p4-v8n1-r5q3-t6w0-y2a4b6c8d0e1
  X-Signature: 8f3a2e1b...

验签流程

客户端                                服务端
  │                                     │
  │  POST /api/xxx                      │
  │  X-AK: {ak}                         │
  │  X-Timestamp: {ts} ──────────────→  1. 提取 Header
  │  X-Nonce: {nonce}                   2. 根据 AK 查 SK (SKStore)
  │  X-Signature: {sig}                 3. 校验 |now - ts| ≤ TimeWindow
  │                                     4. 重算签名 → ConstantTimeCompare
  │                                     5. Nonce 原子检查 (NonceStore)
  │                                     6. 通过 → 放行

API 参考

核心接口
type SKStore interface {
    GetSK(ctx context.Context, ak string) (sk string, err error)
}

type NonceStore interface {
    CheckAndSet(ctx context.Context, nonce string, ttl time.Duration) (exists bool, err error)
}
AuthHeaders

绕过 GinAuth 直接调用 Verifier.Verify() 时需要传入此结构体:

headers := akm.AuthHeaders{
    AK:           c.GetHeader("X-AK"),
    Timestamp:    timestamp,
    Nonce:        c.GetHeader("X-Nonce"),
    Signature:    c.GetHeader("X-Signature"),
    ExtendFields: map[string]string{"appcode": c.GetHeader("X-AppCode")},
}
err := verifier.Verify(ctx, headers, method, path, sortedQuery, body)
默认配置
cfg := akm.DefaultConfig() // TimeWindow: 5 分钟
verifier := akm.NewVerifier(skStore, nonceStore, cfg) // 与 DefaultConfig 对齐

DefaultConfig() 返回与 NewVerifier 默认值一致的配置,避免时间窗口不一致。

密钥生成
kp, err := akm.GenerateKeyPair()
// kp.AK — 20 字符 hex
// kp.SK — 64 字符 hex(绝不打日志、不传输、不硬编码)
Query 排序缓存

GinAuth 中间件默认使用 SortQueryCached() 对 query 参数排序,基于 LRU 缓存(容量 1024),并发安全。高频 API 场景下可消除重复排序开销。如需直接使用:

sortedQuery := akm.SortQueryCached(c.Request.URL.RawQuery) // 缓存版
sortedQuery := akm.SortQuery(rawQuery)                     // 无缓存版
内建实现
接口 实现 说明
SKStore CachedSKStore 本地缓存装饰器,TTL 可配(常用 60s),零 DB 穿透
NonceStore RedisNonceStore 基于 Redis SETNX 原子操作
NonceStore MemoryNonceStore 本地内存降级方案(含后台 GC)
拓展字段

通过 ExtendFields 将业务 Header 纳入签名,防止篡改:

verifier := akm.NewVerifier(skStore, nonceStore, akm.Config{
    TimeWindow: 5 * time.Minute,
    ExtendFields: map[string]string{
        "appcode": "X-AppCode",
    },
})
  • 拓展字段按 field_name 字典序追加到 StringToSign
  • 中间件自动从对应 Header 提取,缺失则返回 401
  • 键/值含 \n \r 的字段自动跳过
Gin 中间件
// 一行接入
api.Use(akm.GinAuth(verifier))

中间件自动完成:Header 提取 → 时间戳校验 → Body 读取 → 拓展字段提取 → 签名验证 → Nonce 防重放。

错误码

错误 HTTP 含义
ErrInvalidAK 401 AK 不存在
ErrTimestampExpired 401 时间戳超出允许窗口
ErrNonceReplayed 401 Nonce 已被使用(重放攻击)
ErrSignatureMismatch 401 签名不匹配(篡改或 SK 错误)
缺少鉴权头 401 必需 Header 缺失
X-Timestamp 格式错误 401 非 Unix 秒级时间戳
请求体读取失败 500 Body 不可读
请求体过大 413 超过 10 MB

安全机制

目标 机制 细节
身份真实性 AK/SK 签名 只有持有正确 SK 的客户端才能生成合法签名
数据完整性 全参数签名 Method、Path、Query、Body 均参与签名
防重放 Timestamp + Nonce 时间窗口 + Redis 原子 SETNX
SK 不传输 仅传签名结果 截获也无法逆推 SK
防时序攻击 ConstantTimeCompare crypto/subtle 固定时间比较
防注入 \n \r 过滤 拓展字段自动跳过含换行符的值

最佳实践

时钟同步

客户端和服务端均使用 NTP 同步。默认时间窗口 5 分钟,可通过 Config.TimeWindow 调整。Nonce TTL = TimeWindow × 2,确保请求在窗口边界处不会因 Nonce 过期而失败。

请求体大小

中间件限制 Body 最大 10 MB,超过返回 413。读取采用双路径策略:已知 Content-Length 时精准预分配一次读取;无法确定大小时回退 LimitReader 流式读取,最后通过 NopCloser(Buffer) 重置 Body 供后续 handler 复用。建议客户端控制在 1 MB 以内。大文件传输使用对象存储预签名 URL。

Query 参数排序约定

SortQuery 对原始 query 字符串按 & 分割后以 ASCII 字典序排序。客户端和服务端必须使用相同的原始编码(percent-encoding),不要在签名前对 query 进行 URL-decode。

SK 管理
  • 绝不硬编码 — 从环境变量、KMS 或密钥管理服务读取
  • 绝不打日志 — 不在日志、错误消息、追踪系统中输出 SK
  • 定期轮换 — 定期调用 GenerateKeyPair() 轮换,旧 SK 保留一个时间窗口。轮换后调用 cachedStore.Invalidate(ak) 主动失效本地缓存,避免不一致窗口。
生产部署 Checklist
  • 实现 SKStore 接口(从数据库/KMS 查询 SK)
  • 使用 CachedSKStore 装饰器缓存 SK(减少 DB 查询)
  • 配置 NonceStore(Redis 优先,MemoryNonceStore 降级)
  • 注册 GinAuth 中间件到需鉴权的路由组
  • (可选)启用速率限制
  • 监控鉴权失败率和 Redis 连接状态

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidAK         = errors.New("akm: AK 不存在")
	ErrTimestampExpired  = errors.New("akm: 时间戳已过期")
	ErrNonceReplayed     = errors.New("akm: Nonce 已被使用(重放攻击)")
	ErrSignatureMismatch = errors.New("akm: 签名不匹配")
)

验签错误类型。

Functions

func BuildStringToSign

func BuildStringToSign(method, path, sortedQuery string, body []byte, timestamp int64, nonce string, extendFields map[string]string) string

BuildStringToSign 按规范拼接待签名字符串。

格式: Method + "\n" + Path + "\n" + SortedQuery + "\n" + Hex(SHA256(Body)) + "\n" + Timestamp + "\n" + Nonce
      + "\n" + ExtendField1Name=ExtendField1Value + ... (按 field_name 字典序)

func GinAuth

func GinAuth(verifier *Verifier) gin.HandlerFunc

GinAuth 返回 Gin 中间件,对每个请求执行 AK/SK 验签。 验证失败时返回 401 并中断请求链。

func Sign

func Sign(sk, stringToSign string) string

Sign 使用 SK 对 stringToSign 进行 HMAC-SHA256 签名,返回 hex 字符串。

func SortQuery

func SortQuery(rawQuery string) string

SortQuery 将原始 query 字符串按 key 字典序排序后返回。 输入如 "c=3&a=1&b=2",输出 "a=1&b=2&c=3"。 空字符串或仅含 "=" 的 key 会保留。

func SortQueryCached

func SortQueryCached(rawQuery string) string

SortQueryCached 带 LRU 缓存的 SortQuery。对于高频重复 query 参数组合的 API, 缓存命中时零分配,避免每次 strings.Split + sort.Strings + strings.Join。

Types

type AuthHeaders

type AuthHeaders struct {
	AK           string
	Timestamp    int64
	Nonce        string
	Signature    string
	ExtendFields map[string]string // 拓展字段:field_name → field_value,如 {"appcode": "my-app"}
}

AuthHeaders 客户端发送的鉴权头。

type CachedSKStore

type CachedSKStore struct {
	// contains filtered or unexported fields
}

CachedSKStore 包装 SKStore,提供本地内存缓存。 缓存命中时零网络开销,TTL 内 SK 变更存在不一致窗口。

func NewCachedSKStore

func NewCachedSKStore(inner SKStore, ttl time.Duration, maxSize int) *CachedSKStore

NewCachedSKStore 创建缓存装饰器。maxSize 限制缓存条目数(0 表示不限制)。

func (*CachedSKStore) GetSK

func (c *CachedSKStore) GetSK(ctx context.Context, ak string) (string, error)

func (*CachedSKStore) Invalidate

func (c *CachedSKStore) Invalidate(ak string)

Invalidate 主动清除指定 AK 的缓存(SK 变更时调用)。

type Config

type Config struct {
	TimeWindow   time.Duration     // 允许的时间偏移,默认 5 分钟
	ExtendFields map[string]string // 拓展字段:field_name → header_name,如 {"appcode": "X-AppCode"}
}

Config 验签引擎配置。

func DefaultConfig

func DefaultConfig() Config

DefaultConfig 返回默认配置(5 分钟时间窗口)。

type KeyPair

type KeyPair struct {
	AK string // 20 字符 hex(10 字节随机)
	SK string // 64 字符 hex(32 字节随机)
}

KeyPair AK/SK 密钥对。

func GenerateKeyPair

func GenerateKeyPair() (*KeyPair, error)

GenerateKeyPair 生成新的 AK/SK 密钥对。 AK = 20 字符 hex(10 字节随机),SK = 64 字符 hex(32 字节随机)。

type MemoryNonceStore

type MemoryNonceStore struct {
	// contains filtered or unexported fields
}

MemoryNonceStore 基于本地内存的 NonceStore 实现。 用于 Redis 不可用时的降级方案。 注意:分布式部署下各实例 Nonce 不共享,但 Nonce 碰撞概率可忽略,且仍受时间窗口约束。

func NewMemoryNonceStore

func NewMemoryNonceStore() *MemoryNonceStore

NewMemoryNonceStore 创建本地内存 Nonce 存储实例,并启动后台 GC 协程。

func (*MemoryNonceStore) CheckAndSet

func (m *MemoryNonceStore) CheckAndSet(ctx context.Context, nonce string, ttl time.Duration) (bool, error)

CheckAndSet 原子检查并设置 nonce。

type NonceStore

type NonceStore interface {
	// CheckAndSet 原子操作:如果 nonce 已存在返回 true(重放攻击),否则设置并返回 false。
	CheckAndSet(ctx context.Context, nonce string, ttl time.Duration) (exists bool, err error)
}

NonceStore 提供 Nonce 防重放的原子检查与存储能力。

type RedisNonceStore

type RedisNonceStore struct {
	// contains filtered or unexported fields
}

RedisNonceStore 基于 Redis 的 NonceStore 实现。 使用 SET key value NX EX ttl 原子命令保证并发安全。

func NewRedisNonceStore

func NewRedisNonceStore(client *redis.Client) *RedisNonceStore

NewRedisNonceStore 创建 Redis Nonce 存储实例。

func (*RedisNonceStore) CheckAndSet

func (r *RedisNonceStore) CheckAndSet(ctx context.Context, nonce string, ttl time.Duration) (bool, error)

CheckAndSet 原子检查并设置 nonce。 若 nonce 已存在(重放)返回 (true, nil);首次出现则设置并返回 (false, nil)。

type SKStore

type SKStore interface {
	GetSK(ctx context.Context, ak string) (sk string, err error)
}

SKStore 提供根据 AK 查询 SK 的能力。

type Verifier

type Verifier struct {
	// contains filtered or unexported fields
}

Verifier AK/SK 请求验签器。

func NewVerifier

func NewVerifier(skStore SKStore, nonceStore NonceStore, cfg Config) *Verifier

NewVerifier 创建验签器实例。

func (*Verifier) Verify

func (v *Verifier) Verify(ctx context.Context, headers AuthHeaders, method, path, sortedQuery string, body []byte) error

Verify 执行完整的验签流程:查 SK → 检查时间戳 → 比对签名 → 防重放。

sortedQuery 应为按 key 字典序排序后的 query 字符串(可通过 SortQuery 获得)。 body 为原始请求体字节(未经任何处理)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL