310 lines
7.2 KiB
Go
310 lines
7.2 KiB
Go
// Package apisign 提供API签名计算与验证功能
|
||
package apisign
|
||
|
||
import (
|
||
"crypto/hmac"
|
||
"crypto/md5"
|
||
"crypto/sha1"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"net/url"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// SignatureAlgorithm 签名算法类型
|
||
type SignatureAlgorithm int
|
||
|
||
const (
|
||
// MD5算法(默认、最快)
|
||
MD5 SignatureAlgorithm = iota
|
||
// SHA1算法
|
||
SHA1
|
||
// SHA256算法
|
||
SHA256
|
||
// HMACSHA256算法(最安全)
|
||
HMACSHA256
|
||
)
|
||
|
||
// String 返回算法的字符串表示
|
||
func (a SignatureAlgorithm) String() string {
|
||
switch a {
|
||
case MD5:
|
||
return "MD5"
|
||
case SHA1:
|
||
return "SHA1"
|
||
case SHA256:
|
||
return "SHA256"
|
||
case HMACSHA256:
|
||
return "HMAC-SHA256"
|
||
default:
|
||
return "UNKNOWN"
|
||
}
|
||
}
|
||
|
||
// ParseAlgorithm 从字符串解析算法类型
|
||
func ParseAlgorithm(s string) (SignatureAlgorithm, error) {
|
||
switch strings.ToUpper(s) {
|
||
case "MD5":
|
||
return MD5, nil
|
||
case "SHA1":
|
||
return SHA1, nil
|
||
case "SHA256":
|
||
return SHA256, nil
|
||
case "HMAC_SHA256", "HMACSHA256", "HMAC-SHA256":
|
||
return HMACSHA256, nil
|
||
default:
|
||
return MD5, fmt.Errorf("unknown algorithm: %s", s)
|
||
}
|
||
}
|
||
|
||
// SignOptions 签名选项
|
||
type SignOptions struct {
|
||
// 签名算法
|
||
Algorithm SignatureAlgorithm
|
||
// AccessKeyId参数名
|
||
KeyName string
|
||
// ChannelId参数名
|
||
ChannelIdName string
|
||
// 时间戳参数名
|
||
TimestampName string
|
||
// 随机字符串参数名
|
||
NonceName string
|
||
// 签名参数名
|
||
SignatureName string
|
||
}
|
||
|
||
// NewDefaultSignOptions 创建默认签名选项
|
||
func NewDefaultSignOptions() *SignOptions {
|
||
return &SignOptions{
|
||
Algorithm: MD5,
|
||
KeyName: "AccessKeyId",
|
||
ChannelIdName: "channelId",
|
||
TimestampName: "timestamp",
|
||
NonceName: "nonce",
|
||
SignatureName: "signature",
|
||
}
|
||
}
|
||
|
||
// APISigner API签名工具
|
||
type APISigner struct {
|
||
options *SignOptions
|
||
}
|
||
|
||
// NewAPISigner 创建API签名工具
|
||
//
|
||
// 参数:
|
||
// - options: 签名选项,如为nil则使用默认选项
|
||
//
|
||
// 返回:
|
||
// - 签名工具实例
|
||
func NewAPISigner(options *SignOptions) *APISigner {
|
||
if options == nil {
|
||
options = NewDefaultSignOptions()
|
||
}
|
||
return &APISigner{options: options}
|
||
}
|
||
|
||
// GenerateNonce 生成随机字符串
|
||
//
|
||
// 返回:
|
||
// - 一个基于当前时间的随机字符串
|
||
func (s *APISigner) GenerateNonce() string {
|
||
return fmt.Sprintf("%d%d", time.Now().UnixNano(), time.Now().Unix()%1000)
|
||
}
|
||
|
||
// GetTimestamp 获取当前时间戳(毫秒)
|
||
//
|
||
// 返回:
|
||
// - 当前的Unix时间戳(毫秒)
|
||
func (s *APISigner) GetTimestamp() int64 {
|
||
return time.Now().UnixMilli()
|
||
}
|
||
|
||
// SignRequest 对请求进行签名
|
||
//
|
||
// 参数:
|
||
// - params: 请求参数
|
||
// - accessKeyId: 访问密钥ID
|
||
// - secretKey: 密钥
|
||
// - channelId: 渠道ID,如果为空则使用params中已有的channelId
|
||
//
|
||
// 返回:
|
||
// - 添加了签名的完整参数
|
||
func (s *APISigner) SignRequest(params map[string]string, accessKeyId, secretKey string, channelId ...string) map[string]string {
|
||
signParams := make(map[string]string)
|
||
for k, v := range params {
|
||
signParams[k] = v
|
||
}
|
||
|
||
timestamp := s.GetTimestamp()
|
||
signParams[s.options.KeyName] = accessKeyId
|
||
|
||
if len(channelId) > 0 && channelId[0] != "" {
|
||
signParams[s.options.ChannelIdName] = channelId[0]
|
||
}
|
||
|
||
signParams[s.options.TimestampName] = strconv.FormatInt(timestamp, 10)
|
||
signParams[s.options.NonceName] = s.GenerateNonce()
|
||
|
||
signature := s.CalculateSignature(signParams, secretKey)
|
||
|
||
signParams[s.options.SignatureName] = signature
|
||
|
||
return signParams
|
||
}
|
||
|
||
// SignURL 对URL进行签名
|
||
//
|
||
// 参数:
|
||
// - baseURL: 基础URL地址
|
||
// - params: 请求参数
|
||
// - accessKeyId: 访问密钥ID
|
||
// - secretKey: 密钥
|
||
// - channelId: 渠道ID,如果为空则使用params中已有的channelId
|
||
//
|
||
// 返回:
|
||
// - 添加了签名的完整URL
|
||
func (s *APISigner) SignURL(baseURL string, params map[string]string, accessKeyId, secretKey string, channelId ...string) string {
|
||
signedParams := s.SignRequest(params, accessKeyId, secretKey, channelId...)
|
||
|
||
values := url.Values{}
|
||
for k, v := range signedParams {
|
||
values.Add(k, v)
|
||
}
|
||
|
||
if strings.Contains(baseURL, "?") {
|
||
return baseURL + "&" + values.Encode()
|
||
}
|
||
|
||
return baseURL + "?" + values.Encode()
|
||
}
|
||
|
||
// CalculateSignature 计算签名
|
||
//
|
||
// 参数:
|
||
// - params: 请求参数
|
||
// - secretKey: 密钥
|
||
//
|
||
// 返回:
|
||
// - 签名字符串
|
||
func (s *APISigner) CalculateSignature(params map[string]string, secretKey string) string {
|
||
signingString := s.CreateSigningString(params)
|
||
|
||
signingString = signingString + "&key=" + secretKey
|
||
|
||
switch s.options.Algorithm {
|
||
case SHA1:
|
||
hasher := sha1.New()
|
||
hasher.Write([]byte(signingString))
|
||
return hex.EncodeToString(hasher.Sum(nil))
|
||
case SHA256:
|
||
hasher := sha256.New()
|
||
hasher.Write([]byte(signingString))
|
||
return hex.EncodeToString(hasher.Sum(nil))
|
||
case HMACSHA256:
|
||
hasher := hmac.New(sha256.New, []byte(secretKey))
|
||
hasher.Write([]byte(signingString))
|
||
return hex.EncodeToString(hasher.Sum(nil))
|
||
default:
|
||
hasher := md5.New()
|
||
hasher.Write([]byte(signingString))
|
||
return hex.EncodeToString(hasher.Sum(nil))
|
||
}
|
||
}
|
||
|
||
// CreateSigningString 创建用于签名的规范化字符串
|
||
//
|
||
// 参数:
|
||
// - params: 请求参数
|
||
//
|
||
// 返回:
|
||
// - 按键名排序并拼接的字符串
|
||
func (s *APISigner) CreateSigningString(params map[string]string) string {
|
||
sortedParams := make(map[string]string)
|
||
for k, v := range params {
|
||
if k != s.options.SignatureName {
|
||
sortedParams[k] = v
|
||
}
|
||
}
|
||
|
||
keys := make([]string, 0, len(sortedParams))
|
||
for k := range sortedParams {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
|
||
parts := make([]string, 0, len(keys))
|
||
for _, k := range keys {
|
||
v := sortedParams[k]
|
||
if needsURLEncode(v) {
|
||
v = url.QueryEscape(v)
|
||
}
|
||
parts = append(parts, k+"="+v)
|
||
}
|
||
|
||
return strings.Join(parts, "&")
|
||
}
|
||
|
||
// needsURLEncode 判断是否需要对字符串进行URL编码
|
||
func needsURLEncode(s string) bool {
|
||
for _, c := range s {
|
||
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// VerifySignature 验证签名
|
||
//
|
||
// 参数:
|
||
// - params: 所有请求参数,包括签名
|
||
// - secretKey: 密钥
|
||
// - maxAgeMs: 允许的最大时间差(毫秒)
|
||
//
|
||
// 返回:
|
||
// - 验证结果和错误信息
|
||
func (s *APISigner) VerifySignature(params map[string]string, secretKey string, maxAgeMs int64) (bool, error) {
|
||
_, ok := params[s.options.KeyName]
|
||
if !ok {
|
||
return false, fmt.Errorf("missing %s", s.options.KeyName)
|
||
}
|
||
|
||
_, ok = params[s.options.ChannelIdName]
|
||
if !ok {
|
||
return false, fmt.Errorf("missing %s", s.options.ChannelIdName)
|
||
}
|
||
|
||
timestampStr, ok := params[s.options.TimestampName]
|
||
if !ok {
|
||
return false, fmt.Errorf("missing %s", s.options.TimestampName)
|
||
}
|
||
|
||
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
|
||
if err != nil {
|
||
return false, fmt.Errorf("invalid timestamp: %v", err)
|
||
}
|
||
|
||
now := time.Now().UnixMilli()
|
||
if now-timestamp > maxAgeMs || timestamp-now > maxAgeMs {
|
||
return false, fmt.Errorf("timestamp expired")
|
||
}
|
||
|
||
_, ok = params[s.options.NonceName]
|
||
if !ok {
|
||
return false, fmt.Errorf("missing %s", s.options.NonceName)
|
||
}
|
||
|
||
providedSignature, ok := params[s.options.SignatureName]
|
||
if !ok {
|
||
return false, fmt.Errorf("missing %s", s.options.SignatureName)
|
||
}
|
||
|
||
expectedSignature := s.CalculateSignature(params, secretKey)
|
||
return expectedSignature == providedSignature, nil
|
||
}
|