65 lines
1.5 KiB
Go
65 lines
1.5 KiB
Go
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
|
||
}
|