From f94df2bdbb83b7d294834baf1c50aeacaf4490ff Mon Sep 17 00:00:00 2001 From: SF-bytebytebrew Date: Wed, 21 May 2025 12:52:35 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E6=B7=BB=E5=8A=A0API=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=E5=B7=A5=E5=85=B7=E7=9A=84=E5=88=9D=E5=A7=8B=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=EF=BC=8C=E5=8C=85=E6=8B=AC=E6=A8=A1=E5=9D=97=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E3=80=81=E4=B8=BB=E7=A8=8B=E5=BA=8F=E3=80=81=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=E5=BA=93=E5=92=8C=E6=96=87=E6=A1=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/README.md | 237 +++++++++++++++++++++++++++++++++ go/apisign/signer.go | 309 +++++++++++++++++++++++++++++++++++++++++++ go/go.mod | 3 + go/go.sum | 0 go/main.go | 139 +++++++++++++++++++ 5 files changed, 688 insertions(+) create mode 100644 go/README.md create mode 100644 go/apisign/signer.go create mode 100644 go/go.mod create mode 100644 go/go.sum create mode 100644 go/main.go diff --git a/go/README.md b/go/README.md new file mode 100644 index 0000000..85f43b2 --- /dev/null +++ b/go/README.md @@ -0,0 +1,237 @@ +# API签名工具 - Go实现 + +## 项目结构 + +根据实际项目文件结构: + +```plaintext +go/ +├── main.go # 主程序入口 +├── apisign/ # 签名库核心代码目录 +│ └── signer.go # 签名工具实现 +├── go.mod # Go模块定义 +└── go.sum # 依赖版本锁定 +``` + +## 使用方法 + +### 构建项目 + +```bash +go build -o apisign +``` + +### 运行命令行工具 + +```bash +./apisign [选项] +``` + +### 命令行选项 + +| 选项 | 描述 | +|------|------| +| `-a, --algorithm` | 签名算法: md5, sha1, sha256, hmac-sha256 | +| `-u, --url` | API基础URL | +| `-p, --param` | 请求参数,格式为key=value,可多次使用 | +| `-k, --key` | 访问密钥ID | +| `-s, --secret` | 密钥 | +| `-c, --channel` | 合作渠道方ID | +| `-h, --help` | 显示帮助信息 | +| `-m, --mode` | 操作模式: url, params, verify | +| `-n, --nonce` | 自定义随机字符串 | +| `-t, --timestamp` | 自定义时间戳 | + +### 常用命令示例 + +**基本用法** + +```bash +./apisign +``` + +**自定义参数** + +```bash +./apisign \ + -u "https://api.example.com/user/info" \ + -p "userId=12345" -p "action=getInfo" \ + -k "YOUR_ACCESS_KEY" \ + -s "YOUR_SECRET_KEY" \ + -c "3" +``` + +**指定签名算法** + +```bash +./apisign -a sha256 +``` + +**生成签名参数** + +```bash +./apisign -m params -p "userId=12345" -p "action=getData" +``` + +**验证签名** + +```bash +./apisign -m verify -p "userId=12345" -p "action=getData" -p "AccessKeyId=test-key" -p "channelId=test-channel" -p "timestamp=1621234567890" -p "nonce=abc123" -p "sign=calculated-signature-here" +``` + +**帮助信息** + +```bash +./apisign --help +``` + +### API接口测试实例 + +使用真实API接口进行测试: + +```bash +# 未签名的API调用测试 - 返回错误 +curl "https://api-v1.sound-force.com:8443/p/album/single/media-url?channelId=3&singleId=381980" +# 返回: {"code":400,"data":null,"msg":"Missing AccessKeyId","success":false} + +# 生成访问https://api-v1.sound-force.com:8443/p/album/single/media-url的签名URL +./apisign \ + -a md5 \ + -u "https://api-v1.sound-force.com:8443/p/album/single/media-url" \ + -p "singleId=381980" \ + -k "YOUR_ACCESS_KEY" \ + -s "YOUR_SECRET_KEY" \ + -c "3" + +# 使用curl测试API接口 +signed_url=$(./apisign \ + -a md5 \ + -u "https://api-v1.sound-force.com:8443/p/album/single/media-url" \ + -p "singleId=381980" \ + -k "YOUR_ACCESS_KEY" \ + -s "YOUR_SECRET_KEY" \ + -c "3" | grep -A 1 "签名后的URL:" | tail -n 1) +curl -v "$signed_url" +``` + +请注意: + +- 替换`YOUR_ACCESS_KEY`为实际的访问密钥ID +- 替换`YOUR_SECRET_KEY`为实际的密钥 +- 示例使用的渠道ID为`3`,请根据实际情况调整 + +使用有效的密钥和签名后,API接口将返回成功响应(状态码200)并提供媒体URL数据。 + +### Go HTTP客户端测试示例 + +```go +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "os/exec" + "strings" +) + +func main() { + // 获取签名URL + cmd := exec.Command("./apisign", + "-a", "md5", + "-u", "https://api-v1.sound-force.com:8443/p/album/single/media-url", + "-p", "singleId=381980", + "-k", "YOUR_ACCESS_KEY", + "-s", "YOUR_SECRET_KEY", + "-c", "3") + output, err := cmd.CombinedOutput() + if err != nil { + panic(err) + } + + // 从输出中提取URL + lines := strings.Split(string(output), "\n") + var url string + for i, line := range lines { + if strings.Contains(line, "签名后的URL:") && i+1 < len(lines) { + url = strings.TrimSpace(lines[i+1]) + break + } + } + + if url == "" { + panic("无法从输出中获取签名URL") + } + + // 发送请求 + resp, err := http.Get(url) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + fmt.Printf("状态码: %d\n", resp.StatusCode) + fmt.Printf("响应内容: %s\n", body) +} +``` + +### 代码集成 + +```go +package main + +import ( + "fmt" + "github.com/soundforce/apisign" +) + +func main() { + // 创建签名选项 + options := apisign.NewOptions(). + WithAlgorithm(apisign.MD5) + + // 创建签名工具 + signer := apisign.NewSigner(options) + + // 准备请求参数 + params := map[string]string{ + "singleId": "381980", + } + + // 执行签名 + signedParams, err := signer.SignRequest( + params, + "YOUR_ACCESS_KEY", + "YOUR_SECRET_KEY", + "3", + ) + if err != nil { + panic(err) + } + + // 或签名URL + signedURL, err := signer.SignURL( + "https://api-v1.sound-force.com:8443/p/album/single/media-url", + params, + "YOUR_ACCESS_KEY", + "YOUR_SECRET_KEY", + "3", + ) + if err != nil { + panic(err) + } + + fmt.Println(signedURL) +} +``` + +### 环境变量 + +该工具支持从`.env`文件加载以下配置: + +- `ACCESS_KEY_ID`: 访问密钥ID +- `SECRET_KEY`: 密钥 +- `CHANNEL_ID`: 渠道ID +- `SIGN_ALGORITHM`: 签名算法 +- `API_BASE_URL`: API基础URL diff --git a/go/apisign/signer.go b/go/apisign/signer.go new file mode 100644 index 0000000..eaf30ed --- /dev/null +++ b/go/apisign/signer.go @@ -0,0 +1,309 @@ +// 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 +} diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..862f0db --- /dev/null +++ b/go/go.mod @@ -0,0 +1,3 @@ +module sound-force/sign-doc/go + +go 1.20 diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/go/main.go b/go/main.go new file mode 100644 index 0000000..ba037dc --- /dev/null +++ b/go/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "flag" + "fmt" + "os" + "sound-force/sign-doc/go/apisign" + "strings" +) + +func main() { + var accessKeyId string + var secretKey string + var apiBaseURL string + var algorithmStr string + var params []string + + flag.StringVar(&accessKeyId, "k", os.Getenv("ACCESS_KEY_ID"), "访问密钥ID") + flag.StringVar(&secretKey, "s", os.Getenv("SECRET_KEY"), "密钥") + flag.StringVar(&apiBaseURL, "u", os.Getenv("API_BASE_URL"), "API基础URL") + flag.StringVar(&algorithmStr, "a", os.Getenv("SIGN_ALGORITHM"), "签名算法") + flag.Func("p", "请求参数 (格式: key=value)", func(s string) error { + params = append(params, s) + return nil + }) + flag.Parse() + + if accessKeyId == "" { + accessKeyId = "test-access-key-id" + } + if secretKey == "" { + secretKey = "test-secret-key" + } + if apiBaseURL == "" { + apiBaseURL = "https://api.example.com/v1" + } + if algorithmStr == "" { + algorithmStr = "MD5" + } + + algorithm, err := apisign.ParseAlgorithm(algorithmStr) + if err != nil { + fmt.Printf("Warning: %v, using MD5 as default\n", err) + algorithm = apisign.MD5 + } + + options := apisign.NewDefaultSignOptions() + options.Algorithm = algorithm + // 将签名参数名从默认的"signature"改为"sign" + options.SignatureName = "sign" + + signer := apisign.NewAPISigner(options) + + requestParams := make(map[string]string) + if len(params) > 0 { + for _, p := range params { + parts := strings.SplitN(p, "=", 2) + if len(parts) == 2 { + requestParams[parts[0]] = parts[1] + } + } + } else { + requestParams = map[string]string{ + "userId": "12345", + "action": "getData", + "data": "测试数据", + } + } + + fmt.Println("===================== API签名示例 =====================") + fmt.Printf("AccessKeyId: %s\n", accessKeyId) + fmt.Printf("SecretKey: %s\n", secretKey) + fmt.Printf("签名算法: %s\n", algorithm.String()) + fmt.Printf("基础URL: %s\n", apiBaseURL) + fmt.Println("请求参数:", requestParams) + + channelId := getEnv("CHANNEL_ID", "test-channel-id") + if val, ok := requestParams["channelId"]; ok { + channelId = val + } + + signedParams := signer.SignRequest(requestParams, accessKeyId, secretKey, channelId) + fmt.Println("\n签名后的参数:") + for k, v := range signedParams { + fmt.Printf(" %s: %s\n", k, v) + } + + signedURL := signer.SignURL(apiBaseURL, requestParams, accessKeyId, secretKey, channelId) + fmt.Println("\n签名后的URL:") + fmt.Println(signedURL) + + valid, err := signer.VerifySignature(signedParams, secretKey, 300000) + fmt.Println("\n签名验证结果:") + if err != nil { + fmt.Printf(" 验证失败: %v\n", err) + } else if valid { + fmt.Println(" 验证成功") + } else { + fmt.Println(" 验证失败: 签名不匹配") + } + + fmt.Println("\n不同算法的签名结果:") + demonstrateAlgorithms(requestParams, accessKeyId, secretKey) +} + +// getEnv 获取环境变量,如果不存在则返回默认值 +func getEnv(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} + +// demonstrateAlgorithms 演示不同算法的签名结果 +func demonstrateAlgorithms(params map[string]string, accessKeyId, secretKey string) { + algorithms := []apisign.SignatureAlgorithm{ + apisign.MD5, + apisign.SHA1, + apisign.SHA256, + apisign.HMACSHA256, + } + + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + strings.Join(keys, "&") + + for _, alg := range algorithms { + options := apisign.NewDefaultSignOptions() + options.Algorithm = alg + options.SignatureName = "sign" + signer := apisign.NewAPISigner(options) + + signature := signer.CalculateSignature(params, secretKey) + fmt.Printf(" %s: %s\n", alg.String(), signature) + } +}