接口签名校验设计
Why-为什么需要签名校验
API签名即对API接口的调用进行签名保护,在进行API调用的时候,加多了一个签名校验的过程,通过签名校验,才能进行接口的调用,这个签名可以理解为一个合法的调用凭证
一个签名需要做什么:
- 明确调用者是谁,即签名中需要带上需要带上用户标识(可以是用户UID,可以是openID等等)
- 明确调用者想干什么,可以日志记录签名校验失败的调用者信息和其调用接口
总体来说,API签名校验是在调用API前的一道防护设施
How-如何做签名校验
签名校验
基本流程:
请求入参解释:
- openID:明确调用者是谁
- apiName:明确调用者想干什么
- sign:客户端携带的签名,后续用于和服务端签名进行比较
- nonce:随机字符串可以是uuid
- ts:时间戳
nonce的作用:
nonce是随机字符串,可以作为请求的唯一标识,防止调用方重复多次请求,每次请求都需要传递不同的nonce,你可以使用uuid作为no
对于部分请求来说,具有请求一次性的要求,即同一请求内容请求一次和两次的结果是不一样的,如果不进行请求防重,恶意攻击者网络抓包到一个合法请求后就会不停的请求,对后端进行攻击
- 如果是写请求,且涉及到db写操作,后端db压力负载会瞬间增大,甚者压垮db
- 如果是读请求,后端性能也会下降
如何实现?
最简单的方法就是将请求中的nonce放在redis中,下一次请求到来后,去查redis,有同样的nonce则判定为重复请求
但是会存在两个问题:
- 存储成本问题:nonce越多,存储空间要求越大,且nonce越多,nonce的存储会越来越大,验证nonce是否存在的时间会越来越长。
- 每次请求都会发起一次redis读操作
ts的作用
时间戳ts就是用来优化nonce存在的问题的
根据时间戳判断是否超过了一定的时间范围,如果超过了就直接拒绝,没有超过继续验证nonce是否使用过。
时间戳校验:
// 时间戳比较,超过指定区间的请求抛弃
timeNow := time.Now().Unix()
timeBefore := timeNow - 60
timeAfter := timeNow + 60
if ts > timeAfter || ts < timeBefore {
zaplog.Info("verify sign fail by ts", zaplog.Reflect("ts", ts),
zaplog.Reflect("timeAfter", timeAfter),
zaplog.Reflect("timeBefore", timeBefore))
return errors.New("ts时间戳超时")
}
JWT token生成
签名校验通过后,则可以通过JWT生成token令牌
JWT是JSON Web Token,是session和cookie外的一种登陆鉴权方法
JWT有三部分:
- Header:请求头主要是用来定义编码算法和token类型的
{
"alg": "HS256",
"typ": "JWT"
}
- Payload:JWT的一些声明信息,用户id名称邮箱一般放置在这,是一个map结构
{
"sub": "xxx-api",
"name": "bgbiao.top",
"admin": true
}
- Signatur:对编码过的header和payload以及密钥在进行一次编码
HMACSHA256(base64(header)+"."+base64(payload),secret)
所以最后的token是:
base64(Header).base64(Payload).Signature
一般的使用场景:客户端拿到服务端下发的token后,拿着token去请求服务端的带鉴权的接口
实现:
// TokenClaims 自定义jwt信息载体
type TokenClaims struct {
OpenID string
ApiName string
jwt.StandardClaims
}
// GenerateJwtToken 生成JwtToken
func GenerateJwtToken(openID, apiName, issuer string) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(300 * time.Second)
claims := TokenClaims{
OpenID: openID,
ApiName: apiName,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireTime.Unix(),
Issuer: issuer,
},
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte("golang"))
return token, err
}
Jwt token解析则是将请求中的token根据.进行字符串分割,第一部分和第二部分是base64编码的,因此先进行解码,然后将解码后的信息和密钥在服务端根据原来的加密算法生成签名,新签名和token中的第三部分进行比较,一样则是正确的token
// 解析Jwt Token 得到Token载体claims
func ParseToken(token string) (*TokenClaims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte("golang"), nil
})
if err != nil {
return nil, err
}
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*TokenClaims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
整体设计
主流程:
设计了两个接口:
- 获取token
- 校验token
客户端根据签名规则将请求参数生成一个签名和请求参数一同提交到后端,后端先进行签名校验,通过后再下发JWT Token