JDK 6でAmazon Product Advertising API をSOAP呼び出しする

SOAPでAPAAPIにアクセスする方法のメモ。仕事で以前つくったものが動かなくなっていたので、それの修正のために調べた。以前からあったAPIキーだけじゃなくて、秘密キーでシグネチャつくれって変更。

具体的にはWSセキュリティを利用せずに SOAP リクエストを処理する方法をJDK1.6に付属のJAX-WSで実装する方法。(BASE64用にcommons-codecもつかってる)

簡単にいうとAWSECommerceService#setHandlerResolverというのがあり、そこでSOAPHandlerを設定することによりSOAPドキュメントをごにょごにょできる。今回はsoap:HeaderにAWSAccessKeyIdとSignatureとTimestampを追加する

このページがいいとこまできてるんだけど、肝心なところが抜けてる

ここで抜けてるところがどんな内容なのかわかった。

ごにょごにょできるとこはhandleMessageという部分なんだけど、このメソッドは送信時、受信時ともに呼ばれるMessageContext.MESSAGE_OUTBOUND_PROPERTYの値を調べることによって、いま送信か受信かがわかる。

Signatureをつくるにはいま何のアクション(メソッド)をよんでいるかがわからないといけないんだけど、これはSOAP ACTION URIから推測した。handleMessage内でMessageContextの"javax.xml.ws.soap.http.soapaction.uri"の値を調べるhttp://soap.amazon.com/ItemSearchとなっているので/以下をとった。

あと日本に検索するときはAWSECommerceServicePortTypeのプロパティを変更する。

wsimportの例 -pで生成されるスタブのパッケージを指定する。

wsimport -p com.amazon.aws.ecommers -b binding.xml http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl

SOAPヘッダーにAWSAccessKeyId,Timestamp, Signatureを入れるSOAPHandler

package com.amazon.aws.ecommerce;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
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.SOAPElement;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
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 AmazonAuthenticationHandler implements SOAPHandler<SOAPMessageContext> {

  private Mac hmacSha_256;
  private String accessKey;

  public AmazonAuthenticationHandler(String accessKey, String secretKeyString)
      throws NoSuchAlgorithmException, InvalidKeyException {
    SecretKeySpec secretKey = new SecretKeySpec(secretKeyString.getBytes(),"HmacSHA256");
    Mac hmac_sha_256 = Mac.getInstance("HmacSHA256");
    hmac_sha_256.init(secretKey);
    this.accessKey = accessKey;
    this.hmacSha_256 = hmac_sha_256;
  }

  @Override
  public void close(MessageContext context) {
  }

  @Override
  public boolean handleFault(SOAPMessageContext context) {
    return false;
  }

  @Override
  public boolean handleMessage(SOAPMessageContext context) {
    Boolean outboundProperty = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
    if (outboundProperty) {
      return handleOutBound(context);
    } else {
      return handleInBound(context);
    }
  }

  protected boolean handleInBound(SOAPMessageContext context) {
    return false;
  }

  protected boolean handleOutBound(SOAPMessageContext context) {
    try {
      String opUri = (String) context.get("javax.xml.ws.soap.http.soapaction.uri");
      String action = opUri.substring(opUri.lastIndexOf('/') + 1);


      SOAPMessage message = context.getMessage();
      SOAPPart part = message.getSOAPPart();
      SOAPEnvelope envelope = part.getEnvelope();

      String timestamp;

      SOAPHeader header = envelope.addHeader();
      SOAPElement ns = header.addNamespaceDeclaration("aws",
          "http://security.amazonaws.com/doc/2007-01-01/");

      SOAPElement accessKey = header.addChildElement(ns.createQName(
          "AWSAccessKeyId", "aws"));
      accessKey.addTextNode(this.accessKey);
      SOAPElement timeStamp = header.addChildElement(ns.createQName(
          "Timestamp", "aws"));
      timeStamp.addTextNode((timestamp = generateTimestamp()));
      SOAPElement signature = header.addChildElement(ns.createQName(
          "Signature", "aws"));
      signature.addTextNode(generateSignature(action, timestamp));

    } catch (SOAPException e) {
      e.printStackTrace();
    } catch (ClassCastException e) {
      e.printStackTrace();
    }
    return true;
  }

  private String generateSignature(String action, String ts) {

    byte[] data = (action + ts).getBytes();
    byte[] hmac = hmacSha_256.doFinal(data);
    Base64 encoder = new Base64();
    String signature = new String(encoder.encode(hmac));
    return signature;
  }

  private String generateTimestamp() {
    Date d = new Date();
    DateFormat f = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    f.setTimeZone(TimeZone.getTimeZone("UTC"));
    return f.format(d);
  }

  @Override
  public Set<QName> getHeaders() {
    return Collections.emptySet();
  }

}

mainクラス

package sample.apaapi;

import java.net.MalformedURLException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;

import javax.xml.ws.BindingProvider;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.HandlerResolver;
import javax.xml.ws.handler.PortInfo;
import javax.xml.ws.handler.soap.SOAPMessageContext;

import com.amazon.aws.ecommerce.AmazonAuthenticationHandler;
import com.amazon.aws.ecommers.AWSECommerceService;
import com.amazon.aws.ecommers.AWSECommerceServicePortType;
import com.amazon.aws.ecommers.ItemSearch;
import com.amazon.aws.ecommers.ItemSearchRequest;
import com.amazon.aws.ecommers.ItemSearchResponse;

public class AmazonSample {

  private static final String SECRET_KEY = "YOUR_SECRET_KEY";
  private static final String ACCESS_KEY = "YOUR_ACCESS_KEY";

  public static void main(String... args) throws MalformedURLException, NoSuchAlgorithmException, InvalidKeyException{
    
    AWSECommerceService service = new AWSECommerceService();
    service.setHandlerResolver(new HandlerResolver() {
      
      @SuppressWarnings("unchecked")
      @Override
      public List<Handler> getHandlerChain(PortInfo portInfo) {
        
        List<Handler> list = new ArrayList<Handler>();
        Handler<SOAPMessageContext> handler;
        try {
          handler = new AmazonAuthenticationHandler(ACCESS_KEY ,SECRET_KEY);
          list.add(handler);
        } catch (InvalidKeyException e) {
        } catch (NoSuchAlgorithmException e) {
        }
        return list;
      }
    });
    
    AWSECommerceServicePortType port = service.getAWSECommerceServicePort();
    ((BindingProvider)port).getRequestContext().put(
        BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
            "https://ecs.amazonaws.jp/onca/soap?Service=AWSECommerceService");
    
    ItemSearchRequest req = new ItemSearchRequest();
    req.setSearchIndex("Books");
    req.setKeywords("Java");
    
    ItemSearch search = new ItemSearch();
    search.getRequest().add(req);
    search.setAWSAccessKeyId(ACCESS_KEY);

    ItemSearchResponse res = port.itemSearch(search);
    System.out.println(res.getItems().get(0).getItem().size());
    
  }
}