2014年2月16日日曜日

JavaによるProduct Advertising APIのSOAP呼び出し(WS-Securityを利用してSOAPリクエストを処理する方法)

なにかアフィリエイトのサイトを作ろうと、まずは、JavaによるProduct Advertising APIのSOAP呼び出し方法を調べてみた。
ドキュメントには、言語に特化した詳しい説明があまりないので思った以上に苦労したので、その内容(主にJavaによる認証部分の実装手順)を備忘録代わりに残しておこうと思う。
(ただし、利用しているツール、ライブラリ、及びProduct Advertising APIの詳細な内容にはあまり触れないのでそれぞれのサイトを確認してください。)

まず、Product Advertising APIをSOAPによる呼び出し方法(認証の方式)には次の2種類がある。

  • WS-Securityを利用せずにSOAPリクエストを処理する方法
  • WS-Securityを利用してSOAPリクエストを処理する方法

本稿では、WS-Securityを利用してSOAPリクエストを処理する方法を説明する。この方法は、WS-Security1.0を利用し認証を行う。JDK単体ではWS-Seurityまでサポートされていないようなので、JAX-WSの実装としてApache CXFを利用する。また、署名に使用する証明書は、keytoolが公開鍵と秘密鍵が別になった証明書のインポートができない(ここは調査不足の可能性も)ようなので、amazonから提供される証明書ではなく、keytoolで生成した自己署名証明書を使用する。

なお、「WS-Securityを利用せずにSOAPリクエストを処理する方法」はこちらを参照。
今回使用したツール、ライブラリは以下の通りです。

ツール、ライブラリ名バージョン

Java Development Kit
7u51(1.7.0_51)
Eclipse IDE for Java EE Developers
4.3.1
Apache CXF
2.7.8
Tomcat
7.5.0

(1)自己署名証明書(秘密鍵・公開鍵のキーペア)の準備


amazonで有効なアルゴリズムはRSAなので、キーアルゴリズムとしてRSAを指定し、自己署名証明書(秘密鍵・公開鍵のキーペア)を生成する。有効期間は、amazonで生成可能な証明書の期間と同じ1年(365日)を設定している。
>keytool -genkeypair -keyalg RSA -dname <X.500識別名> -alias <キーエイリアス> -keypass <キーパスワード> -keystore <キーストアファイル> -storepass <キーストアパスワード> -validity 365

次に生成した自己署名証明書(秘密鍵・公開鍵のキーペア)から、amazonにアップロードするための公開鍵を同じくkeytoolを使用しRFC 1421 証明書符号化のフォーマット(-rfcオプション)で生成する。

>keytool -exportcert -alias <キーエイリアス> -keystore  <キーストアファイル> -file  <公開鍵ファイル> -storepass <キーストアパスワード> -rfc

生成された公開鍵ファイルは、AWS Management ConsoleのSecurity Credentialsからamazonにアップロードする。

X.500識別名
とりあえず適当に"cn=<名前(英字)>, c=JP"とか指定しておく。
キーエイリアス
証明書(秘密鍵・公開鍵のキーペア)にアクセスするためのエイリアス名
キーストアパスワード
キーストアにアクセスするためのパスワード
キーパスワード
証明書(秘密鍵)にアクセスするためのパスワード
キーストアファイル
証明書(秘密鍵・公開鍵のキーペア)を格納するキーストアファイル
キーストアファイル
amazonにアップロードする公開鍵ファイル(RFC 1421 証明書符号化のフォーマット)
※ここで設定したキーストアファイル、キーストアパスワード、キーエイリアス、キーパスワードは、実際のプログラムでも使用するため、以降ここで定義した名前を使用する。

(2)EclipseにApache CXF、Tomcatを設定する

Apache CXFによりコード生成をするため、まずEclipseにApache CXFの設定を行う。また、クライアントコードを生成するだけなのに、なぜかDynamic web Project(Web Serverの実装がないと)でないとApache CXFのコード生成ができないようなのでTomcatも併せてEclipseに設定する。

Apache CXFの設定
Apache CXFをダウンロードし、適当な場所に展開する。Eclipseの「Window」→「Preferences」→「Web Services」→「CXF 2.x Preferences」を開き、「add」ボタンを押下し、展開した場所を設定する。

追加後、一覧に追加したApache CXFの実装がバージョンと共に表示されるので、追加したApache CXFにチェックを入れる。そのほかの設定はとりあえずデフォルトのままでよい。


