2017年9月13日 星期三

Golang 使用 Oauth1.0a 協定對 WooCommerce 發送 API 請求

使用 Golang 對 Wordpress 外掛 WooCommerce 進行單一請求連接方法。

WooCommerce 可以在設定的部分開啟 REST API 給第三方應用程式存取,其 API 會提供 consumer_key, consumer_secret 兩個公私鑰作為驗證。

WooCommerce 有兩種方式可以存取 API ,是參考官方程式碼 (https://github.com/woocommerce/woocommerce/blob/master/includes/api/class-wc-rest-authentication.php#L141):

1. Basic Auth: Basic Auth 只要把 consumer_key:consumer_secret 用 base64 編碼後,把設定 Header 參數: "Authorization" 為 "base64編碼後的數值",就可以登入。 (前提必須走 https)

2. 用 signature 的方式來驗證,也就是本文的敘述。

OAuth 1.0a 流程

根據 oauthbible (oauth 聖經) 所述 (http://oauthbible.com/#oauth-10a-one-legged):
Oauth 有幾種方式可以存取,同時 WooCommerce 也支援的方式:


  1. 導向用戶來請求一個 token (令牌),得到 token 之後伺服器就可以直接拿  token 來訪問 WooCommerce 的資源 (訂單、商品資訊...etc)
  2. 從 WooCommerce 後台拿到 consumer_key, consumer_secret,就直接發送單一請求或是用 Basic Auth 請求資源。
  3. 拿 consumer_key, consumer_secret 直接向伺服器交換 token,就可拿  token 來請求資源。

本文我主要以 2. 為解釋。
以下是參考文獻:

 (連結) (以 1. 的方法為主,介紹 oauth1.0a, 2.0 的驗證流程)

 (連結) (Oauth1 認證方式(英文))

(連結) (Oauth 1.0 認證說明)

(連結) (Oauth 1.0 / 2.0 說明)


簡述 2. 的單一請求實作流程:


  1. 持有 consumer_key, consumer_secret
  2. 建立一個簽章 HMAC-SHS256 (Signature) ,並且用 consumer_secret + "&" 作為簽名


    把 consumer_secret 加上 '&' 是因為有可能會加入 token 的密鑰,根據 Oauth1.0a 規範,不論有沒有,就算是空的 secret都要在兩者之間加上一個 '&' (請參考這裡)。
  3. 將簽章參數 (consumer_key, timestamp, signature, signature_method, nonce) 加入 url parameters ,並且發送請求。

Signature + HMAC-SHA256 製造方法
首先,我需要製造一段 Signature 的字串,他是這樣組成的:

1. 發送 HTTP 請求的方法大寫 (GET、POST、DELETE、PUT...etc)
2. 請求的目標網址路徑 (不含參數)
3. 請求網址的參數 (不需要加上 '?',只要用 '&' 區隔一般的參數即可)

將以上三個值,先做 URLEncode ,再用 '&' 把這三個值連接再一起。
signature_base_string := strings.Join([]string{"GET", url.QueryEscape("http://example.com/wp-json/wc/v2"), url.QueryEscape("oauth_consumer_key=xxxxxx&oauth_nonce=xxxxx...etc") }, "&")

fmt.Println(signature_base_string)

所以獲得的資料應該為:
GET&http%3A%2F%2F192.168.1.140%2Fwp-json%2Fwc%2Fv2%2Fproducts&oauth_consumer_key%3Dck_3090f5bea63d198a29296b62e1d92a26969aa7be%26oauth_nonce%3D3df344cf342f14aa5f8f18560357a1a3b262e92c%26oauth_signature_method%3DHMAC-SHA256%26oauth_timestamp%3D1505211309

針對第 3. 項,網址請求的所需參數為:

  • oauth_consumer_key="xxxxxxxxxxxxxx"
  • oauth_nonce=(sha1隨機產生數)
  • oauth_signature_method="HMAC-SHA256" ( WooCommerce 只支援 HMAC-SHA1, HMAC-SHA256 加密方式,請參考: REST API DOC - Generate Signature)
  • oauth_timestamp=(目前的時間戳記)

到目前為止,Signature 組成的程式碼為:
HTTPMethod := "GET"
RequestURL := "http://example/wp-json/wc/v2/products" //假設我要訪問商品資料的路徑

_KEY := "ck_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
_SECRET := "cs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

HASH := "HMAC-SHA256"

SignParam := url.Values{}
SignParam.Add("oauth_consumer_key", _KEY)

nonce := make([]byte, 16)
rand.Read(nonce)
sha1Nonce := fmt.Sprintf("%x", sha1.Sum(nonce))
SignParam.Add("oauth_nonce", sha1Nonce)

SignParam.Add("oauth_signature_method", HASH)

SignParam.Add("oauth_timestamp", strconv.Itoa(int(time.Now().Unix())))


signature_base_string := strings.Join([]string{HTTPMethod, url.QueryEscape(RequestURL), url.QueryEscape(SignParam.Encode()) }, "&")

fmt.Println(signature_base_string)


產生 Signature string 之後,需要加密這串數值,將使用 HMAC-SHA256 加密演算法做加密,成為一個 Signature , 其加密的 secret 在 Spec 描述為:

consumer_secret + "&" + consumer_token_secret

但我所使用的請求方式沒有 consumer_token_secret ,按照 Oauth1.0 的 sepc 說明: 「 不管兩個 secret 值是否存在,都要加上 & 。 (RFC 5849 - 3.4.2 第 3 項 key 說明)

所以我的 hmac 加密的 secret 則是這樣寫的:
mac := hmac.New(sha256.New, []byte(_SECRET + "&"))
mac.Write([]byte(signature_base_string))
signatureBytes := mac.Sum(nil)
singnature := base64.StdEncoding.EncodeToString(signatureBytes)

fmt.Println(singnature)

singnature = url.QueryEscape(singnature)

完成後,第二個步驟就告一段落。


設定網址參數並發送請求

最後,需要產生一個 http request 去撈資料,網址是:

http://[目標請求位置(包含路徑)]?[必要參數]

必要參數為:

  • oauth_consumer_key
  • oauth_nonce
  • oauth_timestamp
  • oauth_signature
  • oauth_signature_method
組合網址的程式碼為:
sepcParams := "oauth_consumer_key=" + SignParam.Get("oauth_consumer_key") + "&oauth_nonce=" + SignParam.Get("oauth_nonce") + "&oauth_signature=" + singnature + "&oauth_signature_method=" + SignParam.Get("oauth_signature_method") + "&oauth_timestamp=" + SignParam.Get("oauth_timestamp")

//generate url
specUrl := "http://192.168.1.140/wp-json/wc/v2/products" + "?" + specParams
fmt.Println(specUrl)
組合好一個 URL 之後,就可以對 API 發出單一請求,並且取得回傳值,其程式碼為:
func client() *http.Client {
 rawClient := &http.Client{}
 rawClient.Transport = &http.Transport{
  TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
 }
 return rawClient
}

func makeRequest(HttpMethod, urlpath string) (io.ReadCloser, error){
 makeAuth := client()

 data := "{}"

 body := new(bytes.Buffer)
 encoder := json.NewEncoder(body)
 if err := encoder.Encode(data); err != nil {
  return nil, err
 }

 req, err := http.NewRequest(HttpMethod, urlpath , body)
 if err != nil {
  return nil, err
 }
 req.Header.Set("Content-Type", "application/json")
 resp, err := makeAuth.Do(req)
 if err != nil {
  return nil, err
 }
 if resp.StatusCode == http.StatusBadRequest ||
  resp.StatusCode == http.StatusUnauthorized ||
  resp.StatusCode == http.StatusNotFound ||
  resp.StatusCode == http.StatusInternalServerError {
  return nil, fmt.Errorf("Request failed: %s", resp.Status)
 }
 return resp.Body, nil
}

func main() {

 body, err := makeRequest("GET", "http://xxxxxxxxxxxxxxxxxxxxxxxx")
 if err != nil {
  panic(err)
 }
 buf := new(bytes.Buffer)
 buf.ReadFrom(body)
 newStr := buf.String()
 
 fmt.Printf(newStr)

}

完整程式碼為:
程式碼稍微亂了一些,我除了錯整整花了 48 小時,這個坑超大,如果你只要看如何產生網址,你可以直接參考 oauth_generator() 這個函數的內容。
package main

import (
 "io"
 "crypto/hmac"
 "crypto/rand"
 "crypto/sha1"
 "crypto/sha256"
 "crypto/tls"
 "encoding/base64"
 "encoding/json"
 "fmt"
 "net/http"
 "net/url"
 "strconv"
 "strings"
 "time"
 "bytes"

)

func oauth_generator() string {
 HTTPMethod := "GET"
 RequestURL := "http://example.com/wp-json/wc/v2/products"

 _KEY := "ck_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 _SECRET := "cs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

 HASH := "HMAC-SHA256"

 SignParam := url.Values{}
 SignParam.Add("oauth_consumer_key", _KEY)

 nonce := make([]byte, 16)
 rand.Read(nonce)
 sha1Nonce := fmt.Sprintf("%x", sha1.Sum(nonce))
 SignParam.Add("oauth_nonce", sha1Nonce)

 SignParam.Add("oauth_signature_method", HASH)

 SignParam.Add("oauth_timestamp", strconv.Itoa(int(time.Now().Unix())))


 signature_base_string := strings.Join([]string{HTTPMethod, url.QueryEscape(RequestURL), url.QueryEscape(SignParam.Encode()) }, "&")


 mac := hmac.New(sha256.New, []byte(_SECRET + "&"))
 mac.Write([]byte(signature_base_string))
 signatureBytes := mac.Sum(nil)
 singnature := base64.StdEncoding.EncodeToString(signatureBytes)

 fmt.Println(singnature)

 singnature = url.QueryEscape(singnature)

 //spec url
 specUrl := "oauth_consumer_key=" + SignParam.Get("oauth_consumer_key") + "&oauth_nonce=" + SignParam.Get("oauth_nonce") + "&oauth_signature=" + singnature + "&oauth_signature_method=" + SignParam.Get("oauth_signature_method") + "&oauth_timestamp=" + SignParam.Get("oauth_timestamp")

 //generate url
 fmt.Println("http://example.com/wp-json/wc/v2/products" + "?" + specUrl)
 return "http://example.com/wp-json/wc/v2/products" + "?" + specUrl
}

func client() *http.Client {
 rawClient := &http.Client{}
 rawClient.Transport = &http.Transport{
  TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
 }
 return rawClient
}

func makeRequest() (io.ReadCloser, error){
 makeAuth := client()

 data := "{}"

 body := new(bytes.Buffer)
 encoder := json.NewEncoder(body)
 if err := encoder.Encode(data); err != nil {
  return nil, err
 }

 req, err := http.NewRequest("GET", oauth_generator(), body)
 if err != nil {
  return nil, err
 }
 req.Header.Set("Content-Type", "application/json")
 resp, err := makeAuth.Do(req)
 if err != nil {
  return nil, err
 }
 if resp.StatusCode == http.StatusBadRequest ||
  resp.StatusCode == http.StatusUnauthorized ||
  resp.StatusCode == http.StatusNotFound ||
  resp.StatusCode == http.StatusInternalServerError {
  return nil, fmt.Errorf("Request failed: %s", resp.Status)
 }
 return resp.Body, nil
}

func main() {

 body, err := makeRequest()
 if err != nil {
  panic(err)
 }
 buf := new(bytes.Buffer)
 buf.ReadFrom(body)
 newStr := buf.String()
 
 fmt.Printf(newStr)

}


Reference:
http://woocommerce.github.io/woocommerce-rest-api-docs/#authentication-over-http
https://tools.ietf.org/html/rfc5849#section-3.4.2
http://javascriptexamples.info/search/postman-hmac/15
https://godoc.org/github.com/garyburd/go-oauth/oauth
http://blog.xdite.net/posts/2011/11/19/omniauth-clean-auth-provider-1
https://dotblogs.com.tw/regionbbs/2011/04/21/implement_oauth_authentication_and_authorization
http://leoyeh.me:8080/2015/04/26/%E6%8A%80%E8%A1%93%E6%A8%99%E6%BA%96-OAuth-1/
https://github.com/mikespook/wc-api-golang
https://github.com/woocommerce/woocommerce/blob/master/includes/api/class-wc-rest-authentication.php#L141
https://gist.github.com/hnaohiro/4627658
http://weakyon.com/2017/05/04/something-of-golang-url-encoding.html
https://github.com/woocommerce/woocommerce/issues/11714

沒有留言:

張貼留言

© ERIC RILEY , 自由無須告知轉貼
Background Japanese Sayagata by Olga Libby