emahiro/b.log

勉強記録と書評とたまに長めの呟きを書きます

Amazon Product Advertising APIを使ってISBNコードから書籍情報を取得する

やったこと

  • Amazon Product Advertising APIを使って書籍情報を取得すること
  • 書籍情報の検索フックにはISBNコードを使ったこと
  • 利用する際にハマったこと

事前準備

以下は事前にやってあるものとします。

  • Amazon Associate への登録
    • Associate TagにこのIDを使う
  • AcessKeyIdとAccessSecretIdを使う

refs:

blog.apitore.com

実装

※ 書籍の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)について

AmazonProduct Advertising APIで使用する署名(Signature)は 発行したSecretKeyIdを使ってhmacでハッシュ化されたものをbase64でencodeした値 です。

署名の作成は以下のphpのコードを参考にgoで書き直しました。

qiita.com

qiita.com

githubに以下の今回使おうとしているAPIのgoのクライアントを見つけたのでこちらも参考しました。

github.com

署名作成でハマったこと

書いたコードの中で署名を生成している箇所は以下

// 署名
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のテストで自動的にphpjavaのコードは生成してくれますが、こういうときにgoとかで書き直してみるのもいいなと思います。

とりあえず書いたコードはこちら

github.com

xmlのレスポンスをstructにマッピングしたりはまた次回!