接口签名 - 一个实践过的方案(三)

接口签名的方案,请看签名的文章 接口签名 - 一个实践过的方案(一)-CSDN博客

这里给一个在golang的 gin框架中,用middleware实现的代码实例。

创建文件 signature_middleware.go

一、代码中的常量定义

const (
	// 四个要被签名的header头
	CaKey             = "tw-key"
	CaNonce           = "tw-nonce"
	CaSignatureMethod = "tw-signature-method"
	CaTimestamp       = "tw-timestamp"

	SIGNATURE         = "tw-signature"
	SignatureHeaders  = "tw-signature-headers"

	// 支持的方法
	HmacSHA256 = "HmacSHA256"
	HmacSHA1 = "HmacSHA1"

	ContentTypeUrlEncoded = "application/x-www-form-urlencoded"
	ContentTypeFormData = "multipart/form-data"
	ContentTypeApplicationJson = "application/json"
)

定义基本错误

const (
	SErrServiceFailedCode         = 2
	SErrDBFailedCode              = 3
	SErrorMustLoginCode           = 11
	SErrorHeaderSignatureFailCode = 21
	SErrorHeaderSignatureMustCode = 22
	SErrorHeaderNonceMustCode = 23
	SErrorHeaderTimestampMustCode = 24
	SErrorHeaderAppKeyMustCode = 25
	SErrorReplyPreventionCode = 26
)

type SystemError struct {
	Code     int
	Msg      string
	HttpCode int
}

func (b *SystemError) Error() string {
	return fmt.Sprintf("system error code:%d msg:%s", b.Code, b.Msg)
}

func NewSystemError(code int, msg string, httpCode int) *SystemError {
	return &SystemError{
		Code:     code,
		Msg:      msg,
		HttpCode: httpCode,
	}
}


var (
	SErrServiceFailed         = NewSystemError(SErrServiceFailedCode, "服务异常", http.StatusOK)
	SErrDBFailed              = NewSystemError(SErrDBFailedCode, "系统数据错误", http.StatusInternalServerError)
	SErrorMustLogin           = NewSystemError(SErrorMustLoginCode, "需要重新登录", http.StatusUnauthorized)
	SErrorHeaderSignatureFail = NewSystemError(SErrorHeaderSignatureFailCode, "签名错误", http.StatusOK)
	SErrorHeaderSignatureMust = NewSystemError(SErrorHeaderSignatureMustCode, "签名不能为空", http.StatusOK)
	SErrorHeaderNonceMust = NewSystemError(SErrorHeaderNonceMustCode, "nonce不能为空", http.StatusOK)
	SErrorHeaderTimestampMust = NewSystemError(SErrorHeaderTimestampMustCode, "timestamp不正确", http.StatusOK)
	SErrorHeaderAppKeyMust = NewSystemError(SErrorHeaderAppKeyMustCode, "key不能为空", http.StatusOK)
	SErrorReplyPrevention = NewSystemError(SErrorReplyPreventionCode, "重放识别", http.StatusOK)
)

二、基础的帮助方法

2.1 拿表单数据

表单的提交,如果是multipart/form-data,application/x-www-form-urlencoded,就通过gin的 ParseMultipartForm(),执行后就从PostForm中拿。在我们调用了ParseMultipartForm时它里面也执行了ParseForm,会把表单数据存到PostForm中。

具体就是下面一个函数搞定:

// 获取gin post表单所有参数
func GetPostFormParams(c *gin.Context) (map[string]string, error) {
	if err := c.Request.ParseMultipartForm(32<<20); err != nil {
		if !errors.Is(err, http.ErrNotMultipart) {
			return nil, err
		}
	}

	var postMap = make(map[string]string, len(c.Request.PostForm))
	for k, _ := range c.Request.PostForm {
		v := c.PostForm(k)
		postMap[k] = v
	}

	return postMap, nil
}

2.2 拿query数据

// 获取gin query所有参数, 如果一个key对应值是数组就取arr[0]
func GetQueryParams(c *gin.Context) map[string]string {
	query := c.Request.URL.Query()
	var queryMap = make(map[string]string, len(query))
	for k := range query {
		queryMap[k] = c.Query(k)
	}
	return queryMap
}

2.3 加密计算

需要引入下面包

  "crypto/hmac"

  "crypto/md5"

  "crypto/sha1"

  "crypto/sha256"

  "encoding/hex"

