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 也支援的方式:
- 導向用戶來請求一個 token (令牌),得到 token 之後伺服器就可以直接拿 token 來訪問 WooCommerce 的資源 (訂單、商品資訊...etc)
- 從 WooCommerce 後台拿到 consumer_key, consumer_secret,就直接發送單一請求或是用 Basic Auth 請求資源。
- 拿 consumer_key, consumer_secret 直接向伺服器交換 token,就可拿 token 來請求資源。
本文我主要以 2. 為解釋。
以下是參考文獻:
(連結) (以 1. 的方法為主,介紹 oauth1.0a, 2.0 的驗證流程)
(連結) (Oauth1 認證方式(英文))
(連結) (Oauth 1.0 認證說明)
(連結) (Oauth 1.0 / 2.0 說明)
簡述 2. 的單一請求實作流程:
- 持有 consumer_key, consumer_secret
- 建立一個簽章 HMAC-SHS256 (Signature) ,並且用 consumer_secret + "&" 作為簽名
把 consumer_secret 加上 '&' 是因為有可能會加入 token 的密鑰,根據 Oauth1.0a 規範,不論有沒有,就算是空的 secret都要在兩者之間加上一個 '&' (請參考這裡)。 - 將簽章參數 (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
沒有留言:
張貼留言