接口签名的方案,请看签名的文章 接口签名 - 一个实践过的方案(一)-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
}
}
}