RestTemplate 不适用于 S3 预签名放置 URL
RestTemplate not working with S3 pre-signed put URLs
我有一台服务器生成 AWS S3 预签名 PUT URL,然后我尝试使用 RestTemplate 将 byte[]
上传到 URL 中,代码如下:
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));
当我 运行 那个代码时,我得到这个错误:
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(RendererAppController.java:95)
不幸的是,AWS S3 日志中没有任何内容,所以,我不确定发生了什么。如果我采用完全相同的 URL 并将其放入 IntelliJ IDEA 的 REST 客户端,它就可以正常工作(它在 S3 中创建一个空文件)。
知道我的 Java 代码有什么问题吗?
这是一个完整的示例,它执行签名并尝试将一个小的有效负载上传到 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);
}
}
不要将您的 URL 转换为字符串。而是将其转换为 URI。我认为在转换为 String 时存在一些编码问题。例如,字符串格式的 URL 有 %252F
,而它应该只是 %2F
。看起来像是某种双重编码问题。
保留为 URL...
URL putUrl = amazonS3Client.generatePresignedUrl(urlRequest);
转换为 URI...
ResponseEntity<String> re = restTemplate.exchange(putUrl.toURI(), org.springframework.http.HttpMethod.PUT, entity, String.class);
编辑:更多信息以阐明正在发生的事情。
这里出现的问题是,当您在这种情况下调用 URL.toString()
时,您会得到 URL 的编码字符串表示形式。但是 RestTemplate 需要一个尚未编码的 String url 。 RestTemplate 将为您进行编码。
例如看下面的代码...
public static void main(String[] args) {
RestTemplate rt = new RestTemplate();
rt.exchange("http://foo.com/?var=<val>", HttpMethod.GET, HttpEntity.EMPTY, String.class);
}
当您 运行 从 Spring 收到以下调试消息时,请注意调试消息中的 url 是如何编码的。
[main] DEBUG org.springframework.web.client.RestTemplate - Created GET request for "http://foo.com/?var=%3Cval%3E"
因此您可以看到 RestTemplate 将为您编码传递给它的任何字符串 url。但是 AmazonS3Client 提供的 URL 已经编码。请参阅下面的代码。
URL putUrl = amazonS3Client.generatePresignedUrl(urlRequest);
System.out.println("putUrl.toString = " + putUrl.toString());
这会打印出一个已经编码的字符串。
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
因此,当我将其粘贴到 RestTemplate 的交换方法中时,我得到以下调试消息。
[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"
注意 url 字符串中的每个 %2F
是如何变成 %252F
的。 %2F
是 /
的编码表示。但是 %25
是 %
。所以它编码了一个已经编码的 url 。解决方案是将 URI
对象传递给 RestTemplate.exchange
,而不是将编码的字符串 url.
问题的根源是 url 个字符的双重编码。扩展密钥中有/
,被s3Client.generatePresignedUrl
编码为%2
。当已经编码的字符串传递给 restTemplate.exchange
时,它会在内部转换为 URI,并由 RestTemplate
源代码中的 UriTemplateHandler
第二次编码为 %252
。
@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);
}
所以最简单的解决方案是使用 URL.toURI()
将 URL 转换为 URI。如果在调用 RestTemplate
时没有 URI
而有 String
,那么有两个选项是可能的。
将字符串的 URI 传递给交换方法。
restTemplate.exchange(new URI(putUrl.toString()), HttpMethod.PUT, entity, Void.class);
使用 NONE
编码模式创建默认 UriTemplateHandler
并将其传递给 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);
我有一台服务器生成 AWS S3 预签名 PUT URL,然后我尝试使用 RestTemplate 将 byte[]
上传到 URL 中,代码如下:
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));
当我 运行 那个代码时,我得到这个错误:
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(RendererAppController.java:95)
不幸的是,AWS S3 日志中没有任何内容,所以,我不确定发生了什么。如果我采用完全相同的 URL 并将其放入 IntelliJ IDEA 的 REST 客户端,它就可以正常工作(它在 S3 中创建一个空文件)。
知道我的 Java 代码有什么问题吗?
这是一个完整的示例,它执行签名并尝试将一个小的有效负载上传到 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);
}
}
不要将您的 URL 转换为字符串。而是将其转换为 URI。我认为在转换为 String 时存在一些编码问题。例如,字符串格式的 URL 有 %252F
,而它应该只是 %2F
。看起来像是某种双重编码问题。
保留为 URL...
URL putUrl = amazonS3Client.generatePresignedUrl(urlRequest);
转换为 URI...
ResponseEntity<String> re = restTemplate.exchange(putUrl.toURI(), org.springframework.http.HttpMethod.PUT, entity, String.class);
编辑:更多信息以阐明正在发生的事情。
这里出现的问题是,当您在这种情况下调用 URL.toString()
时,您会得到 URL 的编码字符串表示形式。但是 RestTemplate 需要一个尚未编码的 String url 。 RestTemplate 将为您进行编码。
例如看下面的代码...
public static void main(String[] args) {
RestTemplate rt = new RestTemplate();
rt.exchange("http://foo.com/?var=<val>", HttpMethod.GET, HttpEntity.EMPTY, String.class);
}
当您 运行 从 Spring 收到以下调试消息时,请注意调试消息中的 url 是如何编码的。
[main] DEBUG org.springframework.web.client.RestTemplate - Created GET request for "http://foo.com/?var=%3Cval%3E"
因此您可以看到 RestTemplate 将为您编码传递给它的任何字符串 url。但是 AmazonS3Client 提供的 URL 已经编码。请参阅下面的代码。
URL putUrl = amazonS3Client.generatePresignedUrl(urlRequest);
System.out.println("putUrl.toString = " + putUrl.toString());
这会打印出一个已经编码的字符串。
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
因此,当我将其粘贴到 RestTemplate 的交换方法中时,我得到以下调试消息。
[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"
注意 url 字符串中的每个 %2F
是如何变成 %252F
的。 %2F
是 /
的编码表示。但是 %25
是 %
。所以它编码了一个已经编码的 url 。解决方案是将 URI
对象传递给 RestTemplate.exchange
,而不是将编码的字符串 url.
问题的根源是 url 个字符的双重编码。扩展密钥中有/
,被s3Client.generatePresignedUrl
编码为%2
。当已经编码的字符串传递给 restTemplate.exchange
时,它会在内部转换为 URI,并由 RestTemplate
源代码中的 UriTemplateHandler
第二次编码为 %252
。
@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);
}
所以最简单的解决方案是使用 URL.toURI()
将 URL 转换为 URI。如果在调用 RestTemplate
时没有 URI
而有 String
,那么有两个选项是可能的。
将字符串的 URI 传递给交换方法。
restTemplate.exchange(new URI(putUrl.toString()), HttpMethod.PUT, entity, Void.class);
使用 NONE
编码模式创建默认 UriTemplateHandler
并将其传递给 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);