Product Advertising API Signed Requests - Java SOAP using Spring Web Services
Product Advertising API - https://affiliate-program.amazon.com/gp/advertising/api/detail/main.html/178-0006257-2456255We are using Amazon Web Services to advertise products on one of my clients websites. We used Springframework as the dependency container and Spring Web Services as web service platform as we Spring-WS favors contract-first web services. With the recent changes in the API all the requests must be signed. And the sample code (http://developer.amazonwebservices.com/connect/entry.jspa?externalID=2479&categoryID=14) helped me as a reference to migrate our code base to support the signed request requirement.
In the below sections I will walk through the steps to make signed requests using Spring Web Services.
Project Setup:
We used maven2 as our build & reporting tool we followed the maven project directory structure and is as below:
awsprocess
└───src
├───main
│ ├───java
│ │ └───com
│ │ └───ascendant76
│ │ └───awsdemo
│ │ ├───client
│ │ └───service
│ ├───resources
│ └───webapp
└───test
├───java
└───resources
The project object model uses few plug-ins:
maven-compiler-plugin: To compile the code that is compatible with J2SE 5.0.
maven-surefire-plugin: To execute the unit test cases. I used TestNG(http://testng.org/doc/index.html) for the test cases and their by the testng dependency is imported.
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<classifier>jdk15</classifier>
<version>5.9</version>
</dependency>
maven-jaxb2-plugin: Creates JAXB2 binding from XSD. The plug-in scans the xjb files and generates the bindings in our case it AWSECommerceService.xjb in the resources folder.
AWSECommerceService.xjb:
<?xml version='1.0' encoding='utf-8' ?>
<jxb:bindings version="2.0" xmlns:jxb="http://java.sun.com/xml/ns/jaxb"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<jxb:bindings node="/xs:schema"
schemaLocation="AWSECommerceService.xsd">
<jxb:globalBindings fixedAttributeAsConstantProperty="false"
collectionType="java.util.ArrayList" typesafeEnumBase="xs:NCName"
choiceContentProperty="false" typesafeEnumMemberName="generateError"
enableFailFastCheck="true" generateIsSetMethod="true"
underscoreBinding="asCharInWord"
enableJavaNamingConventions="true">
</jxb:globalBindings>
<jxb:schemaBindings>
<jxb:package name="com.ascendant76.awsdemo.model" />
</jxb:schemaBindings>
</jxb:bindings>
</jxb:bindings>
The AWSECommerceService.xsd is extracted manually from the web service contract schema from the WSDL located at http://ecs.amazonaws.com/AWSECommerceService/2009-07-01/AWSECommerceService.wsdl
Account Setup:
Login to amazon at http://aws-portal.amazon.com/gp/aws/developer/account/index.html?ie=UTF8&action=activity-summary. Goto Access Credentials section by following this URL https://aws-portal.amazon.com/gp/aws/developer/account/index.html?ie=UTF8&action=access-key. This page allows you to create new X.509 certificates. Select the Create option to create your certificates and download them. You will have two files pk-XXXX.pem and cert-XXXX.pem
Note: You can download the private key file (pk-XXXX.pem) at the time of certificate creation only and will not be available later. If you lose it, you have to create a new certificate. So keep it safe.
Now we need to convert the certificate into the PCKS12 format. We need openssl (http://www.openssl.org) to create the certificate in pcks12 format.
C:\openssl pkcs12 -export -in cert-XXXX.pem -inkey pk-XXXX.pem -name ramesh_cert -out ramesh_cert.p12
The pkcs12 file will be generated. Execute the following command to verify the generated certificate using the user name.
C:\jdk1.6.0_13\bin\keytool -v -list -storetype pkcs12 -keystore ramesh_cert.p12
Enter keystore password: ramesh
The output appears something like below:
Keystore type: pkcs12
Keystore provider: SunJSSE
Your keystore contains 1 entry
Alias name: ramesh_cert
Creation date: Aug 28, 2009
Entry type: keyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=zzp69oziqnw6, OU=AWS-Developers, O=Amazon.com, C=US
Issuer: CN=AWS Limited-Assurance CA, OU=AWS, O=Amazon.com, C=US
Serial number: d61749a157
Valid from: Fri Aug 28 16:49:58 EDT 2009 until: Sat Aug 28 16:49:58 EDT 2010
Certificate fingerprints:
MD5: 60:BF:22:AF:51:F8:5A:72:36:8F:E8:69:1D:35:8C:C0
SHA1: D0:D9:5A:60:38:35:98:45:C3:41:CE:1A:8F:76:DC:39:80:3A:02:1B
**************************************************************************************
Once you verify the certificate generated is okay with the given user
name, copy the certificate generated above - ramesh_cert.p12 - into the
resources directory.
Codebase:
The dependency diagram of various collaborators is given below.
The AwsGateway is the sub-class of org.springframework.ws.client.core.support.WebServiceGatewaySupport is responsible for marshaling the java objects in to SOAP messages and sends to location mentioned in the service binding of the WSDL and in this case https://ecs.amazonaws.com/onca/soap?Service=AWSECommerceService. The WebServiceGatewaySupport delegates the responsibility to the Marshaller to marshal java request objects to SOAP messages and UnMarshaller to marshal the SOAP response back to Java object.
<!-- Working with the request and response xml's using Jaxb2 -->
<bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="schema" value="classpath:AWSECommerceService.xsd" />
<property name="contextPath" value="com.ascendant76.awsdemo.model" />
</bean>
Spring-WS supports two types of messages factories to deal with SOAP messages org.springframework.ws.soap.axiom.AxiomSoapMessageFactory and org.springframework.ws.soap.saaj.SaajSoapMessageFactory. And I choose to use SAAJ (https://saaj.dev.java.net/nonav/spec-1.3/api/).
<!-- SOAP with Attachments API for Java -->
<bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory" />
Now the web service gateway definition looks as below:
<bean id="awsGateway" class="com.ascendant76.awsdemo.service.AwsGateway">
<constructor-arg value="${aws.accessKey}" />
<property name="defaultUri" value="https://ecs.amazonaws.com/onca/soap?Service=AWSECommerceService" />
<property name="marshaller" ref="marshaller" />
<property name="unmarshaller" ref="marshaller" />
<property name="messageFactory" ref="messageFactory" />
<property name="interceptors">
<list>
<ref bean="signatureSecurityInterceptor" />
</list>
</property>
</bean>
As security is an orthogonal concept, spring-ws handles it using interceptors. We use WSS4J for the authentication to produce signed requests. Amazon API rely on Timestamp and Signature.
<bean id="signatureSecurityInterceptor" class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
<property name="securementActions" value="Timestamp Signature" />
<property name="timestampPrecisionInMilliseconds" value="true" />
<property name="securementSignatureKeyIdentifier" value="DirectReference" />
<property name="securementUsername" value="${aws.keystoreUser}" />
<property name="securementPassword" value="${aws.keystorePassword}" />
<property name="securementSignatureCrypto" ref="cryptoFactoryBean" />
<property name="securementCallbackHandler" ref="passwordCallback" />
</bean>
<bean id="cryptoFactoryBean" class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">
<property name="cryptoProvider" value="org.apache.ws.security.components.crypto.Merlin" />
<property name="keyStoreLocation" value="classpath:${aws.keystoreUser}.p12" />
<property name="keyStorePassword" value="${aws.keystorePassword}" />
<property name="keyStoreType" value="pkcs12" />
<property name="keyStoreProvider" value="SunJSSE" />
</bean>
<bean id="passwordCallback" class="com.ascendant76.awsdemo.service.PasswordCallback">
<constructor-arg index="0" value="${aws.keystorePassword}" />
</bean>
To keep all the properties in one place we can use property place holder. The keystore password is being served from properties file:
<context:property-placeholder location="classpath:app.properties" />
Change the properties files to reflect your credentials
# Amazon Associate Access Key
aws.accessKey=<<AWS ACCESSKEY comes here>>
aws.keystoreUser=<<KEY STORE comes here>>
aws.keystorePassword=<<KEY STORE password>>
Now at this stage we are ready to invoke the web service. Execute mvn clean test to execute the test cases.
Download the code
You can download the code from here: Download the Code
References:
http://www.openssl.org/docs/apps/pkcs12.html