Tomcatの設定
同様にTomcatダウンロードし、適当な場所に展開する。Eclipseの「Window」→「Preferences」→「Server」→「Runtime Environments」を開き、「add」ボタンを押下する。


New Server Runtime Environmentが開くので「Apache」→「Apache Tomcat v7.0」を選択し、展開した場所を設定する。


追加後、一覧に追加したTomcatの実装がバージョンと共に表示される。


(3)WSDLからSOAPのクライアントコードを生成する


プロジェクト(Dynamic Web Project)の作成
クライアントコードを生成するためプロジェクトをEclipseのDynamic Web Projectとして作成し、プロジェクトのPropertiesを開き、「Java Build Path」の「Add Library」を押下し、「CXF Runtime」を追加する。


追加後、一覧に追加したApache CXF Libraryが表示される。


クライアントコードの生成
Eclipseの「File」→「New」→「Other」を選択し、Newウィザードで「Web Services」→「Web Service Client」を選択する。「Web Service Client」ウィザードが開くので、「Server definition」にProduct Advertising APIのWSDLのURLを設定し、「Configuration」のServer Runtimeに「Tomcat v7.0 Server」、Web service runtimeに「Apache CXF 2.x」を設定し「Next」をクリックする。


次に、WSDL2Javaの設定ウィザードが開き、自動生成されたパッケージ名が「com.amazon.webservices.awsecommerceservice.2011-08-01」となっており、このままだとJavaの規約違反となるため「com.amazon.webservices.awsecommerceservice._2011_08_01」に変更し「Finish」ボタンを押下する。


「Finish」ボタンを押下後、パッケージcom.amazon.webservices.awsecommerceservice._2011_08_01配下に次のソースが生成される。

Accessories.java
Arguments.java
AWSECommerceService.java
AWSECommerceServicePortType.java
AWSECommerceServicePortTypeImpl.java
AWSECommerceServicePortTypeImpl1.java
AWSECommerceServicePortTypeImpl10.java
AWSECommerceServicePortTypeImpl2.java
AWSECommerceServicePortTypeImpl3.java
AWSECommerceServicePortTypeImpl4.java
AWSECommerceServicePortTypeImpl5.java
AWSECommerceServicePortTypeImpl6.java
AWSECommerceServicePortTypeImpl7.java
AWSECommerceServicePortTypeImpl8.java
AWSECommerceServicePortTypeImpl9.java
AWSECommerceServicePortType_AWSECommerceServicePortCA_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortCN_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortDE_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortES_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortFR_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortIN_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortIT_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortJP_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortUK_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortUS_Client.java
AWSECommerceServicePortType_AWSECommerceServicePort_Client.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

(4)Product Advertising APIを呼び出す

Product Advertising APIを呼び出すときに、WS-Securityを利用するためのApache WSS4J用のプロパティファイル(client_sign.properties)と、暗号化を行う際に、(1)で生成した証明書(秘密鍵)へアクセスするためのキーパスワードを設定するコールバッククラス(ClientPasswordCallback)を先に作成する。

Apache WSS4J用のプロパティファイル(client_sign.properties)
org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=<キーストアパスワード>
org.apache.ws.security.crypto.merlin.keystore.alias=<キーエイリアス>
org.apache.ws.security.crypto.merlin.keystore.file=<キーストアファイル>

コールバッククラス(ClientPasswordCallback)クラス
import java.io.IOException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.ws.security.WSPasswordCallback;

public class ClientPasswordCallback implements CallbackHandler {

    /** 証明書(秘密鍵)にアクセスするためのキーパスワード */
    private static String keyPassword = "<キーパスワード>";

    public void handle(Callback[] callbacks) throws IOException,
            UnsupportedCallbackException {

        WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];

        // キーパスワードの設定
        pc.setPassword(keyPassword);
    }

}

最後にProduct Advertising APIを実行するmain()メソッドを持つクラスを実装する。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.ws.WebServiceRef;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.ws.security.handler.WSHandlerConstants;

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.ItemSearch;
import com.amazon.webservices.awsecommerceservice._2011_08_01.ItemSearchRequest;
import com.amazon.webservices.awsecommerceservice._2011_08_01.ItemSearchResponse;

public class AmazonService {

