Files
sign-doc/go/apisign/signer.go

310 lines
7.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
}