I'm implementing interactive messages on Slack, which contains some action buttons. Using Slack App I'm able to handle Slack users clicking the buttons on my Java Springboot API.
To this moment, everything is fine. However, I struggle to compute matching request signature (digest) to verify, that it actually comes from Slack. I read all the documentation for that on Slack verification documentation page.
The page decribes, that the signature has to be computed as a HMAC SHA256 hash, using Signing Secret as a key and content as concatenation of slack version, timestamp and request body, for example:
v0:123456789:command=/weather&text=94070
On the page is stated:
...Evaluate only the raw HTTP request body when computing signatures.
... so I'm not encoding/deserializing the request before hash computing (I've attached my received request from Slack below)
To compute the hash I use the code found on StackOverflow:
private String computeMessageDigest(String content) {
final String ALGORITHM = "HmacSHA256";
final String UTF_8 = "UTF-8";
try {
Key signingKey = new SecretKeySpec(signingSecret.getBytes(UTF_8), ALGORITHM);
Mac mac = Mac.getInstance(ALGORITHM);
mac.init(signingKey);
return Hex.encodeHexString(mac.doFinal(content.getBytes(UTF_8)));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
I tried also this online hash generator to compare the results, and they were the same.
The request received from Slack looks like this:
{
"headers": {
"x-forwarded-for": ["::ffff:52.72.111.29"],
"x-forwarded-proto": ["https"],
"x-pagekite-port": ["443"],
"host": ["inqool.pagekite.me"],
"user-agent": ["Slackbot 1.0 (+https://api.slack.com/robots)"],
"accept-encoding": ["gzip,deflate"],
"accept": ["application/json,*/*"],
"x-slack-signature": ["v0=87fbffb089501ba823991cc20058df525767a8a2287b3809f9afff3e3b600dd8"],
"x-slack-request-timestamp": ["1531221943"],
"content-length": ["2731"],
"Content-Type": ["application/x-www-form-urlencoded;charset=UTF-8"]
},
"body": "payload=%7B%22type%22%3A%22interactive_message%22%2C%22actions%22%3A%5B%7B%22name%22%3A%22reject_btn%22%2C%22type%22%3A%22button%22%2C%22value%22%3A%22false%22%7D%5D%2C%22callback_id%22%3A%22artwork%3D40d7a87f-466c-4fc9-b454-09ce020d4465%22%2C%22team%22%3A%7B%22id%22%3A%22T03NP6SA7%22%2C%22domain%22%3A%22artstaq%22%7D%2C%22channel%22%3A%7B%22id%22%3A%22G8F2WR4FJ%22%2C%22name%22%3A%22privategroup%22%7D%2C%22user%22%3A%7B%22id%22%3A%22U66T9QX60%22%2C%22name%22%3A%22majo%22%7D%2C%22action_ts%22%3A%221531221943.512498%22%2C%22message_ts%22%3A%221531221198.000225%22%2C%22attachment_id%22%3A%221%22%2C%22token%22%3A%22ZABrZDXgJCOOLNau5mXnfNQR%22%2C%22is_app_unfurl%22%3Afalse%2C%22original_message%22%3A%7B%22text%22%3A%22User+just+put+item+on+*EXCHANGE*.%22%2C%22bot_id%22%3A%22BBM1W4QEL%22%2C%22attachments%22%3A%5B%7B%22author_name%22%3A%22Slack+Test%3B+slack%40test.com%22%2C%22callback_id%22%3A%22artwork%3D40d7a87f-466c-4fc9-b454-09ce020d4465%22%2C%22fallback%22%3A%22Slack+Test%3B+%3Cmailto%3Aslack%40test.com%7Cslack%40test.com%3E+just+put+item+Panenka+%5C%2F+Doll+by+artist+Jaroslav+Vale%5Cu010dka+into+ON+REQUEST+mode%22%2C%22text%22%3A%22%3Chttp%3A%5C%2F%5C%2Flocalhost%3A8080%5C%2Fartist%5C%2F609cd328-d533-4ab0-b982-ec2f104476f2%7CJaroslav+Vale%5Cu010dka%3E%22%2C%22title%22%3A%22Panenka+%5C%2F+Doll%22%2C%22footer%22%3A%22ARTSTAQ+Slack+Reporter%22%2C%22id%22%3A1%2C%22title_link%22%3A%22http%3A%5C%2F%5C%2Flocalhost%3A8080%5C%2Fartwork%5C%2F40d7a87f-466c-4fc9-b454-09ce020d4465%22%2C%22color%22%3A%22f0d0ad%22%2C%22fields%22%3A%5B%7B%22title%22%3A%22Trading+type%22%2C%22value%22%3A%22ON+REQUEST%22%2C%22short%22%3Atrue%7D%5D%2C%22actions%22%3A%5B%7B%22id%22%3A%221%22%2C%22name%22%3A%22approve_btn%22%2C%22text%22%3A%22APPROVE%22%2C%22type%22%3A%22button%22%2C%22value%22%3A%22true%22%2C%22style%22%3A%22primary%22%2C%22confirm%22%3A%7B%22text%22%3A%22Do+you+really+want+to+approve+this+artwork%3F%22%2C%22title%22%3A%22Approve+artwork%22%2C%22ok_text%22%3A%22Yes%22%2C%22dismiss_text%22%3A%22Cancel%22%7D%7D%2C%7B%22id%22%3A%222%22%2C%22name%22%3A%22reject_btn%22%2C%22text%22%3A%22REJECT%22%2C%22type%22%3A%22button%22%2C%22value%22%3A%22false%22%2C%22style%22%3A%22danger%22%2C%22confirm%22%3A%7B%22text%22%3A%22Do+you+really+want+to+reject+this+artwork%3F%22%2C%22title%22%3A%22Reject+artwork%22%2C%22ok_text%22%3A%22Yes%22%2C%22dismiss_text%22%3A%22Cancel%22%7D%7D%5D%7D%5D%2C%22type%22%3A%22message%22%2C%22subtype%22%3A%22bot_message%22%2C%22ts%22%3A%221531221198.000225%22%7D%2C%22response_url%22%3A%22https%3A%5C%2F%5C%2Fhooks.slack.com%5C%2Factions%5C%2FT03NP6SA7%5C%2F395760858899%5C%2FGlP9jsNQak7FqEciEHhscx4L%22%2C%22trigger_id%22%3A%22395632563524.3771230347.851ab60578de033398338a9faeb41a15%22%7D"
}
When I computed the HMAC SHA256 hash, I got 561034bb6860c07a6b4eaf245b6da3ea869c7806c7f7be20b1a830b6d25c54c8 but I should get 87fbffb089501ba823991cc20058df525767a8a2287b3809f9afff3e3b600dd8, as in the request header.
I also tried to compute the hash from the URL decoded body, but still not be able to get the matching signature.
Am I doing something wrong? Thanks for the answers/hints.
EDIT: here's the whole source code of my REST controller and request verifier:
package com.artstaq.resource;
import com.artstaq.integration.slack.SlackRequestVerifier;
import org.springframework.http.HttpEntity;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.inject.Inject;
#RestController
#RequestMapping("/content_admin")
public class ContentAdminResource {
private SlackRequestVerifier slackVerifier;
#RequestMapping(value = "/slack/artwork/resolve", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void resolve(HttpEntity<String> request) {
slackVerifier.verifySlackRequest(request);
}
#Inject
public void setSlackVerifier(SlackRequestVerifier slackVerifier) {
this.slackVerifier = slackVerifier;
}
}
package com.artstaq.integration.slack;
import com.artstaq.exception.SignatureVerificationException;
import com.artstaq.exception.TimestampTooOldException;
import org.apache.commons.codec.binary.Hex;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
/**
* Class providing request verification received from Slack
*/
#Component
public class SlackRequestVerifier {
#Value("${integration.slack.version:v0}")
private String version;
#Value("${integration.slack.signingSecret}")
private String signingSecret;
/**
* Verifies the integrity of received Slack request.
*/
public void verifySlackRequest(HttpEntity<String> request) {
String timestamp = request.getHeaders().getFirst(SlackHeaders.TIMESTAMP);
Instant timeInstant = Instant.ofEpochSecond(Long.valueOf(timestamp));
if (timeInstant.plus(5, ChronoUnit.MINUTES).compareTo(Instant.now()) < 0) {
throw new TimestampTooOldException(timeInstant);
}
String expectedDigest = request.getHeaders().getFirst(SlackHeaders.SIGNATURE);
String basestring = String.join(":", version, timestamp, request.getBody());
String computedDigest = version + "=" + computeMessageDigest(basestring);
if (!computedDigest.equals(expectedDigest)) {
throw new SignatureVerificationException(expectedDigest, computedDigest);
}
}
/**
* Compute HMAC SHA256 digest for given content using defined slack signing secret
*/
private String computeMessageDigest(String content) {
final String ALGORITHM = "HmacSHA256";
final String UTF_8 = "UTF-8";
try {
Key signingKey = new SecretKeySpec(signingSecret.getBytes(UTF_8), ALGORITHM);
Mac mac = Mac.getInstance(ALGORITHM);
mac.init(signingKey);
return Hex.encodeHexString(mac.doFinal(content.getBytes(UTF_8)));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static class SlackHeaders {
private static final String TIMESTAMP = "X-Slack-Request-Timestamp";
private static final String SIGNATURE = "X-Slack-Signature";
}
}
I stumbled upon the very same problem on a Node.js implementation, and found this Medium article which states the following:
Note: We cannot use the built-in querystring Node package because it only supports RFC3986 space encoding and Slack requires us to implement RFC1738 space encoding.
What's the difference between both encodings? The way spaces are parsed:
RFC3986 will convert " " to "%20"
RFC1738 will convert " " to "+"
For Node.js, it suggests to install qs and to use it like this:
qs.stringify(req.body, { format : 'RFC1738' });
The following worked for us:
public enum SigningVerification {
VERIFIED,
DENIED
}
public SigningVerification verify(ImmutableSigningSecretRequest request) {
String basestring = String.join(":", "v0", request.timestamp(), request.body());
SecretKeySpec secret_key = new SecretKeySpec(signingSecret.getBytes(), "HmacSHA256");
Mac sha256_HMAC = Try.of(() -> Mac.getInstance("HmacSHA256")).getOrElseThrow((SupplierRuntimeException) RuntimeException::new);
Try.run(() -> sha256_HMAC.init(secret_key));
String hash = "v0=" + Hex.encodeHexString(sha256_HMAC.doFinal(basestring.getBytes()));
return hash.equals(request.verificationSignature()) ? VERIFIED : DENIED;
}
Controller:
#PostMapping("/command")
public RichMessage postCommand(#RequestHeader(value = "X-Slack-Request-Timestamp") String timestamp,
#RequestHeader(value = "X-Slack-Signature") String signature,
#RequestParam(value = "text", required = false) String message,
#RequestBody String body) {
SigningSecretVerification.SigningVerification verification = verifier.verify(ImmutableSigningSecretRequest
.builder()
.timestamp(timestamp)
.verificationSignature(signature)
.body(body)
.build()
);
return new RichMessage(message);
}
We basically just followed the steps in the Slack doc and it works fine.
I had the same problem, using Spring's #RequestBody.
After going to the trouble of setting up a mitmproxy between Slack and my Spring app in order to compare request bodies, it turned out that Spring was decoding e.g. asterisk characters instead of leaving them as %2A.
The fix for me was to switch to asking for a [HttpServletRequest](https://github.com/boclips/terry/commit/c51382a5a6a9e96d5b19e22b038654bfb19b65b0#diff-79f3c274c9fa96261f8c9e09306a088bR37) (doesn't need a Spring annotation) and reading the raw body from it using `request.reader.use { it.readText() }` (using Kotlin's `use` to close the reader object after reading).
EDIT: the above technique doesn't work, and getting the raw request body from Spring is a mission in itself! Ongoing.
we just stumbled along with the exact same problem.
Your tip about the asterisk decoding helped us a lot!
I don't know if you already solved your issue with caching the request, but maybe you want to take a look on our open source SlackBot SDK for Spring boot, where we were able to solve that issue: https://github.com/kreait/slack-spring-boot-starter/blob/master/starter/slack-spring-boot/src/main/kotlin/io/olaph/slack/broker/security/VerificationMethodArgumentResolver.kt
This VerificationMethodArgumentResolver basically receives the request, wraps it in a ContentCachingRequestWrapper, and invokes the internalResolveArgument of the normal ArgumentResolvers and verifies the request using the cached request. The tricky part here is, that the cache is empty until you've requested its parameterMap. So it is important to validate the signing after you've consumed the request.
I also got bitten by this. Using #RequestBody does not give you back the original body as #andrew-bruce also noted. Specifically for me it failed on the original %2A ending up as an unencoded * when getting the body like that. Obviously that fails the verification.
I ended up with this solution:
a filter combined with a HttpServletRequest wrapper that allows to read the body multiple times:
import org.springframework.web.filter.OncePerRequestFilter
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
/**
* To verify if requests are coming from Slack we need to implement this:
* https://api.slack.com/authentication/verifying-requests-from-slack. Luckily the Bolt framework already implements
* this for us, however we need to provide it with a body that is unaltered. Somewhere in Springs filterchain Spring
* will already have consumed the [HttpServletRequest#inputstream], so we cannot get it from the [HttpServletRequest]
* directly. Spring obviously provides a [org.springframework.web.bind.annotation.RequestBody] annotation, but this is
* slightly different from the original body. This servlet filter will be put as the very first in
* the chain (see [SlackConfig#multiReadRequestFilter] should make sure
* that we can re-read it and construct the raw body, so that the verification doesn't fail.
*/
class MultiReadHttpServletFilter : OncePerRequestFilter() {
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
val multiReadHttpServletRequest = MultiReadHttpServletRequest(request)
filterChain.doFilter(multiReadHttpServletRequest, response)
}
}
import org.apache.commons.io.IOUtils
import java.io.ByteArrayInputStream
import java.io.IOException
import javax.servlet.ReadListener
import javax.servlet.ServletInputStream
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletRequestWrapper
class MultiReadHttpServletRequest(request: HttpServletRequest) : HttpServletRequestWrapper(request) {
private var body: ByteArray = IOUtils.toByteArray(request.inputStream)
#Throws(IOException::class)
override fun getInputStream(): ServletInputStream {
return object : ServletInputStream() {
val bais = ByteArrayInputStream(body)
override fun isReady(): Boolean = true
override fun isFinished(): Boolean = bais.available() == 0
override fun read(): Int = bais.read()
override fun setReadListener(readListener: ReadListener) {
throw NotImplementedError("Not implemented!")
}
}
}
}
Configure it as first in the chain. Because this filter actually consumes things in-memory I've specifically targeted a specific path, so it's only for the incoming Slack events in this case.
#Bean
fun multiReadRequestFilter(): FilterRegistrationBean<MultiReadHttpServletFilter> {
// this needs to match the path(s) of the controller SlackAppController
val urlPatterns = slacks.allByKey().keys.map { "/v2/$it/slack/events" }.toTypedArray()
val registrationBean = FilterRegistrationBean<MultiReadHttpServletFilter>()
registrationBean.filter = MultiReadHttpServletFilter()
registrationBean.addUrlPatterns(*urlPatterns)
registrationBean.order = Ordered.HIGHEST_PRECEDENCE
return registrationBean
}
Now I can use it to retrieve the 'real' raw body:
#RestController
#RequestMapping("/v2/{slackId}/slack/events")
class SlackAppController() {
// ...
#PostMapping
fun handle(
#PathVariable("slackId") slackId: String,
httpServletRequest: HttpServletRequest,
#RequestParam queryStringParams: MultiValueMap<String, String>,
#RequestHeader headers: MultiValueMap<String, String>): ResponseEntity<*> {
val body = IOUtils.toString(httpServletRequest.inputStream, StandardCharsets.UTF_8)
// ...
}
}
I was having the same problem, In my controller I was receiving the body of the request as a Map, I received all the values but when I was calculating the hash I saw that the slack-signature and my hash wasn't the same.
I tried to receive the request body as a String just like the #Stefan solution and that works for me, so, instead using HttpEntity<String> in your controller, you must receive the body as plain String with #RequestBody String body in your method argument, the reason is that slack sends encoded values in the request, %2F or %3A, with HttpEntity or Map, spring interprets that values as / and : and this is the reason why your hash is not equals as slack signature.
Hope this help you.
Here's what I found out on the topic:
Spring MVC will read the body of the request and will return a different body, where the order of parameters is changed. The reading part usually happens inside one of the first filters on the chain HiddenHttpMethodFilter and this is the main reason why my signature verification failed.
The request is "reconstructed" incorrectly here ServletServerHttpRequest I'm not sure if this should be filed as a bug or not, but it's definitely messed up.
If you use an injected HttpEntity<String> or #RequestBody String body you will receive the wrong body, not the raw content but the "reconstructed" one
And now the solution:
Create a filter for verifying Slack signatures and register it with the highest priority so it's on to top of the filter chain:
#Bean
public FilterRegistrationBean<SlackVerificationFilter> slackVerificationFilterRegistrationBean() {
String path = "/slack";
FilterRegistrationBean<SlackVerificationFilter> frb = new FilterRegistrationBean<>(new SlackVerificationFilter());
frb.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST));
frb.setName("csrfFilter");
frb.setAsyncSupported(true);
frb.addUrlPatterns(path);
frb.setMatchAfter(false);
frb.setEnabled(true);
frb.setOrder(Ordered.HIGHEST_PRECEDENCE);
return frb;
}
Inside the filter, wrap the request with some sort of HttpServletRequestWrapper like this:
public class SlackVerificationFilter extends GenericFilterBean {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final BufferedRequestWrapper request = new BufferedRequestWrapper((HttpServletRequest) req);
final HttpServletResponse response = (HttpServletResponse) res;
String rawBody = IOUtils.toString(request.getInputStream(), "UTF-8");
// do signature verification here
chain.doFilter(request, response);
}
}
I won't go into details about the request wrapper. There are lots of examples of it on this site and elsewhere.
Your HttpServletRequestWrapper must implement the following methods:
public ServletInputStream getInputStream();
public BufferedReader getReader() throws IOException;
public Map<String, String[]> getParameterMap();
public String getParameter(String name);
After this you should no longer have problems verifying Slack signatures.
In my case, I didn't have issues with any of the encoded characters mentioned above (%20, %2A, etc). I only had problems verifying signatures of slash command requests. Message action request were verified correctly because they only had 1 request parameter in the body (payload).
I had this same issue using #RequestBody and not being able to validate the request. Here's how I fixed it in Kotlin. It should translate to Java fairly easily.
import org.springframework.web.bind.annotation.*
import org.apache.commons.io.IOUtils
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import javax.servlet.http.HttpServletRequest
import org.apache.commons.codec.binary.Hex
fun validateSlackWebhook(
#RequestHeader("X-Slack-Request-Timestamp") slackRequestTimestamp: String,
#RequestHeader("X-Slack-Signature") slackSignature: String,
request: HttpServletRequest,
) {
val body = IOUtils.toString(request.reader)
val basestring = "v0:$slackRequestTimestamp:$body"
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(slackSigningSecret.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
val finalHex = "v0=${Hex.encodeHexString(sha256Hmac.doFinal(basestring.toByteArray()))}"
val validated = finalHex.compareTo(slackSignature) == 0
if (!validated) {
// Code to run if request was not validated
return
}
// Code to run if request was validated
}
I finally solved it by looking at the official SDK source code of Slack!
https://github.com/slackapi/java-slack-sdk/blob/f283e45601157a0d2483ea3d3e8074e80b81a0e6/slack-app-backend/src/main/java/com/slack/api/app_backend/SlackSignature.java#L88-L122
public String generate(String slackRequestTimestamp, String requestBody) {
if (slackRequestTimestamp == null) {
return null;
}
// 1) Retrieve the X-Slack-Request-Timestamp header on the HTTP request, and the body of the request.
// "slackRequestTimestamp" here
// 2) Concatenate the version number, the timestamp, and the body of the request to form a basestring.
// Use a colon as the delimiter between the three elements.
// For example, v0:123456789:command=/weather&text=94070. The version number right now is always v0.
String baseString = "v0:" + slackRequestTimestamp + ":" + requestBody;
// 3) With the help of HMAC SHA256 implemented in your favorite programming, hash the above basestring,
// using the Slack Signing Secret as the key.
SecretKeySpec sk = new SecretKeySpec(slackSigningSecret.getBytes(), ALGORITHM);
try {
Mac mac = Mac.getInstance(ALGORITHM);
mac.init(sk);
byte[] macBytes = mac.doFinal(baseString.getBytes());
StringBuilder hashValue = new StringBuilder(2 * macBytes.length);
for (byte macByte : macBytes) {
hashValue.append(String.format("%02x", macByte & 0xff));
}
return "v0=" + hashValue.toString();
// 4) Compare this computed signature to the X-Slack-Signature header on the request.
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
log.error("Failed to hash the base string value with HMAC-SHA256 because {}", e.getMessage(), e);
return null;
}
}
Related
I need to call Oauth2 ResT API service to fetch the access token and expire_in values from the JSON file by it.
Below is a sample CURL which i need to call using JAVA i am beginner in JAVA so not able to figure out how to do it however i can do it using shell script.
curl -u 'ClientId:Clientaccesskey' https://oauth2.url/oauth/token -X POST -d 'response_type=token&client_id=ClientId&username=user&password=userpassword&scope=process&grant_type=password'
Sample JSON retured by above curl command --
{"access_token":"accessTokentobefetched","token_type":"bearer","refresh_token":"refreshToken","expires_in":7199,"scope":"process","jti":"somehexadecimalvaliu"}
In shell script we can fetch the value of access token and other fields using AWK command and other commands.
So i need to call this CURL command in JAVA and fetch the value of access token and other keys from the JSON file.
Any help which can help me start with this is welcome as i am new to JAVA and learning.
There are quite a few libraries that you can use to help you make a regular HTTP POST request from Java, but since you seem to require to send plain text/plain body content - I suggest that you use okhttp3. This is a fairly lightweight and easy to work with HTTP client.
You will need to add the following dependency to your pom.xml, grabbed from https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp/4.7.2:
<!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.7.2</version>
</dependency>
If you are using gradle, just visit the before mentioned URL, and get the gradle equivalent dependency declaration.
And here's a complete class that illustrates how the okhttp3 client can be used to perform the POST request, and extract the return value. This example expects that you are using the spring-boot-starter-web dependency (this will include the jackson and tomcat libraries that are used in the example).
package com.example.demo;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
#Component
public class TokenRequester {
public String getAccessToken() throws IOException {
// Create a new HTTP client
OkHttpClient client = new OkHttpClient().newBuilder().build();
// Create the request body
MediaType mediaType = MediaType.parse("text/plain");
RequestBody body = RequestBody.create(mediaType, "response_type=token&client_id=ClientId&username=user&password=userpassword&scope=process&grant_type=password");
// Build the request object, with method, headers
Request request = new Request.Builder()
.url("https://oauth2.url/oauth/token")
.method("POST", body)
.addHeader("Authorization", createAuthHeaderString("ClientId", "Clientaccesskey"))
.addHeader("Content-Type", "text/plain")
.build();
// Perform the request, this potentially throws an IOException
Response response = client.newCall(request).execute();
// Read the body of the response into a hashmap
Map<String,Object> responseMap = new ObjectMapper().
readValue(response.body().byteStream(), HashMap.class);
// Read the value of the "access_token" key from the hashmap
String accessToken = (String)responseMap.get("access_token");
// Return the access_token value
return accessToken;
}
// Just a helper metod to create the basic auth header
private String createAuthHeaderString(String username, String password) {
String auth = username + ":" + password;
byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.US_ASCII));
String authHeader = "Basic " + new String(encodedAuth);
return authHeader;
}
}
You may need to tweak a few things here. I could ask you to supply me the verbose output from the curl command, in order to be sure about the encoding - but give this one a try and see what you get?
Here's a solution that involves only Spring, using a RestTemplate for the POST request.
I found that when you use curl -X POST -d 'key=data', curl will add the header content-type: application/x-www-form-urlencoded, so the solution here will do the same.
This solution sets up the RestTemplate with the headers and body you have specified, and captures the response in an object equivalent to the one you have described.
The following solution consists of two files that you can try to introduce into your solution:
RestTemplateTokenRequester.java
package com.example.demo;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
#Component
public class RestTemplateTokenRequester {
public TokenResponse requestAccessToken() {
// Create a RestTemplate to describe the request
RestTemplate restTemplate = new RestTemplate();
// Specify the http headers that we want to attach to the request
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("Authorization", createAuthHeaderString("ClientId", "Clientaccesskey"));
// Create a map of the key/value pairs that we want to supply in the body of the request
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("response_type","token");
map.add("client_id","ClientId");
map.add("username","user");
map.add("password","userpassword");
map.add("scope","process");
map.add("grant_type","password");
// Create an HttpEntity object, wrapping the body and headers of the request
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);
// Execute the request, as a POSt, and expecting a TokenResponse object in return
ResponseEntity<TokenResponse> response =
restTemplate.exchange("https://oauth2.url/oauth/token",
HttpMethod.POST,
entity,
TokenResponse.class);
return response.getBody();
}
// Just a helper metod to create the basic auth header
private String createAuthHeaderString(String username, String password) {
String auth = username + ":" + password;
byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.US_ASCII));
String authHeader = "Basic " + new String(encodedAuth);
return authHeader;
}
}
TokenResponse.java
This is simply a POJO that is used by the jackson mapper, to capture the response in an object that you can easily read your result from.
package com.example.demo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
#JsonIgnoreProperties(ignoreUnknown = true)
public class TokenResponse {
#JsonProperty("access_token")
private String accessToken;
#JsonProperty("token_type")
private String tokenType;
#JsonProperty("refresh_token")
private String refreshToken;
#JsonProperty("expires_in")
private Integer expiresIn;
#JsonProperty("scope")
private String scope;
#JsonProperty("jti")
private String jti;
}
I hope this solution will help you - I would prefer it over the other solution I have suggested with okhttp3.
curl is a HTTP client.
better solution is using HTTP client APIs for java to call endpoints.
RestTemplate is common HTTP client comes with spring and it is your best choice.
I am new to API design, I am working on one project where I need to call currency exchange API from National Bank of Poland http://api.nbp.pl but I do not see any indication where I can find API ID. This development is on Spring Boot if I am trying to run the application without API ID it is throwing 404 error.
Here is the piece of code that I have written.
#RequestMapping(method = RequestMethod.GET, value = "/exchangerates/rates/{table}/{code}")
public #ResponseBody Object getAllCurriencyExchangeRates(#PathVariable String table, #PathVariable String code) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
ResponseEntity<Object> response =
restTemplate.getForEntity("http://api.nbp.pl/api/" +table+ "," +code+ Object.class, null, headers);
return response;
}
Actual query http://api.nbp.pl/api/exchangerates/rates/a/chf/
So, my question is can we call an external API without API ID?
First things first, you are trying to reach wrong API. That is why you are getting 404 not found. 404 means there is no url like you are calling.
Check your restTemplate carefully,
restTemplate.getForEntity("http://api.nbp.pl/api/" + table+ "," +code+ Object.class, null, headers);
You are doing wrong when concatenate strings.
It should look something like this;
restTemplate.getForEntity("http://api.nbp.pl/api/exchangerates/rates/"+table+"/"+code, Object.class, null, headers);
And a hint for API developers, firstly you should play with api using Postman and then write code with api.
Try this - I have tested it - it works. Please keep in mind this is just a test implementation. Things inside main method have to be copied into your getAllCurriencyExchangeRates method.
And for sure replace "a" and "chf" through variables. I assume table and code are the variables you want to use. I used String because I don't know which type of object you want to return. You can use your own pojo for sure instead of String.
package scripts;
import java.net.URI;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
/**
* author: flohall
* date: 08.12.19
*/
public class Test {
public static void main(final String[] args){
final String url = "http://api.nbp.pl/api/exchangerates/rates";
final URI uri = UriComponentsBuilder.fromHttpUrl(url).path("/").path("a").path("/").path("chf").build().toUri();
System.out.println(uri);
final RestOperations restTemplate = new RestTemplate();
final ResponseEntity<String> result = restTemplate.getForEntity(uri, String.class);
System.out.println(result.getBody());
}
}
Try with this
ResponseEntity<Object> response =
restTemplate.getForEntity("http://api.nbp.pl/api/exchangerates/rates/" + table + "/" + code, Object.class, headers);
I have a server generate AWS S3 pre-signed PUT URLs and then I'm trying to uploading a byte[] into that URL using RestTemplate with this code:
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.ALL));
HttpEntity<byte[]> entity = new HttpEntity<>("Testing testing testing".getBytes(), headers);
System.out.println(restTemplate.exchange(putUrl, HttpMethod.PUT, entity, String.class));
When I run that code, I get this error:
Exception in thread "JavaFX Application Thread" org.springframework.web.client.HttpClientErrorException: 400 Bad Request
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:63)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:700)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:653)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:531)
at tech.dashman.dashman.controllers.RendererAppController.lambda$null$2(RendererAppController.java:95)
Unfortunately, there's nothing in the AWS S3 logs, so, I'm not sure what's going on. If I take that exact same URL and put it in the REST Client of IntelliJ IDEA, it just works (it creates an empty file in S3).
Any ideas what's wrong with my Java code?
Here's a full example that does the signing and tries to uploading a small payload to S3:
import com.amazonaws.HttpMethod;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import org.joda.time.DateTime;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.web.client.RestTemplate;
import java.util.Date;
public class S3PutIssue {
static public void main(String[] args) {
String awsAccessKeyId = "";
String awsSecretKey = "";
String awsRegion = "";
String path = "";
String awsBucketName = "";
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(awsAccessKeyId, awsSecretKey);
AmazonS3 s3Client = AmazonS3ClientBuilder.standard().withRegion(awsRegion).
withCredentials(new AWSStaticCredentialsProvider(awsCredentials)).build();
Date expiration = new DateTime().plusDays(1).toDate();
GeneratePresignedUrlRequest urlRequest = new GeneratePresignedUrlRequest(awsBucketName, path);
urlRequest.setMethod(HttpMethod.PUT);
urlRequest.setExpiration(expiration);
String putUrl = s3Client.generatePresignedUrl(urlRequest).toString();
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
HttpEntity<byte[]> entity = new HttpEntity<>("Testing testing testing".getBytes(), headers);
restTemplate.exchange(putUrl, org.springframework.http.HttpMethod.PUT, entity, Void.class);
}
}
The source of issue is a double encoding of url characters. There are / in extended secret key which are encoded as %2 by s3Client.generatePresignedUrl. When already encoded string is passed to restTemplate.exchange it's internally converted to URI and encoded for the second time as %252 by UriTemplateHandler in RestTemplate source code.
#Override
#Nullable
public <T> T execute(String url, HttpMethod method, #Nullable RequestCallback requestCallback,
#Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
URI expanded = getUriTemplateHandler().expand(url, uriVariables);
return doExecute(expanded, method, requestCallback, responseExtractor);
}
So the easiest solution is to convert URL to URI using URL.toURI(). If you don't have URI and have String when RestTemplate is invoked then two options are possible.
Pass URI instead for string to exchange method.
restTemplate.exchange(new URI(putUrl.toString()), HttpMethod.PUT, entity, Void.class);
Create default UriTemplateHandler with NONE encoding mode and pass it to RestTemplate.
DefaultUriBuilderFactory defaultUriBuilderFactory = new DefaultUriBuilderFactory();
defaultUriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
restTemplate.setUriTemplateHandler(defaultUriBuilderFactory);
restTemplate.exchange(putUrl.toString(), org.springframework.http.HttpMethod.PUT, entity, Void.class);
Do not convert your URL to String. Instead convert it to URI. I think there are some encoding issues when you converted to String. For example the URL in String format had %252F, where it should have just been %2F. Looks like some sort of double encoding issue.
Leave as URL...
URL putUrl = amazonS3Client.generatePresignedUrl(urlRequest);
Convert to URI...
ResponseEntity<String> re = restTemplate.exchange(putUrl.toURI(), org.springframework.http.HttpMethod.PUT, entity, String.class);
EDIT: More info to clarify what is going on.
The problem that occurred here is that when you call URL.toString() in this instance you are given back an encoded String representation of the URL. But the RestTemplate is expecting a String url that is not yet encoded. RestTemplate will do the encoding for you.
For example look at the code below...
public static void main(String[] args) {
RestTemplate rt = new RestTemplate();
rt.exchange("http://foo.com/?var=<val>", HttpMethod.GET, HttpEntity.EMPTY, String.class);
}
When you run this you get the following debug message from Spring, notice how the url in the debug msg is encoded.
[main] DEBUG org.springframework.web.client.RestTemplate - Created GET request for "http://foo.com/?var=%3Cval%3E"
So you can see the RestTemplate will encode for you any String url's passed to it. But the URL provided by AmazonS3Client is already encoded. See the code below.
URL putUrl = amazonS3Client.generatePresignedUrl(urlRequest);
System.out.println("putUrl.toString = " + putUrl.toString());
This prints out a String that is already encoded.
https://private.s3.amazonaws.com/testing/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20171114T191829Z&X-Amz-SignedHeaders=host&X-Amz-Expires=0&X-Amz-Credential=AKIAIJ7ZSL22IJTM6NTQ%2F20171114%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=eca611ea33d9ad5710207568dcf181e4318ce39271fd0f1ce05bd99ebbf4097
So when I stick that into the exchange method of the RestTemplate I get the following debug message.
[main] DEBUG org.springframework.web.client.RestTemplate - PUT request for "https://turretmaster.s3.amazonaws.com/testing/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20171114T191829Z&X-Amz-SignedHeaders=host&X-Amz-Expires=0&X-Amz-Credential=AKIAIJ7ZSL22IJTM6NTQ%252F20171114%252Fus-east-1%252Fs3%252Faws4_request&X-Amz-Signature=eca611ea33d9ad5710207568dcf181e4318ce39271fd0f1ce05bd99ebbf40975"
Notice how every %2F from the url String turned into %252F. %2F is the encoded representation of /. But %25 is %. So it encoded a url that was already encoded. The solution was to pass a URI object to RestTemplate.exchange, instead of an encoded String url.
I am using URL class to read an InputStream from it. Is there any way I can use RestTemplate for this?
InputStream input = new URL(url).openStream();
JsonReader reader = new JsonReader(new InputStreamReader(input, StandardCharsets.UTF_8.displayName()));
How can I get InputStream with RestTemplate instead of using URL?
The previous answers are not wrong, but they don't go into the depth that I like to see. There are cases when dealing with low level InputStream is not only desirable, but necessary, the most common example being streaming a large file from source (some web server) to destination (a database). If you try to use a ByteArrayInputStream, you will be, not so surprisingly, greeted with OutOfMemoryError. Yes, you can roll your own HTTP client code, but you'll have to deal with erroneous response codes, response converters etc. If you are already using Spring, looking to RestTemplate is a natural choice.
As of this writing, spring-web:5.0.2.RELEASE has a ResourceHttpMessageConverter that has a boolean supportsReadStreaming, which if set, and the response type is InputStreamResource, returns InputStreamResource; otherwise it returns a ByteArrayResource. So clearly, you're not the only one that asked for streaming support.
However, there is a problem: RestTemplate closes the response soon after the HttpMessageConverter runs. Thus, even if you asked for InputStreamResource, and got it, it's no good, because the response stream has been closed. I think this is a design flaw that they overlooked; it should've been dependent on the response type. So unfortunately, for reading, you must consume the response fully; you can't pass it around if using RestTemplate.
Writing is no problem though. If you want to stream an InputStream, ResourceHttpMessageConverter will do it for you. Under the hood, it uses org.springframework.util.StreamUtils to write 4096 bytes at a time from the InputStream to the OutputStream.
Some of the HttpMessageConverter support all media types, so depending on your requirement, you may have to remove the default ones from RestTemplate, and set the ones you need, being mindful of their relative ordering.
Last but not the least, implementations of ClientHttpRequestFactory has a boolean bufferRequestBody that you can, and should, set to false if you are uploading a large stream. Otherwise, you know, OutOfMemoryError. As of this writing, SimpleClientHttpRequestFactory (JDK client) and HttpComponentsClientHttpRequestFactory (Apache HTTP client) support this feature, but not OkHttp3ClientHttpRequestFactory. Again, design oversight.
Edit:
Filed ticket SPR-16885.
Spring has a org.springframework.http.converter.ResourceHttpMessageConverter. It converts Spring's org.springframework.core.io.Resource class.
That Resource class encapsulates a InputStream, which you can obtain via someResource.getInputStream().
Putting this all together, you can actually get an InputStream via RestTemplate out-of-the-box by specifying Resource.class as your RestTemplate invocation's response type.
Here is an example using one of RestTemplate's exchange(..) methods:
import org.springframework.web.client.RestTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.core.io.Resource;
ResponseEntity<Resource> responseEntity = restTemplate.exchange( someUrlString, HttpMethod.GET, someHttpEntity, Resource.class );
InputStream responseInputStream;
try {
responseInputStream = responseEntity.getBody().getInputStream();
}
catch (IOException e) {
throw new RuntimeException(e);
}
// use responseInputStream
You should not get the InputStream directly. RestTemplate is meant to encapsulate processing the response (and request) content. Its strength is handling all the IO and handing you a ready-to-go Java object.
One of RestTemplate's original authors, Brian Clozel, has stated:
RestTemplate is not meant to stream the response body; its contract
doesn't allow it, and it's been around for so long that changing such
a basic part of its behavior cannot be done without disrupting many
applications.
You'll need to register appropriate HttpMessageConverter objects. Those will have access to the response's InputStream, through an HttpInputMessage object.
As Abdull suggests, Spring does come with an HttpMessageConverter implementation for Resource which itself wraps an InputStream, ResourceHttpMessageConverter. It doesn't support all Resource types, but since you should be programming to interfaces anyway, you should just use the superinterface Resource.
The current implementation (4.3.5), will return a ByteArrayResource with the content of the response stream copied to a new ByteArrayInputStream which you can access.
You don't have to close the stream. The RestTemplate takes care of that for you. (This is unfortunate if you try to use a InputStreamResource, another type supported by the ResourceHttpMessageConverter, because it wraps the underlying response's InputStream but is closed before it can be exposed to your client code.)
I encountered the same issue and solved it by extending RestTemplate and closing the connection only after the stream is read.
you can see the code here: https://github.com/ItamarBenjamin/stream-rest-template
Thanks to Abhijit Sarkar's answer for leading the way.
I needed to download a heavy JSON stream and break it into small streamable manageable pieces of data.
The JSON is composed of objects that have big properties: such big properties can be serialized to a file, and thus removed from the unmarshalled JSON object.
Another use case is to download a JSON stream object by object, process it like a map/reduce algorythm and produce a single output without having to load the whole stream in memory.
Yet another use case is to read a big JSON file and only pick a few objects based on a condition, while unmarshalling to Plain Old Java Objects.
Here is an example: we'd like to stream a very huge JSON file that is an array, and we'd like to retrieve only the first object in the array.
Given this big file on a server, available at http://example.org/testings.json :
[
{ "property1": "value1", "property2": "value2", "property3": "value3" },
{ "property1": "value1", "property2": "value2", "property3": "value3" },
... 1446481 objects => a file of 104 MB => take quite long to download...
]
Each row of this JSON array can be parsed as this object:
#lombok.Data
public class Testing {
String property1;
String property2;
String property3;
}
You need this class make the parsing code reusable:
import com.fasterxml.jackson.core.JsonParser;
import java.io.IOException;
#FunctionalInterface
public interface JsonStreamer<R> {
/**
* Parse the given JSON stream, process it, and optionally return an object.<br>
* The returned object can represent a downsized parsed version of the stream, or the result of a map/reduce processing, or null...
*
* #param jsonParser the parser to use while streaming JSON for processing
* #return the optional result of the process (can be {#link Void} if processing returns nothing)
* #throws IOException on streaming problem (you are also strongly encouraged to throw HttpMessageNotReadableException on parsing error)
*/
R stream(JsonParser jsonParser) throws IOException;
}
And this class to parse:
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
#AllArgsConstructor
public class StreamingHttpMessageConverter<R> implements HttpMessageConverter<R> {
private final JsonFactory factory;
private final JsonStreamer<R> jsonStreamer;
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return MediaType.APPLICATION_JSON.isCompatibleWith(mediaType);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return false; // We only support reading from an InputStream
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.singletonList(MediaType.APPLICATION_JSON);
}
#Override
public R read(Class<? extends R> clazz, HttpInputMessage inputMessage) throws IOException {
try (InputStream inputStream = inputMessage.getBody();
JsonParser parser = factory.createParser(inputStream)) {
return jsonStreamer.stream(parser);
}
}
#Override
public void write(R result, MediaType contentType, HttpOutputMessage outputMessage) {
throw new UnsupportedOperationException();
}
}
Then, here is the code to use to stream the HTTP response, parse the JSON array and return only the first unmarshalled object:
// You should #Autowire these:
JsonFactory jsonFactory = new JsonFactory();
ObjectMapper objectMapper = new ObjectMapper();
RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
// If detectRequestFactory true (default): HttpComponentsClientHttpRequestFactory will be used and it will consume the entire HTTP response, even if we close the stream early
// If detectRequestFactory false: SimpleClientHttpRequestFactory will be used and it will close the connection as soon as we ask it to
RestTemplate restTemplate = restTemplateBuilder.detectRequestFactory(false).messageConverters(
new StreamingHttpMessageConverter<>(jsonFactory, jsonParser -> {
// While you use a low-level JsonParser to not load everything in memory at once,
// you can still profit from smaller object mapping with the ObjectMapper
if (!jsonParser.isClosed() && jsonParser.nextToken() == JsonToken.START_ARRAY) {
if (!jsonParser.isClosed() && jsonParser.nextToken() == JsonToken.START_OBJECT) {
return objectMapper.readValue(jsonParser, Testing.class);
}
}
return null;
})
).build();
final Testing firstTesting = restTemplate.getForObject("http://example.org/testings.json", Testing.class);
log.debug("First testing object: {}", firstTesting);
You can pass in your own response extractor. Here is an example where I write out the json to disk in a streaming fashion -
RestTemplate restTemplate = new RestTemplateBuilder().basicAuthentication("user", "their_password" ).build();
int responseSize = restTemplate.execute(uri,
HttpMethod.POST,
(ClientHttpRequest requestCallback) -> {
requestCallback.getHeaders().setContentType(MediaType.APPLICATION_JSON);
requestCallback.getBody().write(body.getBytes());
},
responseExtractor -> {
FileOutputStream fos = new FileOutputStream(new File("out.json"));
return StreamUtils.copy(responseExtractor.getBody(), fos);
}
)
Very simple, yet efficient solution would be using ResponseExtractor. It's especially useful when you want to operate on very large InputStream and your RAM is limited.
Here is how you should be implementing it:
public void consumerInputStreamWithoutBuffering(String url, Consumer<InputStream> streamConsumer) throws IOException {
final ResponseExtractor responseExtractor =
(ClientHttpResponse clientHttpResponse) -> {
streamConsumer.accept(clientHttpResponse.getBody());
return null;
};
restTemplate.execute(url, HttpMethod.GET, null, responseExtractor);
}
And then, invoke the method anywhere you need:
Consumer<InputStream> doWhileDownloading = inputStream -> {
//Use inputStream for your business logic...
};
consumerInputStreamWithoutBuffering("https://localhost.com/download", doWhileDownloading);
Please, be aware of the following common pitfall:
public InputStream getInputStreamFromResponse(String url) throws IOException {
final ResponseExtractor<InputStream> responseExtractor =
clientHttpResponse -> clientHttpResponse.getBody();
return restTemplate.execute(url, HttpMethod.GET, null, responseExtractor);
}
Here InputStream will be closed before you can access it
I solve it by doing that.
I hope it will help you all.
#GetMapping("largeFile")
public ResponseEntity<InputStreamResource> downloadLargeFile(
#RequestParam("fileName") String fileName
) throws IOException {
RestTemplate restTemplate = new RestTemplate();
// Optional Accept header
RequestCallback requestCallback = request -> request.getHeaders()
.setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
// Streams the response instead of loading it all in memory
ResponseExtractor<InputStreamResource> responseExtractor = response -> {
// Here I write the response to a file but do what you like
Path path = Paths.get("tmp/" + fileName);
Files.copy(response.getBody(), path, StandardCopyOption.REPLACE_EXISTING);
return new InputStreamResource(new FileInputStream(String.format("tmp/%s", fileName)));
};
InputStreamResource response = restTemplate.execute(
String.format("http://%s:%s/file/largeFileRestTemplate?fileName=%s", host, "9091", fileName),
HttpMethod.GET,
requestCallback,
responseExtractor
);
return ResponseEntity
.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=%s", fileName))
.body(response);
}
As a variant you can consume response as bytes and than convert to stream
byte data[] = restTemplate.execute(link, HttpMethod.GET, null, new BinaryFileExtractor());
return new ByteArrayInputStream(data);
Extractor is
public class BinaryFileExtractor implements ResponseExtractor<byte[]> {
#Override
public byte[] extractData(ClientHttpResponse response) throws IOException {
return ByteStreams.toByteArray(response.getBody());
}
}
I have a Jersey 2 application containing resources that consume and produce json. My requirement is to add a signature to an Authorization response header generated from a combination of various piece of response data (similar to the Amazon Webservices request signature). One of these pieces of data is the response body but I cant see that there are any filter or interception points that will allow me access to the json content. I imagine this is mainly because the response outputstream is for writing not reading.
Any ideas as to how I can read the response body - or alternative approaches ?
Thank you.
My understanding is that when your application is responding to a request, you want to modify the Authorization header by adding a signature to it's value.
If that's the case, you want to implement a ContainerResponseFilter:
public class MyContainerResponseFilter implements ContainerResponseFilter {
#Override
public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException {
// You can get the body of the response from the ContainerResponseContext
Object entity = containerResponseContext.getEntity();
// You'll need to know what kind of Object the entity is in order to do something useful though
// You can get some data using these functions
Class<?> entityClass = containerResponseContext.getEntityClass();
Type entityType = containerResponseContext.getEntityType();
// And/or by looking at the ContainerRequestContext and knowing what the response entity will be
String method = containerRequestContext.getMethod();
UriInfo uriInfo = containerRequestContext.getUriInfo();
// Then you can modify your Authorization header in some way
String authorizationHeaderValue = containerResponseContext.getHeaderString(HttpHeaders.AUTHORIZATION);
authorizationHeaderValue = authorizationHeaderValue + " a signature you calculated";
containerResponseContext.getHeaders().putSingle(HttpHeaders.AUTHORIZATION, authorizationHeaderValue);
}
}
Be warned that the filter function will be called for all requests to your application, even when Jersey couldn't find a matching resource for the request path, so you may have to do some extra checking.
You can implement ContainerRequestFilter in order to access the content, and once you are finished with your interception logic, forward it to the request. E.g.
import java.io.*;
import com.sun.jersey.api.container.ContainerException;
import com.sun.jersey.core.util.ReaderWriter;
import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.container.ContainerRequestFilter;
public class ExampleFilter implements ContainerRequestFilter {
#Override
public ContainerRequest filter(ContainerRequest req) {
try(InputStream in = req.getEntityInputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();) {
if (in.available() > 0) {
StringBuilder content = new StringBuilder();
ReaderWriter.writeTo(in, out);
byte[] entity = out.toByteArray();
if (entity.length > 0) {
content.append(new String(entity)).append("\n");
System.out.println(content);
}
req.setEntityInputStream(new ByteArrayInputStream(entity));
}
} catch (IOException ex) {
//handle exception
}
return req;
}
}