# API签名验证

# 背景

公司内部服务之间调用是,使用的签名如下:


// 签名验证
func ValidateSignatureMiddleware(svc *service.Service) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		ctxx := contextx.NewContextx(ctx, svc)
		if svc.Config.System.Mode == gin.ReleaseMode {
			query := ctx.Request.URL.Query()
			// sign参数
			sign := query.Get("sign")
			if sign == "" {
				ctxx.Error(fsdkerror.Msg("Missing signature"))
				ctxx.Abort()
				return
			}
			// t参数
			t := query.Get("t")
			if t == "" {
				ctxx.Error(fsdkerror.Msg("Missing timestamp"))
				ctxx.Abort()
				return
			}
			tn, _ := strconv.ParseInt(t, 10, 64)
			if time.Now().Unix()-tn > 300 || time.Now().Unix()-tn < 0 {
				ctxx.Error(fsdkerror.Msg("Invalid timestamp"))
				ctxx.Abort()
				return
			}
			// 签名验证
			if !fsdkutils.ValidateSignature(query, svc.Config.ValidateSignatureSecretKey) {
				ctxx.Error(fsdkerror.Msg("Invalid signature"))
				ctxx.Abort()
				return
			}
		}
		ctxx.Next()
	}
}

// 生成签名
func Signature(query url.Values, secret string) string {
	// 第一步:按照query key排序
	keys := make([]string, 0, len(query))
	for k := range query {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	// 第二步:拼接参数
	str := ""
	for _, k := range keys {
		str += k + "=" + query.Get(k) + "&"
	}
	str = strings.TrimSuffix(str, "&")

	// 第三步:拼接密钥
	str += secret

	// 第四步:MD5加密
	return MD5(str)
}

// 验证签名
func ValidateSignature(query url.Values, secret string) bool {
	sign := query.Get("sign")
	if sign == "" {
		return false
	}
	query.Del("sign")
	return sign == Signature(query, secret)
}
  1. t参数用于防止重放攻击,要求请求时间在当前时间的±300秒内。
  2. sign参数用于验证请求的合法性,要求请求参数按照key排序,然后拼接成字符串,最后加上密钥,再进行MD5加密。

上面的签名验证比较简单,在使用过程中发现存在下面问题:

  1. 没有区分调用方,即所有调用方都使用相同的密钥。不利于权限控制、流量监控等。
  2. t参数用于防止重放攻击过于简单,容易被绕过。
  3. 只对query参数进行签名验证,没有对body参数进行签名验证。
  4. 缺少ip白名单,即所有调用方都可以访问。

# 改进

  1. 添加appkey参数,用于区分调用方,即每个调用方使用不同的密钥。
  2. t参数的基础上,添加nonce参数,用于防止重放攻击,要求请求时间在当前时间的±300秒内,且同一个appkeynonce参数在1分钟内不能重复。
  3. querybody参数进行签名验证。
  4. 添加ip白名单,即只有白名单中的ip才能访问。目前只对ip参数进行校验,没有对ip白名单进行校验。

改进后的签名验证如下:


// 签名验证 v2
func ValidateSignatureV2Middleware(svc *service.Service) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		ctxx := contextx.NewContextx(ctx, svc)
		if svc.Config.System.Mode == gin.ReleaseMode {
			query := ctx.Request.URL.Query()
			// appkey参数
			appkey := query.Get("appkey")
			if appkey == "" {
				ctxx.Error(fsdkerror.Msg("Missing appkey"))
				ctxx.Abort()
				return
			}
			// 校验appkey是否存在 并且赋值secret
			secret := ""
			if appsecret, ok := svc.Config.ValidateSignatureV2[appkey]; !ok {
				ctxx.Error(fsdkerror.Msg("Invalid appkey"))
				ctxx.Abort()
				return
			} else {
				secret = appsecret
			}
			// ip参数
			ip := query.Get("ip")
			if ip == "" {
				ctxx.Error(fsdkerror.Msg("Missing ip"))
				ctxx.Abort()
				return
			}
			// ip参数校验
			if !fsdkutils.IsValidIP(ip) {
				ctxx.Error(fsdkerror.Msg("Invalid ip"))
				ctxx.Abort()
				return
			}
			// todo:: ip白名单验证
			// nonce参数
			nonce := query.Get("nonce")
			if nonce == "" {
				ctxx.Error(fsdkerror.Msg("Missing nonce"))
				ctxx.Abort()
				return
			}
			// appkey对应的nonce是否已经存在
			nonceKey := fmt.Sprintf(config.ValidateSignatureV2NonceKey, appkey, nonce)
			if ok, err := svc.RedisClient.SetNX(context.Background(), nonceKey, 1, time.Minute).Result(); !ok || err != nil {
				if err != nil {
					ctxx.Error(fsdkerror.Err(err))
				} else {
					ctxx.Error(fsdkerror.Msg("Nonce already exists"))
				}
				ctxx.Abort()
				return
			}
			// t参数
			t := query.Get("t")
			if t == "" {
				ctxx.Error(fsdkerror.Msg("Missing timestamp"))
				ctxx.Abort()
				return
			}
			tn, _ := strconv.ParseInt(t, 10, 64)
			if time.Now().Unix()-tn > 300 || time.Now().Unix()-tn < 0 {
				ctxx.Error(fsdkerror.Msg("Invalid timestamp"))
				ctxx.Abort()
				return
			}
			// sign参数
			signature := query.Get("sign")
			if signature == "" {
				ctxx.Error(fsdkerror.Msg("Missing signature"))
				ctxx.Abort()
				return
			}
			// 获取body数据
			body, _ := ctx.GetRawData()
			if !fsdkutils.ValidateSignatureV2(query, string(body), secret) {
				ctxx.Error(fsdkerror.Msg("Invalid signature"))
				ctxx.Abort()
				return
			}
		}
		ctxx.Next()
	}
}


// 生成签名V2
func SignatureV2(query url.Values, body, secret string) string {
	// 第一步:param排序
	keys := make([]string, 0, len(query))
	for k := range query {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	// 第二步:拼接参数
	str := ""
	for _, k := range keys {
		str += k + "=" + query.Get(k) + "&"
	}
	str = strings.TrimSuffix(str, "&")
	str += body + secret

	// 第三步:MD5加密
	return MD5(str)
}

// 验证签名V2
func ValidateSignatureV2(query url.Values, body, secret string) bool {
	sign := query.Get("sign")
	if sign == "" {
		return false
	}
	query.Del("sign")
	return sign == SignatureV2(query, body, secret)
}