# 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)
}
t
参数用于防止重放攻击,要求请求时间在当前时间的±300秒内。sign
参数用于验证请求的合法性,要求请求参数按照key排序,然后拼接成字符串,最后加上密钥,再进行MD5加密。
上面的签名验证比较简单,在使用过程中发现存在下面问题:
- 没有区分调用方,即所有调用方都使用相同的密钥。不利于权限控制、流量监控等。
t
参数用于防止重放攻击过于简单,容易被绕过。- 只对
query
参数进行签名验证,没有对body
参数进行签名验证。 - 缺少
ip
白名单,即所有调用方都可以访问。
# 改进
- 添加
appkey
参数,用于区分调用方,即每个调用方使用不同的密钥。 - 在
t
参数的基础上,添加nonce
参数,用于防止重放攻击,要求请求时间在当前时间的±300秒内,且同一个appkeynonce
参数在1分钟内不能重复。 - 对
query
和body
参数进行签名验证。 - 添加
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)
}