func HmacSha256(key, str string) string {
	hash := hmac.New(sha256.New, []byte(key))
	hash.Write([]byte(str))
	sum := hash.Sum(nil)

	sign := hex.EncodeToString(sum)
	return sign
}

func HmacSha1(key, str string) string {
	hash := hmac.New(sha1.New, []byte(key))
	hash.Write([]byte(str))
	sum := hash.Sum(nil)

	sign := hex.EncodeToString(sum)
	return sign
}

func Md5(str string) string {
	hash := md5.New()
	hash.Write([]byte(str))
	sum := hash.Sum(nil)
	sign := hex.EncodeToString(sum)
	return sign
}

2.4 排序

需要引入下面包

  "golang.org/x/text/encoding/simplifiedchinese"

  "golang.org/x/text/transform"

type SortByDict []string // 设置自定义类型

func (a SortByDict) Len() int {
	return len(a)
}

func (a SortByDict) Swap(i, j int) {
	a[i], a[j] = a[j], a[i]
}

func (a SortByDict) Less(i, j int) bool {
	v1, _ := UTF82GBK(a[i])
	v2, _ := UTF82GBK(a[j])
	bLen := len(v2)
	for idx, chr := range v1 {
		if idx > bLen-1 {
			return false
		}
		if chr != v2[idx] {
			return chr < v2[idx]
		}
	}
	return true
}

// UTF82GBK : transform UTF8 rune into GBK byte array
func UTF82GBK(src string) ([]byte, error) {
	GB18030 := simplifiedchinese.All[0]
	return io.ReadAll(transform.NewReader(bytes.NewReader([]byte(src)), GB18030.NewEncoder()))
}

// GBK2UTF8 : transform  GBK byte array into UTF8 string
func GBK2UTF8(src []byte) (string, error) {
	GB18030 := simplifiedchinese.All[0]
	bytes, err := io.ReadAll(transform.NewReader(bytes.NewReader(src), GB18030.NewDecoder()))
	return string(bytes), err
}

2.5 http返回值封装

