Last active
October 21, 2019 08:24
-
-
Save devilelephant/4ae94f8e9f24b6056733 to your computer and use it in GitHub Desktop.
Java/Groovy example of using Amazon AWS AWS4Signer class to sign requests (in our case elasticsearch calls)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.clario.aws | |
import com.amazonaws.DefaultRequest | |
import com.amazonaws.SignableRequest | |
import com.amazonaws.auth.AWS4Signer | |
import com.amazonaws.auth.AWSCredentialsProvider | |
import com.amazonaws.http.HttpMethodName | |
import groovy.util.logging.Slf4j | |
import org.apache.http.client.utils.URLEncodedUtils | |
import org.springframework.http.HttpHeaders | |
import org.springframework.http.HttpRequest | |
/** | |
* Sign a url using Amazon {@link AWS4Signer}. | |
* | |
* Note: if you get a 403 signature error you can put a breakpoint at the end of Amazon's AWS4Signer.sign() | |
* (or turn on debug level on com.amazonaws.auth package ) to capture the generated canonicalRequest String and compare | |
* it to Amazon's expected string that is returned with the error. | |
* | |
* @author George Coller | |
*/ | |
@Slf4j | |
class V4RequestSigner { | |
private final String regionName | |
private final String serviceName | |
private final AWSCredentialsProvider awsCredentialsProvider | |
V4RequestSigner(AWSCredentialsProvider awsCredentialsProvider, String regionName, String serviceName) { | |
this.regionName = regionName | |
this.awsCredentialsProvider = awsCredentialsProvider | |
this.serviceName = serviceName | |
} | |
void signRequest(HttpRequest request, byte[] body) { | |
def headers = request.headers | |
if (body == null || body.length == 0) { | |
// Signer wanted the value when zero to be empty-string but Spring's rest template tries to parse it to a Long. Easier to just remove the header if it exits. | |
headers.keySet().findAll { it.equalsIgnoreCase('Content-Length') }.each { headers.remove(it) } | |
} | |
def signableRequest = makeSignableRequest(request, body) | |
headers.clear() | |
headers.putAll(signHeaders(signableRequest)) | |
} | |
HttpHeaders signHeaders(SignableRequest<String> signableRequest) { | |
AWS4Signer signer = new AWS4Signer(false) | |
signer.regionName = regionName | |
signer.serviceName = serviceName | |
signer.sign(signableRequest, awsCredentialsProvider.credentials) | |
def headers = new HttpHeaders() | |
signableRequest.headers.each { k, v -> | |
headers.add(k, v) | |
} | |
return headers | |
} | |
SignableRequest<String> makeSignableRequest(HttpRequest httpRequest, byte[] bytes) { | |
def request = new DefaultRequest<String>(serviceName) | |
// Separate URI base and resource path | |
def uri = httpRequest.URI | |
request.setEndpoint(new URI(uri.scheme, null, uri.host, uri.port, '', '', '')) | |
def rawPath = uri.rawPath.replaceAll('\\+', '%2B') // Signer wasn't happy about urls with spaces, wanted all '+' to be encoded as %2B. | |
request.setResourcePath(rawPath) | |
URLEncodedUtils.parse(uri, 'UTF-8').each { nameValue -> | |
request.addParameter(nameValue.name, nameValue.value) | |
} | |
request.setHttpMethod(HttpMethodName.valueOf(httpRequest.method.toString())) | |
request.setHeaders(httpRequest.headers.collectEntries { k, v -> [k, v.join(',')] } as Map<String, String>) | |
request.setContent(new ByteArrayInputStream(bytes)) | |
return request | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Snippit of how to inject the AWS4Signer class into a Spring RestTemplate so it signs every REST call: | |
@Bean | |
RestTemplate restTemplate() { | |
def requestFactory = new HttpComponentsClientHttpRequestFactory() | |
requestFactory.setReadTimeout(60_000) | |
requestFactory.setConnectTimeout(5_000) | |
def template = new RestTemplate(requestFactory) | |
template.interceptors.add(new ClientHttpRequestInterceptor() { | |
// In our case we're using us east 1 region and are going against the AWS elasticsearch endpoint | |
final signer = new V4RequestSigner(awsCredentials(), 'us-east-1', 'es') | |
@Override | |
ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { | |
signer.signRequest(request, body) | |
return execution.execute(request, body); | |
} | |
}) | |
return template | |
} |
Updated April 27 2018. Signer failed when URL had spaces (e.g "/files/My Dumb File.txt") because it expected them to be escaped with %2B instead of '+'
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here's how I used Aws4Signer, to sign the HTTP request, in a more OOP manner:
http://www.amihaiemil.com/2017/02/18/decorators-with-tunnels.html