ドキュメントには、言語に特化した詳しい説明があまりないので思った以上に苦労したので、その内容(主にJavaによる認証部分の実装手順)を備忘録代わりに残しておこうと思う。
(ただし、利用しているツール、ライブラリ、及びProduct Advertising APIの詳細な内容にはあまり触れないのでそれぞれのサイトを確認してください。)
まず、Product Advertising APIをSOAPによる呼び出し方法(認証の方式)には次の2種類がある。
- WS-Securityを利用せずにSOAPリクエストを処理する方法
- WS-Securityを利用してSOAPリクエストを処理する方法
本稿では、WS-Securityを利用せずにSOAPリクエストを処理する方法を説明する。この方法は、AWS アクセスキー識別子のHMAC-SHA256によるハッシュ情報をSOAPヘッダに埋め込む方法で、Javaの標準機能(JAX-WS)だけで簡単に実装が可能(ただ、実装上Base64エンコードが必要となるため、
その部分だけは、Apache Commons Codecを使用しました)。なお、WS-Securityを利用してSOAPリクエストを処理する方法はこちらを参照。
今回使用したツール、ライブラリは以下の通りです。
ツール、ライブラリ名 | バージョン |
---|
Java Development Kit
7u51(1.7.0_51)
Eclipse IDE for Java EE Developers
4.3.1
Apache Commons Codec
1.3
(1)WSDLからSOAPのクライアントコードを生成する
JDKに付属しているwsimportを使用し下記コマンドを実行し、Product Advertising APIが公開されているWSDLからクライアントコードを生成する。あとで、生成したコードを一部修正するので、ここではソースコードのみ生成する。(WSDLのURLは、日本語ドキュメントだと内容が古いままの箇所があるので、英語ドキュメントも確認すること。)
>wsimport -Xnocompile -s . <Product Advertising API WSDLのURL>
実行後、パッケージcom.amazon.webservices.awsecommerceservice._2011_08_01配下に次のソースが生成される。
Accessories.java Arguments.java AWSECommerceService.java AWSECommerceServicePortType.java Bin.java BrowseNode.java BrowseNodeLookup.java BrowseNodeLookupRequest.java BrowseNodeLookupResponse.java BrowseNodes.java Cart.java CartAdd.java CartAddRequest.java CartAddResponse.java CartClear.java CartClearRequest.java CartClearResponse.java CartCreate.java CartCreateRequest.java CartCreateResponse.java CartGet.java CartGetRequest.java CartGetResponse.java CartItem.java CartItems.java CartModify.java CartModifyRequest.java CartModifyResponse.java Collections.java CorrectedQuery.java CustomerReviews.java DecimalWithUnits.java EditorialReview.java EditorialReviews.java Errors.java HTTPHeaders.java Image.java ImageSet.java Item.java ItemAttributes.java ItemLink.java ItemLinks.java ItemLookup.java ItemLookupRequest.java ItemLookupResponse.java Items.java ItemSearch.java ItemSearchRequest.java ItemSearchResponse.java LoyaltyPoints.java Merchant.java NewReleases.java NonNegativeIntegerWithUnits.java ObjectFactory.java Offer.java OfferAttributes.java OfferListing.java Offers.java OfferSummary.java OperationRequest.java OtherCategoriesSimilarProducts.java package-info.java Price.java Promotion.java Promotions.java Property.java RelatedItem.java RelatedItems.java Request.java SavedForLaterItems.java SearchBinSet.java SearchBinSets.java SearchResultsMap.java SimilarityLookup.java SimilarityLookupRequest.java SimilarityLookupResponse.java SimilarProducts.java SimilarViewedProducts.java StringWithUnits.java TopItemSet.java TopSellers.java Tracks.java VariationAttribute.java VariationDimensions.java Variations.java VariationSummary.java
(2)AWS アクセスキー識別子のHMAC-SHA256によるハッシュ情報をSOAPヘッダを埋め込むSOAPハンドラを実装する
AWS アクセスキー識別子のHMAC-SHA256によるハッシュ情報をSOAPヘッダを埋め込むSOAPハンドラを実装する。SOAPヘッダには次の3つの要素を埋め込む必要がある。- AWSAccessKeyId
- AWS アクセスキー識別子(Product Advertising APIのアカウントID)
- Timestamp
- リクエストに含めるタイムスタンプ
- Signatur
- AWS 秘密キー(Product Advertising APIのアカウントIDに対応する秘密キー)を元に、Action および Timestamp パラメータの連結した文字列のHMAC-SHA256のハッシュ値
SOAPヘッダイメージ
<S:Header xmlns:aws="http://security.amazonaws.com/doc/2007-01-01/"> <aws:AWSAccessKeyId">xxxxxxxxxxxxxxxxxxxx</aws:AWSAccessKeyId"> <aws:Timestamp">2014-02-15T10:07:43Z</aws:Timestamp"> <aws:Signature">zI0LD8X7yclKaBzk0b5g8L0G5P2TbC3ncxCD2ng2J3E=</aws:Signature"> </S:Header">
実際の実装は、SOAPのメッセージ・ハンドラ(AWSECommerceServiceProxyHandler) クラスを実装し、SOAPのメッセージ・ハンドラのhandleMessage()メソッド内でヘッダへの埋め込み処理を実装する。
package com.amazon.webservices.awsecommerceservice._2011_08_01; import java.io.UnsupportedEncodingException; import java.net.URL; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Set; import java.util.TimeZone; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.xml.namespace.QName; import javax.xml.soap.Name; import javax.xml.soap.SOAPEnvelope; import javax.xml.soap.SOAPHeader; import javax.xml.soap.SOAPHeaderElement; import javax.xml.soap.SOAPMessage; import javax.xml.ws.BindingProvider; import javax.xml.ws.handler.MessageContext; import javax.xml.ws.handler.soap.SOAPHandler; import javax.xml.ws.handler.soap.SOAPMessageContext; import org.apache.commons.codec.binary.Base64; public class AWSECommerceServiceProxyHandler implements SOAPHandler<SOAPMessageContext> { /** シグネチャ(ハッシュ計算)用のアルゴリズム */ private static final String SIGNATURE_ALGORITHM = "HmacSHA256"; /** AWSの認証用のネームスペース */ private static final String AWS_SECURITY_NS = "http://security.amazonaws.com/doc/2007-01-01/"; /** AWSの認証用のネームスペースのプレフィックス */ private static final String AWS_SECURITY_NS_PREFIX = "aws"; /** AWSの認証用のヘッダパラメータ(AWSAccessKeyId) */ private static final String AWS_HEADER_AWS_PARAMETER_ACCESSKEYID = "AWSAccessKeyId"; /** AWSの認証用のヘッダパラメータ(Timestamp) */ private static final String AWS_HEADER_AWS_PARAMETER_TIMESTAMP = "Timestamp"; /** AWSの認証用のヘッダパラメータ(Signature) */ private static final String AWS_HEADER_AWS_PARAMETER_SIGNATURE = "Signature"; /** AWS アクセスキー識別子 */ private String awsAccessKeyId = null; /** AWS 秘密キー */ private String awsSecretKey = null; /** 秘密鍵 */ private SecretKeySpec keySpec = null; /** メッセージ認証コード */ private Mac mac = null; /** * コンストラクタ */ public AWSECommerceServiceProxyHandler() { init(); } @Override public void close(MessageContext mc) { } @Override public boolean handleFault(SOAPMessageContext mc) { return false; } @Override public boolean handleMessage(SOAPMessageContext mc) { // アウトバウンド以外のメッセージは無視する。 if (!((Boolean) mc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY))) { return false; } String action = getAction(mc); String timestamp = getTimestamp(); String signature = null; try { signature = calculateSignature(action, timestamp); } catch (Exception e) { throw new RuntimeException(e); } try { SOAPMessageContext smc = (SOAPMessageContext) mc; SOAPMessage message = smc.getMessage(); SOAPEnvelope envelope = message.getSOAPPart().getEnvelope(); // メッセージにSOAPヘッダーが存在していない場合、SOAPヘッダを生成する。 if (envelope.getHeader() == null) { envelope.addHeader(); } SOAPHeader header = envelope.getHeader(); header.addNamespaceDeclaration(AWS_SECURITY_NS_PREFIX, AWS_SECURITY_NS); Name awsidName = envelope.createName( AWS_HEADER_AWS_PARAMETER_ACCESSKEYID, AWS_SECURITY_NS_PREFIX, AWS_SECURITY_NS); Name tsName = envelope.createName( AWS_HEADER_AWS_PARAMETER_TIMESTAMP, AWS_SECURITY_NS_PREFIX, AWS_SECURITY_NS); Name sigName = envelope.createName( AWS_HEADER_AWS_PARAMETER_SIGNATURE, AWS_SECURITY_NS_PREFIX, AWS_SECURITY_NS); SOAPHeaderElement akidElement = header.addHeaderElement(awsidName); SOAPHeaderElement tsElement = header.addHeaderElement(tsName); SOAPHeaderElement sigElement = header.addHeaderElement(sigName); akidElement.addTextNode(awsAccessKeyId); tsElement.addTextNode(timestamp); sigElement.addTextNode(signature); } catch (Exception e) { throw new RuntimeException(e); } return true; } @Override @SuppressWarnings("unchecked") public Set<QName> getHeaders() { return java.util.Collections.EMPTY_SET; } /** * 初期化メソッド */ private void init() { // AWS アクセスキー識別子、AWS 秘密キーの設定(必要に応じて外出しにする) awsAccessKeyId = "xxxxxxxxxxxxxxxxxxxxxx"; awsSecretKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; try { byte[] bytes = awsSecretKey.getBytes("UTF-8"); this.keySpec = new SecretKeySpec(bytes, SIGNATURE_ALGORITHM); this.mac = Mac.getInstance(SIGNATURE_ALGORITHM); this.mac.init(keySpec); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (InvalidKeyException e) { throw new RuntimeException(e); } } /** * エンドポイントのURLからAction名を取得する。 * * @param mc * @return アクション */ private String getAction(SOAPMessageContext mc) { String action = null; try { String endpointAddress = (String) mc .get(BindingProvider.SOAPACTION_URI_PROPERTY); String actionUri = (new URL(endpointAddress)).getPath(); String tokens[] = actionUri.split("/"); action = tokens[tokens.length - 1]; } catch (Exception e) { new RuntimeException(e); } return action; } /** * UTCによるタイムスタンプ(ISO8601フォーマット)の生成 * * @return UTCによるタイムスタンプ(ISO8601フォーマット) */ private String getTimestamp() { Calendar calendar = Calendar.getInstance(); SimpleDateFormat is08601 = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss'Z'"); is08601.setTimeZone(TimeZone.getTimeZone("UTC")); return is08601.format(calendar.getTime()); } /** * シグネチャ(ハッシュ値)の計算 アクションとタイムスタンプを連結した文字列を元にハッシュ値を計算する。 * * @param action * 実行するアクション * @param timestamp * UTCによるタイムスタンプ(ISO8601フォーマット) * @return シグネチャ(ハッシュ値) * @throws Exception * If there were errors or missing, required classes when trying * to calculate the hash. */ private String calculateSignature(String action, String timestamp) { String toSign = (action + timestamp); byte[] sigBytes = mac.doFinal(toSign.getBytes()); return new String(Base64.encodeBase64(sigBytes)); } }
AWSECommerceServiceProxyHandlerの説明
- 70~121行目
- ハッシュ情報のヘッダへの埋め込みを行う部分。本実装では72~75行目でアウトバウンド(リクエストメッセージ)以外は無視するようにしている。77~84行目ではヘッダに埋め込むためのアクション、タイムスタンプ、ハッシュ値を取得し、86~118行目で取得した値のヘッダへの埋め込みを行う。
- 125~127行目
- ヘッダはhandleMessage()メソッドで動的な内容を含め設定するので、ここでは一旦空のヘッダを返している。
- 132~151行目
- AWS アクセスキー識別子、AWS 秘密キーの設定と、それらを元にしたハッシュ計算用の秘密鍵を設定している。ここで自分のAWS アクセスキー識別子、AWS 秘密キーの設定をする。また、AWS アクセスキー識別子、AWS 秘密キーは外出しにして、設定ファイル等から取得するようにしてもよい。
- 159~171行目
- エンドポイントのURLから実行するアクション(エンドポイントのURLの一番最後の階層)を取得する。
- 178~185行目
- 現在時刻(UTC)からタイムスタンプの文字列(ISO8601フォーマット)を生成する。
(3)実装したSOAPハンドラ(AWSECommerceServiceProxyHandler)をハンドラ・チェーンに設定する
ハンドラ・チェーンの設定ファイルの作成
(2)で実装したAWSECommerceServiceProxyHandlerをハンドラ・チェーンに設定するために、パッケージ内にハンドラ・チェーンの設定ファイル(AWSECommerceService-HandlerChain.xml)を作成(下記参照)し、パッケージ(com.amazon.webservices.awsecommerceservice._2011_08_01)のフォルダ内に配置する。<?xml version="1.0" encoding="UTF-8"?> <handler-chains xmlns="http://java.sun.com/xml/ns/javaee"> <handler-chain> <handler> <handler-name>proxyHandler</handler-name> <handler-class>com.amazon.webservices.awsecommerceservice._2011_08_01.AWSECommerceServiceProxyHandler</handler-class> </handler> </handler-chain> </handler-chains>
ハンドラ・チェーンの設定
作成したハンドラ・チェーンの設定ファイルを実装に組み込むために、(1)で生成したProduct Advertising APIのAWSECommerceServiceクラスのAWSECommerceService()メソッドにハンドラ・チェーンのアノテーションを追加する(下記参照)。@WebServiceClient(name = "AWSECommerceService", targetNamespace = "http://webservices.amazon.com/AWSECommerceService/2011-08-01", wsdlLocation = "http://webservices.amazon.com/AWSECommerceService/AWSECommerceService.wsdl") public class AWSECommerceService extends Service ↓ @WebServiceClient(name = "AWSECommerceService", targetNamespace = "http://webservices.amazon.com/AWSECommerceService/2011-08-01", wsdlLocation = "http://webservices.amazon.com/AWSECommerceService/AWSECommerceService.wsdl") @HandlerChain(file="AWSECommerceService-HandlerChain.xml") ←この行を追加する。 public class AWSECommerceService extends Service
以上で機能的な部分の実装は完了。
(4)Product Advertising APIを呼び出す
ここでは単純にmain()メソッドからProduct Advertising APIを実行するサンプルを実装する。import java.util.ArrayList; import java.util.List; import javax.xml.ws.Holder; import javax.xml.ws.WebServiceRef; import com.amazon.webservices.awsecommerceservice._2011_08_01.AWSECommerceService; import com.amazon.webservices.awsecommerceservice._2011_08_01.AWSECommerceServicePortType; import com.amazon.webservices.awsecommerceservice._2011_08_01.Item; import com.amazon.webservices.awsecommerceservice._2011_08_01.ItemSearchRequest; import com.amazon.webservices.awsecommerceservice._2011_08_01.Items; import com.amazon.webservices.awsecommerceservice._2011_08_01.OperationRequest; public class AmazonService { @WebServiceRef private static AWSECommerceService AWSECommerceService; /** AWS アクセスキー識別子 */ private static String awsAccessKeyId = "xxxxxxxxxxxxxxxxxxxx"; /** AmazonアソシエイトのアカウントID */ private static String associateTag = "XXXXXXXXXX-nn"; /** 共通のリクエストパラメータ(Validate) */ private static String validate = "False"; /** 共通のリクエストパラメータ(XMLEscaping) */ private static String xmlEscaping = "Single"; public static void main(String[] args) { // サービスのエンドポイントの取得 AWSECommerceService = new AWSECommerceService(); AWSECommerceServicePortType endPoint = AWSECommerceService .getAWSECommerceServicePortJP(); // 変数の初期化 String marketplaceDomain = null; ItemSearchRequest shared = null; List<ItemSearchRequest> request = new ArrayList<ItemSearchRequest>(); Holder<OperationRequest> operationRequest = new Holder<OperationRequest>(); Holder<List<Items>> items = new Holder<List<Items>>(); // ItemSearch用のリクエストオブジェクトの生成 ItemSearchRequest itemSearchRequest = new ItemSearchRequest(); itemSearchRequest.setKeywords("新居昭乃"); itemSearchRequest.setAvailability("Available"); itemSearchRequest.setSearchIndex("Blended"); itemSearchRequest.getResponseGroup().add("Medium"); itemSearchRequest.getResponseGroup().add("ItemAttributes"); request.add(itemSearchRequest); // サービス(itemSearch)の実行 endPoint.itemSearch(marketplaceDomain, awsAccessKeyId, associateTag, xmlEscaping, validate, shared, request, operationRequest, items); // 検索結果の表示 for (Item item : items.value.get(0).getItem()) { System.out.println(item.getItemAttributes().getTitle()); System.out.println("\t" + item.getDetailPageURL()); } } }
- 19~26行目
- AWSアクセス識別子、amazonアソシエイトIDなど必要な初期値を設定する。(認証はヘッダ情報で処理されるため、ここ設定されるAWSアクセス識別子は実際見ていない模様。また、共通のリクエストパラメータの設定が効いてないかも、Validateの設定を変えても結果が変わらなかった。この点はちょっと不明)
- 30~33行目
- (1)で生成したサービスクラスから、エンドポイント(ここでは日本サイト)を取得する。
- 35~50行目
- itemSearchを呼び出すのに必要なオブジェクトを初期化・設定する。設定する内容はProduct Advertising APIを参照。
- 52~55行目
- itemSearchの実行
- 56~60行目
- 検索結果を表示
実際のSOAP呼び出しの際、SOAPメッセージに必要な要素がない場合、だいたい400:Bad Requestとなり、IDなどの認証情報に誤りがある場合403:Forbiddenとなるようだ。
0 件のコメント:
コメントを投稿