やったこと
- Amazon Product Advertising APIを使って書籍情報を取得すること
- 書籍情報の検索フックにはISBNコードを使ったこと
- 利用する際にハマったこと
事前準備
以下は事前にやってあるものとします。
refs:
実装
※ 書籍のAPIを取得するところのみ抜粋
ディレクトリ構造
- PROJECT - conf - token_cred.json - src - project - main.go - handler/ - amazon.go
amazon.go の実装は下記
const ( EC_SERVICE_ENDPOINT = "webservices.amazon.co.jp" EC_SERVICE_URI = "/onca/xml" ) func readConf() ([]byte, error) { f, err := os.Open("./conf/token_cred.json") if err != nil { fmt.Printf("token_cred.json open error: err; %v", err) return nil, err } b, err := ioutil.ReadAll(f) if err != nil { fmt.Printf("json file read error: err; %v", err) return nil, err } return b, nil } func SearchISBN(w http.ResponseWriter, r *http.Request) { b, err := readConf() if err != nil { fmt.Printf("readConf error. err: %v", err) return } var cred model.AmazonTokenCred if err := json.Unmarshal(b, &cred); err != nil { fmt.Printf("json unmarshal error. err: %v", err) return } params := url.Values{} params.Set("Service", "AWSECommerceService") params.Set("Operation", "ItemLookup") params.Set("ItemId", "ISBNコード") params.Set("IdType", "ISBN") params.Set("SearchIndex", "Books") params.Set("Timestamp", time.Now().UTC().Format(time.RFC3339)) params.Set("AWSAccessKeyId", cred.AccessKeyId) params.Set("AssociateTag", cred.AssociateTag) params.Set("ResponseGroup", "Images,ItemAttributes,Offers") // 署名 canonical_params := params.Encode() strToSign := fmt.Sprintf("GET\n%v\n%v\n%v", EC_SERVICE_ENDPOINT, EC_SERVICE_URI, canonical_params) mac := hmac.New(sha256.New, []byte(cred.SecretKeyId)) mac.Write([]byte(strToSign)) signature := url.QueryEscape(base64.StdEncoding.EncodeToString(mac.Sum(nil))) canonical_params = fmt.Sprintf("%v&Signature=%v", canonical_params, signature) // http request res, err := http.Get(fmt.Sprintf("http://%v%v?%v", EC_SERVICE_ENDPOINT, EC_SERVICE_URI, canonical_params)) if err != nil { fmt.Printf("response error. err: %v", err) return } // response はよしなに整形する }
ハマったポイント
APIのDocs(https://images-na.ssl-images-amazon.com/images/G/09/associates/paapi/dg/index.html?RG_ItemAttributes.html の例のところ)に書いてある遠りのコード書いたら
The request must contain the parameter Timestamp.
The request must contain the parameter Signature.
の2つのエラーにぶち当たりました。
REST APIのドキュメントをそのまま叩いたのは以下
curl -i "http://ecs.amazonaws.com/onca/xml?Service=AWSECommerceService&AWSAccessKeyId=XXXXXX&Operation=ItemLookup&ItemId=B00008OE6I" HTTP/1.1 400 Bad Request Date: Fri, 08 Dec 2017 19:10:17 GMT Server: Apache-Coyote/1.1 Vary: Accept-Encoding,User-Agent nnCoection: close Transfer-Encoding: chunked <?xml version="1.0"?> <ItemLookupErrorResponse xmlns="http://ecs.amazonaws.com/doc/2005-10-05/"><Error><Code>MissingParameter</Code><Message>The request must contain the parameter Signature.</Message></Error><RequestID>831754e7-5761-4d0a-adae-6febc205949b</RequestID></ItemLookupErrorResponse>⏎
TimeStampについて
今の時刻をISO8601の形式で使います。
GOにおいては time.RFC3339
で求められてる形式で時刻を取得します。
署名(Signature)について
AmazonのProduct Advertising APIで使用する署名(Signature)は 発行したSecretKeyIdを使ってhmacでハッシュ化されたものをbase64でencodeした値
です。
署名の作成は以下のphpのコードを参考にgoで書き直しました。
githubに以下の今回使おうとしているAPIのgoのクライアントを見つけたのでこちらも参考しました。
署名作成でハマったこと
書いたコードの中で署名を生成している箇所は以下
// 署名 canonical_params := params.Encode() strToSign := fmt.Sprintf("GET\n%v\n%v\n%v", EC_SERVICE_ENDPOINT, EC_SERVICE_URI, canonical_params) mac := hmac.New(sha256.New, []byte(cred.SecretKeyId)) mac.Write([]byte(strToSign)) signature := url.QueryEscape(base64.StdEncoding.EncodeToString(mac.Sum(nil))) canonical_params = fmt.Sprintf("%v&Signature=%v", canonical_params, signature)
url.Values
でクエリパラメータをセットして Encode
したら完了かと思っていたのですが、上記の署名生成のコードの最後の行で記載しているように Signatureは最後につけないといけない
というところで思いっきりハマりました。
例は以下
# params.Encode()でqueryparamsを作成したときのクエリパラメータ "AWSAccessKeyId=[AccessKeyId]&AssociateTag=[AssociateTag]&IdType=ISBN&ItemId=9784774193328&Operation=ItemLookup&ResponseGroup=Images,ItemAttributes,Offers&SearchIndex=Books&Service=AWSECommerceService&Signature=[生成した署名]&Timestamp=2017-12-10T06:16:16Z" # -> 403 Forbidden # 文字列結合でsignatureをつけたときのクエリパラメータ "AWSAccessKeyId=[AccessKeyId]&AssociateTag=[AssociateTag]&IdType=ISBN&ItemId=9784774193328&Operation=ItemLookup&ResponseGroup=Images,ItemAttributes,Offers&SearchIndex=Books&Service=AWSECommerceService&Timestamp=2017-12-10T06:16:16Z&Signature=[生成した署名]" # status OK
当初 params.Encode()
で文字列作成をしてし待ってましたが、params.Encode()
はqueryのkeyを自動でsortしてしまい、 Signature
が末尾に来ません。
そのため、params.Encode()
でクエリパラメータを生成し、署名内容とurlは文字列結合の時と同じでも、403を返して来てしまいました。
まとめ
Amazon Product Advertising APIは署名を自前で作らないといけなかったりして忘れかけたことを思い出すきっかけをくれたので久しぶりに触ってみてよかったです。
APIのテストで自動的にphpとjavaのコードは生成してくれますが、こういうときにgoとかで書き直してみるのもいいなと思います。
とりあえず書いたコードはこちら