<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Golang 簽名采坑記

          共 10035字,需瀏覽 21分鐘

           ·

          2021-07-31 20:39

          本文來自網(wǎng)友投稿!

          前言

          最近在接入第三方接口的時候,要驗證參數(shù)里的簽名,簽名采用SHA256withRSA (RSA2),以確認(rèn)數(shù)據(jù)是不是被修改了。具體SHA256withRSA的原理不在這里講解,本文主要記錄在go(gin框架)驗簽時,踩到的一些坑,加以總結(jié)和記錄。

          ps: 以下的代碼有些沒有做錯誤處理,實際開發(fā)中不可取。

          SHA256withRSA

          待簽字符串

          接口參數(shù)以x-www-form-urlencoded的形式傳入, 如下的形式

          utc_timestamp:1624864579690
          sign:LPyc1kQNle9fTNfPi7zDz77eZFG0XD0YBXsRQNw/cCq00YE2dISzZIizi5S30ssHfVS2uuQsOyYYZoI8BgT1VR3vcf3CdOY8rkPPdqhBgcEJyKNRvQ3z+3VnM33gP84J5Ntg/LS8ZAlGpGjL9xTWtKVUbHZk0oy1qJwt3Da+wqchk5oh/cYeQnTyyUheQBf2WwPeNYCoauUS6R3KCtF3X8d2qUjx2ZEMkAMQhqGG9DwapWdTdoStjDZt+/Uz2wNT/4ctTa0iTvKPh5Zn1fBhBEKiflXlC32tRjS5hC2RfXR/1JR/AF+u937THwZmWv4xDPAQwRNcNwIH+a6mafygKg==
          sign_type:RSA2
          app_id:20210701
          content:{"page":1,"size":20}

          組裝待簽名字符串:

          1. 獲取全部參數(shù),剔除signsign_type參數(shù)

          2. 將篩選的參數(shù)按照第一個字符的鍵值ASCII碼遞增排序(字母升序排序),如果遇到相同字符則按照第二個字符的鍵值ASCII碼遞增排序,以此類推

          3. 將排序后的參數(shù)與其對應(yīng)值,組合成“參數(shù)=參數(shù)值”的格式,并且把這些參數(shù)用&字符連接起來,此時生成的字符串為待簽名字符串

          按照要求,則獲取的待簽字符串為:app_id=20210701&content={"page":1,"size":20}&utc_timestamp=1624864579690

          簽名與驗簽

          雖然是作為驗簽方,但是為了方便測試,也實現(xiàn)了簽名方法,先準(zhǔn)備公鑰與私鑰,私鑰簽名,公鑰驗簽。

          -----BEGIN PUBLIC KEY-----
          MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCyPWejY7A+stkupI5Ow1aqlDgQ8g04gByyuyOiqw/wl8j8maerG1e7YKiF5qGOKr+Jw83HPdMFLCZDZebS63taPA2aIA+2x1CpIVfss5jSRQNsVzez9eDW7HTI+Nplx95BLl8OVE724hCgWFEjpwZ4GzORQMzmIXxxw67sdo9iuwIDAQAB
          -----END PUBLIC KEY-----


          -----BEGIN PRIVATE KEY-----
          MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALI9Z6NjsD6y2S6kjk7DVqqUOBDyDTiAHLK7I6KrD/CXyPyZp6sbV7tgqIXmoY4qv4nDzcc90wUsJkNl5tLre1o8DZogD7bHUKkhV+yzmNJFA2xXN7P14NbsdMj42mXH3kEuXw5UTvbiEKBYUSOnBngbM5FAzOYhfHHDrux2j2K7AgMBAAECgYBXtQGfk/lxEN7wJcdlGJg3/hGMvR8mU1xL0uyZKiYA1R/wtMed2imUqd6jbTbIV17DMte6mECThgMaHTW1Smz6yrXYwPLmorkZmDxC4ggpvriH7sDgvBL++lOlLfRQqL7XLx72ZDaFWC0qFokKc5vviXBqWnTVMf/SQenSZGkgEQJBAN5z1x9Dyv2XyYwyJqXzEHWmvx7jjwqGQx6nFWnIVfeXQyJSSY7tqT6J4fGHe9eq5nbnqQo964RrR91Q+2iRGMkCQQDNHqjvgoT/skAXy80BP2Mt5W5pFjjeVlaCoaf006mTngkfB24ZmvxoxX5NfNBEGB/iS2KCsU5/h1ykpU3Lj+VjAkA9MwVl9pKr/cxXI5z6XsqSc5N0/gnmTVW94x3DAniUKysvEBBon/3F1M0yU6HAjaXl5Ine5XYb8h/NRXBFLlXxAkEAub1muqOU7bmqoiGxPMz6cWgNh+lQi7zgz5+06FT2fK6hkdB3mYYnxHP5wA8ixFaYIKGkzbXi4EZh1NG/VXKzAwJAFp+hcKz9oRO1LodExpdmATTd031g53X+3MMKG+PJREjAnC9wQL4RsmbzYP5NZ2dORIpNgRWawF2b1KJxWiiCsg==
          -----END PRIVATE KEY-----

          實現(xiàn)簽名函數(shù)

          func RsaSignWithSha256(data []byte, keyBytes []byte) ([]byte, error) {
           h := sha256.New()
           h.Write(data)
           hashed := h.Sum(nil)
           block, _ := pem.Decode(keyBytes)
           if block == nil {
            return nil, errors.New("private key error")
           }
           privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
           if err != nil {
            return nil, err
           }

           signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey.(*rsa.PrivateKey), crypto.SHA256, hashed)
           if err != nil {
            return nil, err

           }

           return signature, nil
          }

          最終返回的結(jié)果是一個字節(jié)數(shù)組,但是參數(shù)是以字符串的形式傳遞的,因此需要把這個字節(jié)數(shù)組轉(zhuǎn)為字符串,有兩種形式 :

          • hex hex.EncodeToString
          • base64 base64.StdEncoding.EncodeToString

          編碼之后的數(shù)據(jù),也會有明顯的差異

          // base64
          AezhDSynfsTMrU517zHK12e2SzczNczm+yRht+Dr+I0K7VE+TLeUbpB1SiMbxLIdT2SsunIm0h5vaeHAyf9QwAFvjlcPG6JhJBOo58AtXx2moVVuu2pAEtO/tJw61VKbT4j5nAIiC1Ac2i1+u5BdbYoAV6Fc+HtfAJBS1iWinwQ=

          // hex
          01ece10d2ca77ec4ccad4e75ef31cad767b64b373335cce6fb2461b7e0ebf88d0aed513e4cb7946e90754a231bc4b21d4f64acba7226d21e6f69e1c0c9ff50c0016f8e570f1ba2612413a8e7c02d5f1da6a1556ebb6a4012d3bfb49c3ad5529b4f88f99c02220b501cda2d7ebb905d6d8a0057a15cf87b5f009052d625a29f04

          不管采用哪種編碼,在驗證簽名的時候都要先解碼轉(zhuǎn)成字節(jié)數(shù)組,否則驗簽不會通過,以下是驗簽函數(shù),采用hex

          func RsaVerySignWithSha256(data, signData, keyBytes []byte) bool {
           block, _ := pem.Decode(keyBytes)
           if block == nil {
            panic(errors.New("public key error"))
           }
           pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
           if err != nil {
            panic(err)
           }

           hashed := sha256.Sum256(data)

            // 注意這里簽名字符串要解碼
           //sig, _ := base64.StdEncoding.DecodeString(string(signData))  base64解碼
           sig, _ := hex.DecodeString(string(signData)) // hex解碼

           err = rsa.VerifyPKCS1v15(pubKey.(*rsa.PublicKey), crypto.SHA256, hashed[:], sig)
           if err != nil {
            panic(err)
           }
           return true
          }

          最后整個簽名與驗簽的過程就完成了

          func main (){
            s := `app_id=20210701&content={"page":1,"size":20}&utc_timestamp=1624864579690`
            sign, _ := RsaSignWithSha256([]byte(s), prvKey)
            sigs := hex.EncodeToString(sign)
            fmt.Println(RsaVerySignWithSha256([]byte(s), []byte(sigs), pubKey)) // true
          }

          既然驗簽過程完成了,接下來就是應(yīng)用在項目中了,這里使用的是gin框架,而接下來的這部分,踩了不少的坑 Orz

          go(Gin)踩坑

          從參數(shù)到待簽字符串

          在使用gin中,我都會使用ShouldBind將參數(shù)與結(jié)構(gòu)體綁定,這次也自然而然的這么使用。將參數(shù)綁定到結(jié)構(gòu)體之后,想拼接待簽字符串,那就要遍歷結(jié)構(gòu)體,要遍歷結(jié)構(gòu)體,就要靠反射

          func GetPendingSign(p interface{}) []byte {
           var typeInfo = reflect.TypeOf(p)
           var valInfo = reflect.ValueOf(p)

           num := typeInfo.NumField()
           var keys = make([]string0, num)
           var field = make([]string0, num)
           for i := 0; i < num; i++ {
            key := typeInfo.Field(i).Tag.Get("form"// 結(jié)構(gòu)體的form tag才是待簽字符串的key
            if key != "sign" && key != "sign_type" {
             keys = append(keys, key)
              field[key] = typeInfo.Field(i).Name // 找不通過tagName獲取值的,因此做了一個form tag和屬性名的對應(yīng)
            }
           }

           sort.Strings(keys)
            
           s := ""
           for i, k := range keys {
            temp := valInfo.FieldByName(field[i]).Interface() // 通過上面的對應(yīng),獲取值
            if k == "content" {
                // 因為content被反序列化了,所以這里重新序列化成json,以拼接字符串
             b, _ := json.Marshal(temp)
             s = fmt.Sprintf("%s%s=%s&", s, k, string(b))
            } else {
             s = fmt.Sprintf("%s%s=%v&", s, k, temp)
            }
           }
           s = s[:len(s)-1//待簽名字符串
           return []byte(s)
          }

          上面的方法中,有幾個部分做了注釋,這幾個地方也是比較關(guān)鍵的。接著用postman進行測試,將最開始的參數(shù)傳入,會得到和預(yù)期一樣的待簽字符串。

            r := gin.Default()
            r.POST("/test"func(c *gin.Context) {
            var body Body
            c.ShouldBind(&body)
            fmt.Println(string(GetPendingSign(body)))
            })
            r.Run(":8081")

          然而就這樣結(jié)束了嗎?不!這種方式,有一個很大的問題!如果把content:{"page":1,"size":20}改成content:{"size":20,"page":1}入?yún)?,會發(fā)現(xiàn)簽名驗證失敗了!是的!就是調(diào)換了sizepage的位置,這種方式的問題就暴露出來了。

          結(jié)構(gòu)體map和json

          為什么會驗簽失敗呢?第一個想法就是待簽字符串是否一致?再次請求,打印拼接出來的字符串,會發(fā)現(xiàn)仍然是content={"page":1,"size":20}而不是傳入的content:{"size":20,"page":1}看一看結(jié)構(gòu)體的定義

          type Content struct {
           Page int `json:"page" form:"page"`
           Size int `json:"size" form:"size"`
          }

          明顯的發(fā)現(xiàn),序列化之后key的順序和定義結(jié)構(gòu)體屬性的順序保持了一致,而不是以最開始的json為準(zhǔn)了,即:結(jié)構(gòu)體序列化成json時,json的key值順序以定義結(jié)構(gòu)體時,屬性的順序為準(zhǔn)

          既然說到了結(jié)構(gòu)體,再來看看map

           s := `{"size":20,"page":1}`
           m := make(map[string]int)
           json.Unmarshal([]byte(s), &m)
           b, _ := json.Marshal(&m)
           fmt.Println(s)
           fmt.Println(string(b)) // {"page":1,"size":20}

          key的順序也是被調(diào)整了,那么map又是以什么規(guī)則來調(diào)整key的順序呢?

          從源碼的 encoding/json/encode.go 第793行中看到這行代碼

          sort.Slice(sv, func(i, j int) bool { return sv[i].s < sv[j].s }) 

          即:map轉(zhuǎn)json是有序的,按照ASCII碼升序排列key。

          好吧,既然兩個方式都會改變key的順序,那么這種先綁定結(jié)構(gòu)體再遍歷拼接的方式就不可取了。

          解決方案

          既然不能先反序列化,那么就要采取其他的方案了。不以ShouldBind的形式獲取參數(shù),那么就用ioutil.ReadAll的方式來獲取參數(shù),打印看看獲取到的參數(shù)

          utc_timestamp=1624864579690&sign=LPyc1kQNle9fTNfPi7zDz77eZFG0XD0YBXsRQNw%2FcCq00YE2dISzZIizi5S30ssHfVS2uuQsOyYYZoI8BgT1VR3vcf3CdOY8rkPPdqhBgcEJyKNRvQ3z%2B3VnM33gP84J5Ntg%2FLS8ZAlGpGjL9xTWtKVUbHZk0oy1qJwt3Da%2Bwqchk5oh%2FcYeQnTyyUheQBf2WwPeNYCoauUS6R3KCtF3X8d2qUjx2ZEMkAMQhqGG9DwapWdTdoStjDZt%2B%2FUz2wNT%2F4ctTa0iTvKPh5Zn1fBhBEKiflXlC32tRjS5hC2RfXR%2F1JR%2FAF%2Bu937THwZmWv4xDPAQwRNcNwIH%2Ba6mafygKg%3D%3D&sign_type=RSA2&app_id=20210701&content=%7B%22size%22%3A20%2C%22page%22%3A1%7D

          x-www-form-urlencoded的參數(shù)形式,和query的參形式類似,都是用&和=來拼接,既然都是字符串,那么就手動切割,然后拼成map,最后遍歷map,拼接成待簽字符串。

           bodyArray := strings.Split(string(body), "&"//1、先按&切割
           data := make(map[string]string)
           for _, v := range bodyArray {
            // 2、按照 = 切分組裝map
            vs := strings.Split(v, "=")
            if len(vs) == 2 {
             value, err := url.QueryUnescape(vs[1]) // 從上面打印的字符,可以看出被urlescape過,因此要Unescape
             if err != nil {
              c.Abort()
              return
             }
             data[vs[0]] = value
            }
           }

          按照上面的形式,就可以得到一個map,而content的值,因為只是個字符串沒有被重新處理。因此就不會再出現(xiàn)key順序不一致的問題。接著只需要遍歷這個map,按照要求組裝待簽字符串即可。

          最后把這些步驟都封裝成一個中間件使用,驗簽功能完成。

          總結(jié)

          來看看最終都有哪些知識:

          • 簽名有base64hex的編碼方式,驗簽的時候,要對應(yīng)解碼
          • 使用反射遍歷結(jié)構(gòu)體
          • 結(jié)構(gòu)體序列化成json時,json key按照結(jié)構(gòu)體的屬性順序重新排序
          • map序列化成json時,json key按照ASCII碼升序排列
          • x-www-form-urlencoded 的參數(shù)形式,以&和=拼接,并且會被urlescape,處理的時候要unescape
          • 最后一個小知識點, 在中間件中用ioutil.ReadAll讀完body,記得重新把body寫回去 c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)),否則后面的路由就讀不到body

          go路漫漫~,感謝閱讀 Thanks!



          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進階看什么。關(guān)注公眾號 「polarisxu」,回復(fù) ebook 獲??;還可以回復(fù)「進群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 57
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲男人69天堂 | www.豆花视频无码 | 日本中文字幕爱爱 | 五月天免费色 | 先锋av资源在线 先锋影音成人在线 |