func JsonSuccessResponse(c *gin.Context, data interface{}) {
	if data == nil {
		c.JSON(http.StatusOK, gin.H{
			"code":    200,
			"message": "success",
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"code":    200,
		"message": "success",
		"data":    data,
	})
}

func JsonErrResponse(c *gin.Context, err error) {
	switch err.(type) {
	case *SystemError:
		e := err.(*SystemError)
		c.JSON(e.HttpCode, gin.H{
			"code":    e.Code,
			"message": e.Msg,
		})
	default:
		c.JSON(200, gin.H{
			"code":    1,
			"message": err.Error(),
		})
	}
}

2.6 redis操作接口

定义一个redis操作的接口,有一个实现ReplyPreventionCacheImpl ,后面方法会用到这个对象,来操作redis。


var ReplyPreventionCacheImpl = NewReplyPreventionCache(iredis.GetGlobalRedis()

var _ ReplyPreventionCache = (*replyPreventionCache)(nil)

// -------------------

const (
	replyPreventionKey = "reply_prev_key"
)

func NewReplyPreventionCache(client *redis.Client) ReplyPreventionCache {
	return &replyPreventionCache{
		client: client,
	}
}

type replyPreventionCache struct {
	client *redis.Client
}

func (r replyPreventionCache) Expire(ctx context.Context, nonce string, t time.Duration) {
	key := fmt.Sprintf("%s:%s", replyPreventionKey, nonce)
	err := r.client.Expire(ctx, key, t).Err()
	if err != nil {
		log.Printf("redis reply prevention key set expire failed, key:%s", key)
	}
}

// SetNX 设置redis的key值,如果key存在,返回 false,nil,  如果key不存在,返回 true,nil。
func (r replyPreventionCache) SetNX(ctx context.Context, nonce string) (bool, error) {
	key := fmt.Sprintf("%s:%s", replyPreventionKey, nonce)
	has, err := r.client.SetNX(ctx, key, "1", time.Second*300).Result()
	if err != nil {
		log.Printf("redis reply prevention SetNX failed, err:%s", err.Error())
	}
	return has, err
}

// ReplyPreventionCache 放重放功能缓存interface
type ReplyPreventionCache interface {
	// SetNX 设置key. 不存在就能设置成功
	SetNX(context.Context, string) (bool, error)

	// Expire 设置值有效期
	Expire(context.Context, string, time.Duration)
}

三、构建签名数据

3.1 body数据进行md5

如果有body数据,要进行md5,加入到签名计算

func requestBodyMd5(c *gin.Context) string {
	if strings.Contains(c.ContentType(), ContentTypeUrlEncoded) ||
		strings.Contains(c.ContentType(), ContentTypeFormData) {
		return ""
	}

	body, _ := c.GetRawData()
	// 保证可以读取多次!!!
	c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
	if len(body) == 0 {
		return ""
	}

	s := Md5(string(body))
	return s
}

3.2 拿header中的数据

从header中拿几个需要的header头,并进行拼接

// headers签名串
// 有4个内容可能需要加入签名串,并按照key的字典顺序排序
// 1、tw-appkey
// 2、tw-nonce
// 3、tw-signature-method
// 4、tw-timestamp
func HeaderSignString(m map[string]string) string {
	str := ""
	if key, ok := m[CaKey]; ok {
		str += fmt.Sprintf("%s:%s", CaKey, key)
	}

	if nonce, ok := m[CaNonce]; ok {
		str += fmt.Sprintf("\n%s:%s", CaNonce, nonce)
	}

	if method, ok := m[CaSignatureMethod]; ok {
		str += fmt.Sprintf("\n%s:%s", CaSignatureMethod, method)
	}

	if time, ok := m[CaTimestamp]; ok {
		str += fmt.Sprintf("\n%s:%s", CaTimestamp, time)
	}

	return str
}


// 从header头中拿到关键参数,tw-appkey,  tw-nonce, tw-timestamp
// 将header的名字变成全小写
// 将header的值去掉开头和结尾的空白字符串
// 经过上一步之后值header转换为name:value
func getHeaderString(c *gin.Context) (string, error) {
	// key【必须】
	key := c.Request.Header.Get(CaKey)
	if key == "" {
		return "", SErrorHeaderAppKeyMust
	}
	c.Set(CaKey, key)

	// signature-method【可选】, 没传或者值不对默认采用HmacSHA256,
	method := c.Request.Header.Get(CaSignatureMethod)
	if method == "" {
		method = HmacSHA256
	} else {
		switch method {
		case HmacSHA256:
		case HmacSHA1:
		default:
			method = HmacSHA256
		}
	}
	c.Set(CaSignatureMethod, method)

	// nonce【可选】
	nonce := c.Request.Header.Get(CaNonce)
	if nonce != "" {
		c.Set(CaNonce, nonce)
	}

	// timestamp【可选】, 如果传了,值必须是毫秒的,即13位
	t := time.Unix(0, 0)
	timestamp := c.Request.Header.Get(CaTimestamp)
	if timestamp != "" {
		if len(timestamp) != 13 {
			return "", SErrorHeaderTimestampMust
		}
		_timestamp, err := strconv.Atoi(timestamp)
		if err != nil {
			return "", SErrorHeaderTimestampMust
		}
		t = time.UnixMilli(int64(_timestamp))
		c.Set(CaTimestamp, t)
	}

	// signature-headers
	headersList := make(map[string]string, 0)
	headers := c.Request.Header.Get(SignatureHeaders)
	if headers != "" {
		ss := strings.Split(headers, ",")
		for _, h := range ss {
			switch h {
			case CaKey:
				headersList[CaKey] = key
				break
			case CaSignatureMethod:
				headersList[CaSignatureMethod] = method
				break
			case CaNonce:
				headersList[CaNonce] = nonce
				break
			case CaTimestamp:
				headersList[CaTimestamp] = timestamp
				break
			}
		}
	}
	return HeaderSignString(headersList), nil
}

3.3 query和post表单数据拼接

// 获取url query中的参数, key全部转为小写
// 获取url post请求表单数据
func getQueryKV(c *gin.Context) (string, error) {
	KeysMap := make(map[string]string, 0)
	keys := make([]string, 0)

	// 获取query参数
	queryParams := GetQueryParams(c)
	// 获取form参数
	formParams, err := GetPostFormParams(c)
	if err != nil {
		return "", err
	}

	for k, v := range queryParams {
		KeysMap[k] = v
		keys = append(keys, k)
	}

	for k, v := range formParams {
		if _, ok := KeysMap[k]; ok == false {
			KeysMap[k] = v
			keys = append(keys, k)
		}
	}

	// 排序
	s := helpers.SortByDict(keys)
	sort.Sort(s)

	str := ""
	for _, k := range s {
		v := KeysMap[k]
		if v == "" {
			str += fmt.Sprintf("%s&", k)
		} else {
			str += fmt.Sprintf("%s=%s&", k, v)
		}
	}
	str = strings.TrimRight(str, "&")
	return str, nil
}

四、实现middleware - 签名验证


// 签名有5个部分组成
// 1、http method,大写的,比如“POST”
func Signature() gin.HandlerFunc {
	return func(c *gin.Context) {
		sign := c.Request.Header.Get(SIGNATURE)
		if sign == "" {
			c.Abort()
			JsonErrResponse(c, SErrorHeaderSignatureMust)
			return
		}

		// -------- 准备待签名串 --------
		// 1、http method
		method := c.Request.Method

		// 2、url path

		httpPath := c.Request.URL.Path

		// 3、headers
		headerStr, err := getHeaderString(c)
		if err != nil {
			c.Abort()
			JsonErrResponse(c, err)
			return
		}

		// 4、content md5
		bodyMd5 := requestBodyMd5(c)

		// 5、query parameters
		queryStr, err := getQueryKV(c)
		if err != nil {
			c.Abort()
			JsonErrResponse(c, err)
			return
		}

		// 拼接待签名串
		caStr := fmt.Sprintf("%s\n%s", method, httpPath)
		if headerStr != "" {
			caStr = fmt.Sprintf("%s\n%s", caStr, headerStr)
		}
		if bodyMd5 != "" {
			caStr = fmt.Sprintf("%s\n%s", caStr, bodyMd5)
		}
		if queryStr != "" {
			caStr = fmt.Sprintf("%s\n%s", caStr, queryStr)
		}

		// 计算签名串
		secret := "123456"
		signCals := ""
		signatureMethod, _ := c.Get(CaSignatureMethod)
		_signatureMethod := signatureMethod.(string)
		switch _signatureMethod {
		case HmacSHA1:
			signCals = HmacSha1(secret, caStr)
		default:
			signCals = HmacSha256(secret, caStr)
		}

		if sign != signCals {
			log.Printf("reply prevention sign is not ok, with http path: %s", httpPath)
			c.Abort()

			// c.JSON(http.StatusOK, gin.H{
			// 	"code":    200,
			// 	"message": fmt.Sprintf("%s\n======\n%s\n========", caStr, signCals),
			// 	"data":    nil,
			// })

			JsonErrResponse(c, SErrorHeaderSignatureFail)
			return
		}
		c.Next()
	}
}

五、实现middleware - 防止重放

从一个请求的header中能拿到nonce和timestamp值,

时间戳时毫秒的,5min内的请求才有效。

拿到nonce值往redis中写,如果redis已经存在了,说明已经请求一次了,不允许重复请求。

// ReplyPrevention 防止重放校验, 依赖于Signature, 签名已经解析,那么nonce和timestamp已经拿到了
func ReplyPrevention() gin.HandlerFunc {
	return func(c *gin.Context) {
		applyTime := time.Second * 300

		apiTime := c.Request.Header.Get(CaTimestamp)
		if apiTime == "" || len(apiTime) != 13 {
			log.Printf("reply prevention timestamp is not provided, with url: %s", c.Request.URL)
			return
		}

		// 有效期
		// 时间戳必须在5min内
		_apiTime, err := strconv.Atoi(apiTime)
		if err != nil {
			log.Printf("reply prevention time format failed, apiTime:%s", apiTime)
			c.Abort()
			JsonErrResponse(c, SErrorReplyPrevention)
			return
		}
		_timestamp := time.UnixMilli(int64(_apiTime))
		if time.Now().Sub(_timestamp) > applyTime {
			log.Printf("reply prevention timestamp is expired, with url: %s", c.Request.URL)
			c.Abort()
			JsonErrResponse(c, SErrorReplyPrevention)
			return
		}

		// 时间戳合理,再检验nonce必须存在
		nonce := c.Request.Header.Get(CaNonce)
		if nonce == "" {
			log.Printf("reply prevention nonce is empty, with url: %s", c.Request.URL)
			return
		}

		ok, err := ReplyPreventionCacheImpl.SetNX(context.Background(), nonce)
		if err != nil {
			// 如果redis异常,记录error
			log.Printf("redis reply prevention failed when redis SetNX %s, err %s", nonce, err.Error())
			c.Abort()
			JsonErrResponse(c, SErrDBFailed)
			return
		}

		// 如果nonce存在,说明已经请求过,此次为重复请求
		if !ok {
			log.Printf("reply prevention nonce is exist, with url: %s", c.Request.URL)
			c.Abort()
			JsonErrResponse(c, SErrorReplyPrevention)
			return
		}
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值