feat: 引入签名认证、加密工具包及大量goctl代码生成模板,并更新API、Admin和Node服务逻辑。

This commit is contained in:
2026-02-27 08:49:37 -08:00
parent 9dacd85a89
commit e1896f677c
166 changed files with 2913 additions and 489 deletions
+51
View File
@@ -0,0 +1,51 @@
package signature
import (
"crypto/sha256"
"fmt"
"net/url"
"sort"
"strings"
)
// BuildStringToSign 构造待签字符串
// StringToSign = METHOD\nPATH\nCANONICAL_QUERY\nBODY_SHA256\nX-App-Id\nX-Timestamp\nX-Nonce
func BuildStringToSign(method, path, rawQuery string, body []byte, appId, timestamp, nonce string) string {
canonical := canonicalQuery(rawQuery)
bodyHash := sha256Hex(body)
parts := []string{
strings.ToUpper(method),
path,
canonical,
bodyHash,
appId,
timestamp,
nonce,
}
return strings.Join(parts, "\n")
}
func canonicalQuery(rawQuery string) string {
if rawQuery == "" {
return ""
}
values, err := url.ParseQuery(rawQuery)
if err != nil {
return rawQuery
}
keys := make([]string, 0, len(values))
for k := range values {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, fmt.Sprintf("%s=%s", k, values.Get(k)))
}
return strings.Join(parts, "&")
}
func sha256Hex(data []byte) string {
h := sha256.Sum256(data)
return fmt.Sprintf("%x", h)
}
+7
View File
@@ -0,0 +1,7 @@
package signature
type SignatureConf struct {
AppSecrets map[string]string
ValidWindowSeconds int64 // 默认 300
SkipPrefixes []string
}
+10
View File
@@ -0,0 +1,10 @@
package signature
import "errors"
var (
ErrSignatureMissing = errors.New("signature missing")
ErrSignatureExpired = errors.New("signature expired")
ErrSignatureInvalid = errors.New("signature invalid")
ErrSignatureReplay = errors.New("signature replay")
)
+32
View File
@@ -0,0 +1,32 @@
package signature
import (
"context"
"github.com/zeromicro/go-zero/core/stores/redis"
)
// NonceStore 防重放存储接口
type NonceStore interface {
// SetIfNotExists 返回 (true, nil) 表示 key 已存在(重放攻击)
SetIfNotExists(ctx context.Context, appId, nonce string, ttlSeconds int64) (bool, error)
}
type RedisNonceStore struct {
client *redis.Redis
}
func NewRedisNonceStore(client *redis.Redis) *RedisNonceStore {
return &RedisNonceStore{client: client}
}
func (s *RedisNonceStore) SetIfNotExists(_ context.Context, appId, nonce string, ttlSeconds int64) (bool, error) {
key := "sig:nonce:" + appId + ":" + nonce
// go-zero redis.Redis.SetNX 返回 true 表示设置成功(首次)
ok, err := s.client.SetnxEx(key, "1", int(ttlSeconds))
if err != nil {
return false, err
}
// SetnxEx ok=true 首次,ok=false 已存在(重放)
return !ok, nil
}
+105
View File
@@ -0,0 +1,105 @@
package signature
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"testing"
"time"
)
// mockNonceStore 内存实现,用于测试
type mockNonceStore struct {
seen map[string]bool
}
func newMockStore() *mockNonceStore {
return &mockNonceStore{seen: map[string]bool{}}
}
func (m *mockNonceStore) SetIfNotExists(_ context.Context, appId, nonce string, _ int64) (bool, error) {
key := appId + ":" + nonce
if m.seen[key] {
return true, nil
}
m.seen[key] = true
return false, nil
}
func makeSignature(secret, stringToSign string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(stringToSign))
return hex.EncodeToString(mac.Sum(nil))
}
func TestValidate_Success(t *testing.T) {
conf := SignatureConf{
AppSecrets: map[string]string{"web-client": "uB4G,XxL2{7b"},
ValidWindowSeconds: 300,
}
v := NewValidator(conf, newMockStore())
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := fmt.Sprintf("%x", time.Now().UnixNano())
sts := BuildStringToSign("POST", "/api/v1/order", "", []byte(`{"plan_id":1}`), "web-client", ts, nonce)
sig := makeSignature("uB4G,XxL2{7b", sts)
if err := v.Validate(context.Background(), "web-client", ts, nonce, sig, sts); err != nil {
t.Fatal(err)
}
}
func TestValidate_Expired(t *testing.T) {
conf := SignatureConf{
AppSecrets: map[string]string{"web-client": "uB4G,XxL2{7b"},
ValidWindowSeconds: 300,
}
v := NewValidator(conf, newMockStore())
ts := strconv.FormatInt(time.Now().Unix()-400, 10) // 已过期 400s
nonce := "abc"
sts := BuildStringToSign("GET", "/api/v1/user/info", "", nil, "web-client", ts, nonce)
sig := makeSignature("uB4G,XxL2{7b", sts)
if err := v.Validate(context.Background(), "web-client", ts, nonce, sig, sts); err != ErrSignatureExpired {
t.Fatalf("expected ErrSignatureExpired, got %v", err)
}
}
func TestValidate_Replay(t *testing.T) {
conf := SignatureConf{
AppSecrets: map[string]string{"web-client": "uB4G,XxL2{7b"},
ValidWindowSeconds: 300,
}
store := newMockStore()
v := NewValidator(conf, store)
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := "same-nonce-replay"
sts := BuildStringToSign("GET", "/api/v1/user/info", "", nil, "web-client", ts, nonce)
sig := makeSignature("uB4G,XxL2{7b", sts)
_ = v.Validate(context.Background(), "web-client", ts, nonce, sig, sts)
if err := v.Validate(context.Background(), "web-client", ts, nonce, sig, sts); err != ErrSignatureReplay {
t.Fatalf("expected ErrSignatureReplay, got %v", err)
}
}
func TestValidate_InvalidSignature(t *testing.T) {
conf := SignatureConf{
AppSecrets: map[string]string{"web-client": "uB4G,XxL2{7b"},
ValidWindowSeconds: 300,
}
v := NewValidator(conf, newMockStore())
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := "nonce-invalid-sig"
sts := BuildStringToSign("POST", "/api/v1/order", "", []byte(`{"plan_id":1}`), "web-client", ts, nonce)
if err := v.Validate(context.Background(), "web-client", ts, nonce, "badsignature", sts); err != ErrSignatureInvalid {
t.Fatalf("expected ErrSignatureInvalid, got %v", err)
}
}
+64
View File
@@ -0,0 +1,64 @@
package signature
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"time"
)
// Validator 验证签名
type Validator struct {
conf SignatureConf
nonceStore NonceStore
}
func NewValidator(conf SignatureConf, store NonceStore) *Validator {
return &Validator{conf: conf, nonceStore: store}
}
func (v *Validator) windowSeconds() int64 {
if v.conf.ValidWindowSeconds <= 0 {
return 300
}
return v.conf.ValidWindowSeconds
}
// Validate 验证请求签名,返回具体错误便于调用方映射到 xerr 错误码
func (v *Validator) Validate(ctx context.Context, appId, timestamp, nonce, signature, stringToSign string) error {
// 1. appId 是否存在
secret, ok := v.conf.AppSecrets[appId]
if !ok {
return ErrSignatureMissing
}
// 2. 时间窗口
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return ErrSignatureExpired
}
diff := time.Now().Unix() - ts
if diff < 0 {
diff = -diff
}
if diff > v.windowSeconds() {
return ErrSignatureExpired
}
// 3. Nonce 防重放(Redis 不可用时降级,仅时间窗口)
replayed, err := v.nonceStore.SetIfNotExists(ctx, appId, nonce, v.windowSeconds()+60)
if err == nil && replayed {
return ErrSignatureReplay
}
// 4. HMAC 验签(防时序攻击)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(stringToSign))
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(signature)) {
return ErrSignatureInvalid
}
return nil
}