    @WebServiceRef
    private static AWSECommerceService AWSECommerceService;

    /** WS-Security(シグネチャのキー) */
    private final static String WS_SECURITY_SIGNATURE_KEY_IDENTIFIER = "DirectReference";
    /** WS-Security(シグネチャのキーアルゴリズム) */
    private final static String WS_SECURITY_SIGNATURE_ALGORITHM = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";

    /** 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) throws Exception {

        // サービスのエンドポイントの取得
        AWSECommerceService = new AWSECommerceService();
        AWSECommerceServicePortType endPoint = AWSECommerceService
                .getAWSECommerceServicePortJP();
        Client client = ClientProxy.getClient(endPoint);

        // WS-Securityを使用するためのプロパティ設定
        Map<String, Object> outProps = new HashMap<String, Object>();
        outProps.put(WSHandlerConstants.USER, "<キーエイリアス>");
        outProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.TIMESTAMP
                + " " + WSHandlerConstants.SIGNATURE);
        outProps.put(WSHandlerConstants.SIG_KEY_ID,
                WS_SECURITY_SIGNATURE_KEY_IDENTIFIER);
        outProps.put(WSHandlerConstants.SIG_ALGO,
                WS_SECURITY_SIGNATURE_ALGORITHM);
        outProps.put(WSHandlerConstants.SIG_PROP_FILE, "client_sign.properties");
        outProps.put(WSHandlerConstants.PW_CALLBACK_CLASS,
                ClientPasswordCallback.class.getName());
        WSS4JOutInterceptor wssOut = new WSS4JOutInterceptor(outProps);
        client.getOutInterceptors().add(wssOut);

        // ItemSearch用のリクエストオブジェクトの生成
        List<ItemSearchRequest> request = new ArrayList<ItemSearchRequest>();
        String marketplaceDomain = null;
        ItemSearchRequest itemSearchRequest = new ItemSearchRequest();
        itemSearchRequest.setKeywords("新居昭乃");
        itemSearchRequest.setAvailability("Available");
        itemSearchRequest.setSearchIndex("Blended");
        itemSearchRequest.getResponseGroup().add("Medium");

        itemSearchRequest.getResponseGroup().add("ItemAttributes");
        request.add(itemSearchRequest);

        ItemSearch itemSearch = new ItemSearch();
        itemSearch.setAssociateTag(associateTag);
        itemSearch.setAWSAccessKeyId(awsAccessKeyId);
        itemSearch.setMarketplaceDomain(marketplaceDomain);
        itemSearch.setXMLEscaping(xmlEscaping);
        itemSearch.setValidate(validate);
        itemSearch.getRequest().add(itemSearchRequest);

        // サービス(itemSearch)の実行
        ItemSearchResponse itemSearchResponse = endPoint.itemSearch(itemSearch);

        // 検索結果の表示
        for (Item item : itemSearchResponse.getItems().get(0).getItem()) {
            System.out.println(item.getItemAttributes().getTitle());
            System.out.println("\t" + item.getDetailPageURL());
        }

    }

}

41~45行目
(1)で生成したサービスクラスから、エンドポイント(ここでは日本サイト)を取得する。
47~60行目
WS-Securityを使用するためのプロパティの設定する。WS-Securityのアクション(WSHandlerConstants.ACTION)には、WSHandlerConstants.TIMESTAMPとWSHandlerConstants.SIGNATUREを指定し、WS-Securityのユーザー(WSHandlerConstants.USER)には、(1)で生成したキーストアの証明書のキーエイリアスを指定し、シグネチャを生成するアルゴリズム(WSHandlerConstants.SIG_ALGO)には「http://www.w3.org/2000/09/xmldsig#rsa-sha1」を指定し、シグネチャーの識別子(WSHandlerConstants.SIG_KEY_ID)にはメッセージに埋め込む方式の「DirectReference」を指定する。
62~80行目
itemSearchを呼び出すのに必要なオブジェクトを初期化・設定する。設定する内容はProduct Advertising APIを参照。
82~83行目
itemSearchの実行
85~89行目
検索結果を表示
これで、WS-Securityを利用する方法で実行が可能。
実際のSOAP呼び出しの際、SOAPメッセージに必要な要素がない場合、だいたい400:Bad Requestとなり、IDなどの認証情報に誤りがある場合403:Forbiddenとなる。

0 件のコメント:

コメントを投稿